@@ -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 :
0 commit comments