Skip to content

Commit

Permalink
Support falcon datasets with latest micromanager (#237)
Browse files Browse the repository at this point in the history
* use "DefaultXYStage" to get stage positions

* read new MM tile creator strings

* style

* fix tests

* simplify getting stage position name

* use regex for HCS label parsing

* bugfix

* more strict label matching

* add DefaultXYStage to ndtiff stage_positions metadata

* modify getting stage positions

* add tests that stage position is present in metadata

---------

Co-authored-by: Ivan Ivanov <[email protected]>
  • Loading branch information
talonchandler and ieivanov authored Nov 12, 2024
1 parent 16b5571 commit 1957ea0
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 33 deletions.
8 changes: 4 additions & 4 deletions iohub/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,10 @@ def _get_position_coords(self):
if not self.reader.stage_positions:
raise ValueError("Stage positions not available.")
for idx, pos in enumerate(self.reader.stage_positions):
stage_pos = (
pos.get("XYStage") or pos.get("XY") or pos.get("XY Stage")
)
if stage_pos is None:
try:
xy_stage = pos["DefaultXYStage"]
stage_pos = pos[xy_stage]
except KeyError:
raise ValueError(
f"Stage position is not available for position {idx}"
)
Expand Down
68 changes: 40 additions & 28 deletions iohub/mm_fov.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import re
from pathlib import Path

from xarray import DataArray
Expand Down Expand Up @@ -109,42 +110,53 @@ def stage_positions(self, value):
@property
def hcs_position_labels(self):
"""Parse plate position labels generated by the HCS position generator,
e.g. 'A1-Site_0' or '1-Pos000_000', and split into row, column, and
FOV names.
e.g. 'A1-Site_0', '1-Pos000_000', or 'Pos-9-000_000', and split into
row, column, and FOV names.
Returns
-------
list[tuple[str, str, str]]
FOV name paths, e.g. ('A', '1', '0') or ('0', '1', '000000')
FOV name paths, e.g. ('A', '1', '0'), ('0', '1', '000000'), or
('0', '9', '000000').
"""
if not self.stage_positions:
raise ValueError("Stage position metadata not available.")

try:
# Look for "'A1-Site_0', 'H12-Site_1', ... " format
labels = [
pos["Label"].split("-Site_") for pos in self.stage_positions
]
return [(well[0], well[1:], fov) for well, fov in labels]
except Exception:
try:
# Look for "'1-Pos000_000', '2-Pos000_001', ... "
# and split into ('1', '000_000'), ...
labels = [
pos["Label"].split("-Pos") for pos in self.stage_positions
]
# remove underscore from FOV name, i.e. '000_000'
# collect all wells in row '0' so output is
# ('0', '1', '000000')
return [
("0", col, fov.replace("_", "")) for col, fov in labels
]
except Exception:
labels = [pos.get("Label") for pos in self.stage_positions]
raise ValueError(
"HCS position labels are in the format of "
"'A1-Site_0', 'H12-Site_1', or '1-Pos000_000' "
f"Got labels {labels}"
)
labels = [pos["Label"] for pos in self.stage_positions]
except KeyError:
raise ValueError("Stage positions do not have labels.")

# See https://chatgpt.com/share/e/670097cc-2854-8008-bd33-b54cad7c99b9
pattern = re.compile(
r"([A-Z])(\d+)-Site_(\d+)|"
r"Pos-(\d+)-(\d+)_(\d+)|"
r"(\d+)-Pos(\d+)_(\d+)"
)
row_col_fov = []
for label in labels:
if (match := re.match(pattern, label)) is not None:
if match.group(1): # "A1-Site_0" case
row_col_fov.append(
(match.group(1), match.group(2), match.group(3))
)
elif match.group(4): # "Pos-5-000_005" case
row_col_fov.append(
("0", match.group(4), match.group(5) + match.group(6))
)
elif match.group(7): # "1-Pos000_000" case
row_col_fov.append(
("0", match.group(7), match.group(8) + match.group(9))
)

if not row_col_fov:
raise ValueError(
"HCS position labels are in the format of "
"'A1-Site_0', 'H12-Site_1', or '1-Pos000_000', "
f"or 'Pos-1-000_000'. Got labels {labels}"
)

return row_col_fov

@property
def zyx_scale(self) -> tuple[float, float, float]:
Expand Down
4 changes: 3 additions & 1 deletion iohub/ndtiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ def _get_summary_metadata(self):
"YPosition_um_Intended",
]
):
position_metadata[img_metadata["Core-XYStage"]] = (
xy_stage = img_metadata["Core-XYStage"]
position_metadata["DefaultXYStage"] = xy_stage
position_metadata[xy_stage] = (
img_metadata["XPosition_um_Intended"],
img_metadata["YPosition_um_Intended"],
)
Expand Down
2 changes: 2 additions & 0 deletions tests/mmstack/test_mmstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ def test_mmstack_metadata(ome_tiff):
assert isinstance(mmstack.micromanager_metadata, dict)
assert mmstack.micromanager_metadata["Summary"]
assert mmstack.micromanager_summary
if mmstack.stage_positions:
assert "DefaultXYStage" in mmstack.stage_positions[0]


def test_fov_axes_names(ome_tiff):
Expand Down
2 changes: 2 additions & 0 deletions tests/test_ndtiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ def test_dataset_metadata(ndtiff_dataset):
assert isinstance(dataset.micromanager_metadata, dict)
assert dataset.micromanager_metadata["Summary"]
assert isinstance(dataset.micromanager_summary, dict)
if dataset.stage_positions:
assert "DefaultXYStage" in dataset.stage_positions[0]


@pytest.mark.parametrize("ndtiff_v2", ndtiff_v2_datasets)
Expand Down

0 comments on commit 1957ea0

Please sign in to comment.