Skip to content

Commit

Permalink
Extend HCS label parsing (#189)
Browse files Browse the repository at this point in the history
* extend hcs label parsing

* bugfix

* fix p=1 name fetching

* bugfixes

* split three parts in the numerical labels

also strips leading zeros
so that the FOV names start with '0'
and is compatible with napari-ome-zarr

* fix import sorting

* better ndtiff reading them p_idx=0 missing

* revert hcs position label parsing

* select hcs datasets

* simplify _get_pos_names

---------

Co-authored-by: Ziwen Liu <[email protected]>
  • Loading branch information
ieivanov and ziw-liu authored Oct 31, 2023
1 parent ca593de commit f5b2e72
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 29 deletions.
17 changes: 7 additions & 10 deletions iohub/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,16 +307,13 @@ def _get_pos_names(self):
"""Append a list of pos names in ascending order
(order in which they were acquired).
"""
if self.p > 1:
self.pos_names = []
for p in range(self.p):
try:
name = self.reader.stage_positions[p]["Label"]
except (IndexError, KeyError):
name = p
self.pos_names.append(name)
else:
self.pos_names = ["0"]
self.pos_names = []
for p in range(self.p):
try:
name = self.reader.stage_positions[p]["Label"]
except (IndexError, KeyError):
name = str(p)
self.pos_names.append(name)

def _get_image_array(self, p: int, t: int, c: int, z: int):
try:
Expand Down
35 changes: 26 additions & 9 deletions iohub/reader_base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Union

import zarr
from numpy.typing import DTypeLike, NDArray

Expand All @@ -11,7 +13,7 @@ def __init__(self):
self.width: int = None
self.dtype: DTypeLike = None
self._mm_meta: dict = None
self._stage_positions: list[dict] = []
self._stage_positions: list[dict[str, Union[str, float]]] = []
self.z_step_size: float = None
self.channel_names: list[str] = None

Expand Down Expand Up @@ -112,24 +114,39 @@ def get_num_positions(self) -> int:
@property
def hcs_position_labels(self):
"""Parse plate position labels generated by the HCS position generator,
e.g. 'A1-Site_0', and split into row, column, and FOV names.
e.g. 'A1-Site_0' or '1-Pos000_000', and split into row, column, and
FOV names.
Returns
-------
list[tuple[str, str, str]]
FOV name paths, e.g. ('A', '1', '0')
FOV name paths, e.g. ('A', '1', '0') or ('0', '1', '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:
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', ... "
f"Got labels {labels}"
)
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}"
)
21 changes: 21 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,27 @@ def setup_mm2gamma_ome_tiffs():
yield test_data, one_folder, rand_folder


@pytest.fixture(scope="function")
def setup_mm2gamma_ome_tiff_hcs():
test_data = pjoin(
os.getcwd(), ".pytest_temp", "test_data", "MM20_ome-tiffs"
)

subfolders = [
f for f in os.listdir(test_data) if os.path.isdir(pjoin(test_data, f))
]
# select datasets with multiple positioons; here they all have 4 positions
hcs_subfolders = [f for f in subfolders if '4p' in f]

# specific folder
one_folder = pjoin(test_data, hcs_subfolders[0])
# random folder
rand_folder = pjoin(test_data, random.choice(hcs_subfolders))
# return path to unzipped folder containing test images
# as well as specific folder paths
yield test_data, one_folder, rand_folder


@pytest.fixture(scope="function")
def setup_mm2gamma_ome_tiffs_incomplete():
"""
Expand Down
52 changes: 42 additions & 10 deletions tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,6 @@ def test_converter_ometiff(
assert intensity == raw_array.sum()


def test_converter_ometiff_hcs_not_available(
setup_test_data, setup_mm2gamma_ome_tiffs
):
_, _, data = setup_mm2gamma_ome_tiffs
with TemporaryDirectory() as tmp_dir:
output = os.path.join(tmp_dir, "converted.zarr")
with pytest.raises(ValueError, match="position"):
_ = TIFFConverter(data, output, hcs_plate=True)


@pytest.fixture(scope="function")
def mock_hcs_ome_tiff_reader(
setup_mm2gamma_ome_tiffs, monkeypatch: pytest.MonkeyPatch
Expand All @@ -117,6 +107,26 @@ def mock_hcs_ome_tiff_reader(
return data, expected_ngff_name


@pytest.fixture(scope="function")
def mock_non_hcs_ome_tiff_reader(
setup_mm2gamma_ome_tiffs, monkeypatch: pytest.MonkeyPatch
):
all_ometiffs, _, _ = setup_mm2gamma_ome_tiffs
# dataset with 4 positions without HCS site names
data = os.path.join(all_ometiffs, "mm2.0-20201209_4p_2t_5z_1c_512k_1")
mock_stage_positions = [
{"Label": "0"},
{"Label": "1"},
{"Label": "2"},
{"Label": "3"},
]
monkeypatch.setattr(
"iohub.convert.MicromanagerOmeTiffReader.stage_positions",
mock_stage_positions,
)
return data


def test_converter_ometiff_mock_hcs(setup_test_data, mock_hcs_ome_tiff_reader):
data, expected_ngff_name = mock_hcs_ome_tiff_reader
with TemporaryDirectory() as tmp_dir:
Expand All @@ -129,6 +139,28 @@ def test_converter_ometiff_mock_hcs(setup_test_data, mock_hcs_ome_tiff_reader):
}


def test_converter_ometiff_mock_non_hcs(mock_non_hcs_ome_tiff_reader):
data = mock_non_hcs_ome_tiff_reader
with TemporaryDirectory() as tmp_dir:
output = os.path.join(tmp_dir, "converted.zarr")
with pytest.raises(ValueError, match="HCS position labels"):
TIFFConverter(data, output, hcs_plate=True)


def test_converter_ometiff_hcs_numerical(
setup_test_data, setup_mm2gamma_ome_tiff_hcs
):
_, data, _ = setup_mm2gamma_ome_tiff_hcs
with TemporaryDirectory() as tmp_dir:
output = os.path.join(tmp_dir, "converted.zarr")
converter = TIFFConverter(data, output, hcs_plate=True)
converter.run()
with open_ome_zarr(output, mode="r") as plate:
for name, _ in plate.positions():
for segment in name.split("/"):
assert segment.isdigit()


@given(**CONVERTER_TEST_GIVEN)
@settings(CONVERTER_TEST_SETTINGS)
def test_converter_ndtiff(
Expand Down

0 comments on commit f5b2e72

Please sign in to comment.