Skip to content

Commit ff25781

Browse files
mikemckamcmero
authored andcommitted
opal metadata detection cherry pick and merge
1 parent b28b602 commit ff25781

1 file changed

Lines changed: 56 additions & 45 deletions

File tree

‎bin/cellmeasurement.py‎

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)