Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend HCS label parsing #189

Merged
merged 11 commits into from
Oct 31, 2023
4 changes: 2 additions & 2 deletions iohub/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,15 @@ def _get_pos_names(self):
"""Append a list of pos names in ascending order
(order in which they were acquired).
"""
if self.p > 1:
try:
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:
except Exception:
ziw-liu marked this conversation as resolved.
Show resolved Hide resolved
self.pos_names = ["0"]

def _get_image_array(self, p: int, t: int, c: int, z: int):
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