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

Convert scale metadata #118

Merged
merged 7 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion iohub/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ def info(files, verbose):
type=str,
help="Data type, 'ometiff', 'ndtiff', 'singlepagetiff'",
)
@click.option(
"--scale-voxels",
"-s",
required=False,
type=bool,
default=True,
help="Write voxel size (XY pixel size and Z-step, in micrometers) "
"as scale coordinate transformation in NGFF. By default true.",
)
@click.option(
"--grid-layout",
"-g",
Expand All @@ -80,12 +89,13 @@ def info(files, verbose):
is_flag=True,
help="Dump postion labels in MM metadata to Omero metadata",
)
def convert(input, output, format, grid_layout, label_positions):
def convert(input, output, format, scale_voxels, grid_layout, label_positions):
"""Converts Micro-Manager TIFF datasets to OME-Zarr"""
converter = TIFFConverter(
input_dir=input,
output_dir=output,
data_type=format,
scale_voxels=scale_voxels,
grid_layout=grid_layout,
label_positions=label_positions,
)
Expand Down
63 changes: 50 additions & 13 deletions iohub/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from tqdm import tqdm

from iohub._version import version as iohub_version
from iohub.ngff import open_ome_zarr
from iohub.ngff import TransformationMeta, open_ome_zarr
from iohub.reader import MicromanagerSequenceReader, read_micromanager


Expand Down Expand Up @@ -82,6 +82,9 @@ class TIFFConverter:
label_positions : bool, optional
Dump postion labels in MM metadata to Omero metadata,
by default False
scale_voxels : bool, optional
Write voxel size (XY pixel size and Z-step) as scaling transform,
by default True
"""

def __init__(
Expand All @@ -92,6 +95,7 @@ def __init__(
grid_layout: int = False,
chunks: tuple[int] = None,
label_positions: bool = False,
scale_voxels: bool = True,
):
logging.debug("Checking output.")
if not output_dir.strip("/").endswith(".zarr"):
Expand Down Expand Up @@ -143,6 +147,7 @@ def __init__(
else:
self._make_default_grid()
self.chunks = chunks if chunks else (1, 1, 1, self.y, self.x)
self.transform = self._scale_voxels() if scale_voxels else None

def _make_default_grid(self):
self.position_grid = np.expand_dims(
Expand Down Expand Up @@ -294,6 +299,36 @@ def _get_channel_names(self):
cns = [str(i) for i in range(self.c)]
return cns

def _scale_voxels(self):
z_um = self.reader.z_step_size
if self.z_dim > 1 and not z_um:
logging.warning(
ziw-liu marked this conversation as resolved.
Show resolved Hide resolved
"Z step size is not available. "
"Setting the Z axis scaling factor to 1."
)
z_um = 1.0
xy_warning = (
" Setting X and Y scaling factors to 1."
" Suppress this warning by setting `scale-voxels` to false."
)
if isinstance(self.reader, MicromanagerSequenceReader):
logging.warning(
"Pixel size detection is not supported for single-page TIFFs."
+ xy_warning
)
xy_um = 1.0
else:
try:
xy_um = self.reader.xy_pixel_size
except AttributeError as e:
logging.warning(str(e) + xy_warning)
xy_um = 1.0
return [
TransformationMeta(
type="scale", scale=[1.0, 1.0, z_um, xy_um, xy_um]
)
]

def _init_hcs_arrays(self):
self.writer = open_ome_zarr(
self.output_dir,
Expand All @@ -303,23 +338,25 @@ def _init_hcs_arrays(self):
version="0.4",
)
self.well_list = []
arr_kwargs = {
"name": "0",
"shape": (
self.t if self.t != 0 else 1,
self.c if self.c != 0 else 1,
self.z if self.z != 0 else 1,
self.y,
self.x,
),
"dtype": self.reader.dtype,
"chunks": self.chunks,
"transform": self.transform,
}
for row, columns in enumerate(self.position_grid):
for column in columns:
pos_name = self.pos_names[len(self.well_list)]
pos = self.writer.create_position(row, column, pos_name="0")
self.well_list.append(os.path.join(str(row), str(column)))
_ = pos.zgroup.zeros(
"0",
shape=(
self.t if self.t != 0 else 1,
self.c if self.c != 0 else 1,
self.z if self.z != 0 else 1,
self.y,
self.x,
),
chunks=self.chunks,
)
pos._create_image_meta("0")
_ = pos.create_zeros(**arr_kwargs)
pos.metadata.omero.name = pos_name
pos.dump_meta()

Expand Down
43 changes: 29 additions & 14 deletions iohub/multipagetiff.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import glob
import logging
import os
from copy import copy

Expand Down Expand Up @@ -34,7 +35,9 @@ def __init__(self, folder: str, extract_data: bool = False):

# Grab all image files
self.data_directory = folder
self._files = glob.glob(os.path.join(self.data_directory, "*.ome.tif"))
self._files = sorted(
glob.glob(os.path.join(self.data_directory, "*.ome.tif"))
)

# Generate Data Specific Properties
self.coords = None
Expand All @@ -47,10 +50,9 @@ def __init__(self, folder: str, extract_data: bool = False):
self.slices = 0
self.height = 0
self.width = 0
self._set_dtype()
self._infer_image_meta()

# Initialize MM attributes
self.z_step_size = None
self.channel_names = []

# Read MM data
Expand Down Expand Up @@ -280,19 +282,32 @@ def _create_position_array(self, pos):
pos, t, c, z
)

def _set_dtype(self):
def _infer_image_meta(self):
"""
gets the datatype from any image plane metadata

Returns
-------

Infer data type and pixel size from the first image plane metadata.
"""

tf = TiffFile(self._files[0])

self.dtype = tf.pages[0].dtype
tf.close()
with TiffFile(self._files[0]) as tf:
page = tf.pages[0]
self.dtype = page.dtype
for tag in page.tags.values():
if tag.name == "MicroManagerMetadata":
# assuming X and Y pixel sizes are the same
xy_size = tag.value.get("PixelSizeUm")
self._xy_pixel_size = xy_size if xy_size else None
return
else:
continue
logging.warning(
"Micro-Manager image plane metadata cannot be loaded."
)
self._xy_pixel_size = None

@property
def xy_pixel_size(self):
"""XY pixel size of the camera in micrometers."""
if self._xy_pixel_size is None:
raise AttributeError("XY pixel size cannot be determined.")
return self._xy_pixel_size

def _get_dimensions(self, position):
"""
Expand Down
1 change: 1 addition & 0 deletions iohub/ndtiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(self, data_path: str):
self.channel_names = list(self.dataset.get_channel_names())
self.stage_positions = self.mm_meta["Summary"]["StagePositions"]
self.z_step_size = self.mm_meta["Summary"]["z-step_um"]
self.xy_pixel_size = self.mm_meta["Summary"]["PixelSize_um"]

def _get_summary_metadata(self):
pm_metadata = self.dataset.summary_metadata
Expand Down
2 changes: 1 addition & 1 deletion iohub/ngff.py
Original file line number Diff line number Diff line change
Expand Up @@ -1418,7 +1418,7 @@ def wells(self):
for _, well in row.wells():
yield well.zgroup.path, well

def positions(self):
def positions(self) -> Generator[tuple[str, Position], None, None]:
"""Returns a generator that iterate over the path and value
of all the positions (along rows, columns, and wells) in the plate.

Expand Down
9 changes: 6 additions & 3 deletions tests/cli/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,21 @@ def test_cli_info_ome_zarr(setup_test_data, setup_hcs_ref, verbose):
assert re.search(r"Wells:\s+1", result.output)


@given(f=st.booleans(), g=st.booleans(), p=st.booleans())
@given(f=st.booleans(), g=st.booleans(), p=st.booleans(), s=st.booleans())
@settings(
suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=20000
)
def test_cli_convert_ome_tiff(
setup_test_data, setup_mm2gamma_ome_tiffs, f, g, p
setup_test_data, setup_mm2gamma_ome_tiffs, f, g, p, s
):
_, _, input_dir = setup_mm2gamma_ome_tiffs
runner = CliRunner()
f = "-f ometiff" if f else ""
g = "-g" if g else ""
p = "-p" if p else ""
with TemporaryDirectory() as tmp_dir:
output_dir = os.path.join(tmp_dir, "converted.zarr")
cmd = ["convert", "-i", input_dir, "-o", output_dir]
cmd = ["convert", "-i", input_dir, "-o", output_dir, "-s", s]
if f:
cmd += ["-f", "ometiff"]
if g:
Expand Down
60 changes: 46 additions & 14 deletions tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,43 @@
from tifffile import TiffFile, TiffSequence

from iohub.convert import TIFFConverter
from iohub.ngff import open_ome_zarr
from iohub.ngff import Position, open_ome_zarr
from iohub.reader import (
MicromanagerOmeTiffReader,
MicromanagerSequenceReader,
NDTiffReader,
)


@given(grid_layout=st.booleans(), label_positions=st.booleans())
@settings(
suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=20000
CONVERTER_TEST_SETTINGS = settings(
suppress_health_check=[HealthCheck.function_scoped_fixture],
deadline=20000,
)

CONVERTER_TEST_GIVEN = dict(
grid_layout=st.booleans(),
label_positions=st.booleans(),
scale_voxels=st.booleans(),
)


def _check_scale_transform(position: Position, scale_voxels: bool):
tf = position.metadata.multiscales[0].coordinate_transformations[0]
if scale_voxels:
assert tf.type == "scale"
assert tf.scale[:2] == [1.0, 1.0]
else:
assert tf.type == "identity"


@given(**CONVERTER_TEST_GIVEN)
@settings(CONVERTER_TEST_SETTINGS)
def test_converter_ometiff(
setup_test_data, setup_mm2gamma_ome_tiffs, grid_layout, label_positions
setup_test_data,
setup_mm2gamma_ome_tiffs,
grid_layout,
label_positions,
scale_voxels,
):
logging.getLogger("tifffile").setLevel(logging.ERROR)
_, _, data = setup_mm2gamma_ome_tiffs
Expand All @@ -34,6 +57,7 @@ def test_converter_ometiff(
output,
grid_layout=grid_layout,
label_positions=label_positions,
scale_voxels=scale_voxels,
)
assert isinstance(converter.reader, MicromanagerOmeTiffReader)
with TiffSequence(glob(os.path.join(data, "*.tif*"))) as ts:
Expand All @@ -54,16 +78,19 @@ def test_converter_ometiff(
with open_ome_zarr(output, mode="r") as result:
intensity = 0
for _, pos in result.positions():
_check_scale_transform(pos, scale_voxels)
intensity += pos["0"][:].sum()
assert intensity == raw_array.sum()


@given(grid_layout=st.booleans(), label_positions=st.booleans())
@settings(
suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=20000
)
@given(**CONVERTER_TEST_GIVEN)
@settings(CONVERTER_TEST_SETTINGS)
def test_converter_ndtiff(
setup_test_data, setup_pycromanager_test_data, grid_layout, label_positions
setup_test_data,
setup_pycromanager_test_data,
grid_layout,
label_positions,
scale_voxels,
):
logging.getLogger("tifffile").setLevel(logging.ERROR)
_, _, data = setup_pycromanager_test_data
Expand All @@ -74,6 +101,7 @@ def test_converter_ndtiff(
output,
grid_layout=grid_layout,
label_positions=label_positions,
scale_voxels=scale_voxels,
)
assert isinstance(converter.reader, NDTiffReader)
raw_array = np.asarray(Dataset(data).as_array())
Expand All @@ -88,19 +116,20 @@ def test_converter_ndtiff(
with open_ome_zarr(output, mode="r") as result:
intensity = 0
for _, pos in result.positions():
_check_scale_transform(pos, scale_voxels)
intensity += pos["0"][:].sum()
assert intensity == raw_array.sum()


@given(grid_layout=st.booleans(), label_positions=st.booleans())
@settings(
suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=20000
)
@given(**CONVERTER_TEST_GIVEN)
@settings(CONVERTER_TEST_SETTINGS)
def test_converter_singlepagetiff(
setup_test_data,
setup_mm2gamma_singlepage_tiffs,
grid_layout,
label_positions,
scale_voxels,
caplog,
):
logging.getLogger("tifffile").setLevel(logging.ERROR)
_, _, data = setup_mm2gamma_singlepage_tiffs
Expand All @@ -111,8 +140,11 @@ def test_converter_singlepagetiff(
output,
grid_layout=grid_layout,
label_positions=label_positions,
scale_voxels=scale_voxels,
)
assert isinstance(converter.reader, MicromanagerSequenceReader)
if scale_voxels:
assert "Pixel size detection is not supported" in caplog.text
with TiffSequence(glob(os.path.join(data, "**/*.tif*"))) as ts:
raw_array = ts.asarray()
assert np.prod([d for d in converter.dim if d > 0]) == np.prod(
Expand Down