@@ -218,47 +218,66 @@ def load_image(path: str) -> Tuple[np.ndarray, List[str]]:
218218 ch_names : list of str
219219 Human-readable channel names, length ``C``.
220220 """
221- img = tifffile .imread (path )
221+ # Use the same loading strategy as cellsam_segment.py to ensure channel
222+ # detection stays consistent across modules. Iterating over ``tif.pages``
223+ # explicitly avoids ``tifffile.imread`` collapsing a multi-page TIFF
224+ # (e.g. an OPAL QPTIFF or BACKSUB output) into a single 2-D plane.
225+ import json as _json
226+
222227 ch_names : List [str ] = []
223228
224229 with tifffile .TiffFile (path ) as tf :
225- # Strategy 1: OME-XML Channel/@Name (covers OME-TIFF, OPAL QPTIFF, COMET)
226- try :
227- import xml .etree .ElementTree as ET
228-
229- ome = tf .ome_metadata
230- # Fallback: OPAL QPTIFF stores OME-XML in first page ImageDescription
231- if not ome and tf .pages :
232- first_desc = tf .pages [0 ].description
233- if isinstance (first_desc , str ) and first_desc .strip ().startswith ("<" ):
234- ome = first_desc
235- if ome :
236- root = ET .fromstring (ome )
237- ns = {"ome" : "http://www.openmicroscopy.org/Schemas/OME/2016-06" }
238- channels = root .findall (".//ome:Channel" , ns )
239- if channels :
240- ch_names = [
241- ch .get ("Name" ) or ch .get ("ID" ) or ""
242- for ch in channels
243- ]
244- except Exception as e :
245- print (f"Warning: failed to parse OME metadata for channel names ({ e } )" )
246- ch_names = []
247-
248- # Strategy 2: MIBI JSON metadata in per-page ImageDescription
249- if not ch_names :
230+ first_page = tf .pages [0 ] if tf .pages else None
231+ is_mibi = False
232+
233+ # Detect MIBI-style per-page JSON metadata.
234+ if first_page is not None :
250235 try :
251- import json as _json
252- first_desc = tf .pages [0 ].description if tf .pages else ""
253- _json .loads (first_desc ) # probe for JSON
254- ch_names = [
255- str (_json .loads (p .description ).get ("channel.target" , "" ))
256- for p in tf .pages
257- ]
258- except (ValueError , TypeError , KeyError , AttributeError ):
236+ first_desc = _json .loads (first_page .description )
237+ is_mibi = "channel.target" in first_desc
238+ except (ValueError , TypeError ):
259239 pass
260240
261- # Strategy 3: ImageJ metadata Labels
241+ if is_mibi :
242+ pages = []
243+ for page in tf .pages :
244+ desc = _json .loads (page .description )
245+ ch_names .append (str (desc .get ("channel.target" , "" )))
246+ pages .append (page .asarray ())
247+ img = np .stack (pages , axis = 0 )
248+ elif len (tf .pages ) > 1 :
249+ # Multi-page TIFF where each page is one channel (OME-TIFF, OPAL
250+ # QPTIFF, COMET, BACKSUB output, etc.).
251+ img = np .stack ([page .asarray () for page in tf .pages ], axis = 0 )
252+ else :
253+ # Single-page TIFF — could be (H, W) or interleaved (H, W, C).
254+ img = tf .asarray ()
255+
256+ # Strategy 1: OME-XML Channel/@Name (OME-TIFF, OPAL QPTIFF, COMET)
257+ if not ch_names :
258+ try :
259+ import xml .etree .ElementTree as ET
260+
261+ ome = tf .ome_metadata
262+ # OPAL QPTIFF stores OME-XML in first page ImageDescription
263+ if not ome and first_page is not None :
264+ first_desc = first_page .description
265+ if isinstance (first_desc , str ) and first_desc .strip ().startswith ("<" ):
266+ ome = first_desc
267+ if ome :
268+ root = ET .fromstring (ome )
269+ ns = {"ome" : "http://www.openmicroscopy.org/Schemas/OME/2016-06" }
270+ channels = root .findall (".//ome:Channel" , ns )
271+ if channels :
272+ ch_names = [
273+ ch .get ("Name" ) or ch .get ("ID" ) or ""
274+ for ch in channels
275+ ]
276+ except Exception as e :
277+ print (f"Warning: failed to parse OME metadata for channel names ({ e } )" )
278+ ch_names = []
279+
280+ # Strategy 2: ImageJ metadata Labels
262281 if not ch_names :
263282 try :
264283 ij = tf .imagej_metadata
@@ -267,23 +286,15 @@ def load_image(path: str) -> Tuple[np.ndarray, List[str]]:
267286 except Exception :
268287 pass
269288
270- # Record the TIFF page count while we still have the file open.
271- # This is used below to disambiguate (C,H,W) vs (H,W,C) layout for
272- # 3D plain TIFFs without metadata: if page count == first dimension,
273- # the array is already (C,H,W); if page count == 1 and img is 3D,
274- # the TIFF was loaded as a single (H,W,C) interleaved frame.
275289 n_pages = len (tf .pages ) if tf .pages else 0
276290
277291 # --- Normalize image shape to (C, H, W) regardless of input layout ---
278292 if img .ndim == 2 :
279293 # Single-channel greyscale: promote to (1, H, W)
280294 img = img [np .newaxis , ...]
281295 elif img .ndim == 3 :
282- # Disambiguate (C, H, W) vs (H, W, C) using TIFF page count.
283- # If the number of pages matches the first dimension the array
284- # is already page-per-channel (C, H, W).
285- # If pages == 1, the single frame was loaded as (H, W, C).
286- # Fall back to the shape heuristic only when page count is ambiguous.
296+ # If we stacked pages above, the array is already (C, H, W).
297+ # Only single-page TIFFs can land here as interleaved (H, W, C).
287298 if n_pages > 1 and img .shape [0 ] == n_pages :
288299 pass # already (C, H, W) — each page is one channel
289300 elif n_pages == 1 or (n_pages > 1 and img .shape [2 ] == n_pages ):
0 commit comments