Skip to content

Commit 580e628

Browse files
d33bspre-commit-ci-lite[bot]jenna-tomkinson
authored
Fix dependency update and test issues (#190)
* fix dependencies and tests * [pre-commit.ci lite] apply automatic fixes * add comments for conditionals Co-Authored-By: Jenna Tomkinson <[email protected]> --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jenna Tomkinson <[email protected]>
1 parent 87f3b88 commit 580e628

File tree

6 files changed

+133
-42
lines changed

6 files changed

+133
-42
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ repos:
5454
hooks:
5555
- id: actionlint
5656
- repo: https://github.com/astral-sh/ruff-pre-commit
57-
rev: "v0.15.0"
57+
rev: "v0.15.1"
5858
hooks:
5959
- id: ruff-format
6060
- id: ruff-check

media/coverage-badge.svg

Lines changed: 1 addition & 1 deletion
Loading

poetry.lock

Lines changed: 14 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ sqlalchemy = ">=1.3.6,<3"
5858
coverage = "^7.6.0"
5959
jupyterlab = "^4.3.0"
6060
jupytext = "^1.16.4"
61+
# jupyterlab/httpx requires a modern httpcore; avoid resolver selecting 0.13.x
62+
httpcore = ">=0.18,<0.19"
6163
black = ">=24.10,<27.0"
6264
isort = ">=5.13.2,<8.0.0"
6365
jupyterlab-code-formatter = "^3.0.2"

src/cytodataframe/frame.py

Lines changed: 96 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,10 @@ def to_ome_parquet( # noqa: PLR0915, PLR0912, C901
716716
"CytoDataFrame.to_ome_parquet requires the optional 'ome-arrow' "
717717
"dependency. Install it via `pip install ome-arrow`."
718718
) from exc
719+
try:
720+
from ome_arrow import from_numpy as ome_from_numpy # type: ignore
721+
except ImportError:
722+
ome_from_numpy = None
719723

720724
try:
721725
import importlib.metadata as importlib_metadata
@@ -907,24 +911,64 @@ def to_ome_parquet( # noqa: PLR0915, PLR0912, C901
907911
column_values[col_name].append(None)
908912
continue
909913

910-
temp_path = (
911-
tmpdir_path
912-
/ f"{sanitized_col}_{layer_key}_{uuid.uuid4().hex}.tiff"
913-
)
914914
try:
915-
with warnings.catch_warnings():
916-
warnings.simplefilter("ignore", UserWarning)
917-
imageio.imwrite(temp_path, layer_array, format="tiff")
918-
except Exception as exc:
919-
logger.error(
920-
"Failed to write temporary TIFF for OMEArrow (%s): %s",
921-
layer_key,
922-
exc,
923-
)
924-
column_values[col_name].append(None)
925-
continue
926-
try:
927-
ome_struct = OMEArrow(data=str(temp_path)).data
915+
# Prefer direct in-memory conversion when available.
916+
# This avoids TIFF round-trips and keeps channel
917+
# layout explicit.
918+
if (
919+
ome_from_numpy is not None
920+
and layer_array.ndim < MIN_VOLUME_NDIM
921+
):
922+
ome_struct = ome_from_numpy(
923+
np.asarray(layer_array),
924+
dim_order="YX",
925+
)
926+
elif (
927+
ome_from_numpy is not None
928+
and layer_array.ndim == MIN_VOLUME_NDIM
929+
and layer_array.shape[-1] in RGB_LIKE_CHANNEL_COUNTS
930+
):
931+
# OME-Arrow expects channels-first for
932+
# 2D multi-channel arrays.
933+
channel_first = np.moveaxis(
934+
np.asarray(layer_array), -1, 0
935+
)
936+
ome_struct = ome_from_numpy(
937+
channel_first,
938+
dim_order="CYX",
939+
)
940+
elif (
941+
layer_array.ndim == MIN_VOLUME_NDIM
942+
and layer_array.shape[-1] in RGB_LIKE_CHANNEL_COUNTS
943+
):
944+
# Compatibility fallback for environments where
945+
# `ome_arrow.from_numpy` is not available.
946+
temp_path = tmpdir_path / (
947+
f"{sanitized_col}_{layer_key}_"
948+
f"{uuid.uuid4().hex}.tiff"
949+
)
950+
with warnings.catch_warnings():
951+
warnings.simplefilter("ignore", UserWarning)
952+
imageio.imwrite(
953+
temp_path,
954+
layer_array,
955+
format="tiff",
956+
)
957+
ome_struct = OMEArrow(data=str(temp_path)).data
958+
else:
959+
# Generic fallback for all other array shapes.
960+
temp_path = tmpdir_path / (
961+
f"{sanitized_col}_{layer_key}_"
962+
f"{uuid.uuid4().hex}.tiff"
963+
)
964+
with warnings.catch_warnings():
965+
warnings.simplefilter("ignore", UserWarning)
966+
imageio.imwrite(
967+
temp_path,
968+
layer_array,
969+
format="tiff",
970+
)
971+
ome_struct = OMEArrow(data=str(temp_path)).data
928972
if hasattr(ome_struct, "as_py"):
929973
ome_struct = ome_struct.as_py()
930974
except Exception as exc:
@@ -1255,7 +1299,7 @@ def search_for_mask_or_outline( # noqa: PLR0913, PLR0911, C901
12551299

12561300
return None, None
12571301

1258-
def _extract_array_from_ome_arrow( # noqa: PLR0911
1302+
def _extract_array_from_ome_arrow( # noqa: C901, PLR0911, PLR0912
12591303
self: CytoDataFrame_type,
12601304
data_value: Any,
12611305
) -> Optional[np.ndarray]:
@@ -1269,6 +1313,7 @@ def _extract_array_from_ome_arrow( # noqa: PLR0911
12691313
size_x = int(pixels_meta.get("size_x"))
12701314
size_y = int(pixels_meta.get("size_y"))
12711315
size_z = int(pixels_meta.get("size_z") or 1)
1316+
size_c = int(pixels_meta.get("size_c") or 1)
12721317
planes = data_value.get("planes")
12731318

12741319
if size_x <= 0 or size_y <= 0 or planes is None:
@@ -1284,21 +1329,43 @@ def _extract_array_from_ome_arrow( # noqa: PLR0911
12841329
if not plane_entries:
12851330
return None
12861331

1287-
plane = plane_entries[0]
1288-
pixels = plane.get("pixels")
1289-
if pixels is None:
1290-
return None
1291-
1292-
np_pixels = np.asarray(pixels)
12931332
base = size_x * size_y
1294-
if base <= 0 or np_pixels.size == 0 or np_pixels.size % base != 0:
1333+
if base <= 0:
12951334
return None
12961335

1297-
channel_count = np_pixels.size // base
1298-
if channel_count == 1:
1299-
array = np_pixels.reshape((size_y, size_x))
1336+
if size_c > 1:
1337+
planes_by_c = {}
1338+
for plane in plane_entries:
1339+
if int(plane.get("t") or 0) != 0 or int(plane.get("z") or 0) != 0:
1340+
continue
1341+
channel_index = int(plane.get("c") or 0)
1342+
pixels = plane.get("pixels")
1343+
if pixels is None:
1344+
continue
1345+
np_pixels = np.asarray(pixels)
1346+
if np_pixels.size != base:
1347+
continue
1348+
planes_by_c[channel_index] = np_pixels.reshape((size_y, size_x))
1349+
1350+
if len(planes_by_c) != size_c:
1351+
return None
1352+
1353+
array = np.stack([planes_by_c[c] for c in range(size_c)], axis=-1)
13001354
else:
1301-
array = np_pixels.reshape((size_y, size_x, channel_count))
1355+
plane = plane_entries[0]
1356+
pixels = plane.get("pixels")
1357+
if pixels is None:
1358+
return None
1359+
1360+
np_pixels = np.asarray(pixels)
1361+
if np_pixels.size == 0 or np_pixels.size % base != 0:
1362+
return None
1363+
1364+
channel_count = np_pixels.size // base
1365+
if channel_count == 1:
1366+
array = np_pixels.reshape((size_y, size_x))
1367+
else:
1368+
array = np_pixels.reshape((size_y, size_x, channel_count))
13021369

13031370
return self._ensure_uint8(array)
13041371
except Exception as exc:

tests/test_frame.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,25 @@ def test_is_3d_image_array_accepts_thin_small_volume_shapes() -> None:
11241124
assert CytoDataFrame._is_3d_image_array(singleton_x) is True
11251125

11261126

1127+
def test_extract_array_from_ome_arrow_rebuilds_multichannel_planes() -> None:
1128+
cdf = CytoDataFrame(pd.DataFrame({"A": [1]}))
1129+
data_value = {
1130+
"type": "ome.arrow",
1131+
"pixels_meta": {"size_x": 2, "size_y": 2, "size_z": 1, "size_c": 3},
1132+
"planes": [
1133+
{"t": 0, "c": 0, "z": 0, "pixels": [10, 20, 30, 40]},
1134+
{"t": 0, "c": 1, "z": 0, "pixels": [50, 60, 70, 80]},
1135+
{"t": 0, "c": 2, "z": 0, "pixels": [90, 100, 110, 120]},
1136+
],
1137+
}
1138+
1139+
image = cdf._extract_array_from_ome_arrow(data_value)
1140+
1141+
assert image is not None
1142+
assert image.shape == (2, 2, 3)
1143+
assert np.array_equal(image[0, 0], np.array([10, 50, 90], dtype=np.uint8))
1144+
1145+
11271146
def _install_fake_pyvista( # noqa: C901
11281147
monkeypatch: pytest.MonkeyPatch,
11291148
screenshot_image: np.ndarray | None = None,

0 commit comments

Comments
 (0)