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

Add support for ome pyramid (multi-resolution) files #24

Merged
merged 26 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d1290fd
Split tiff/imagej/ome reader at metadata level instead
folterj Feb 24, 2024
fecc64c
Add zarr to dependencies
GenevieveBuckley Feb 26, 2024
da56519
Restored imagej metadata test
folterj Feb 26, 2024
ab4f521
Implemented ome metadata processing
folterj Feb 28, 2024
ee9c216
Added metadata testing
folterj Mar 5, 2024
ccbc28f
Create split channel images, simplify testing
folterj Mar 5, 2024
967d3e4
ome-tiff: Improved data and metadata creation, extended testing
folterj Mar 6, 2024
17af7eb
Moved napari tests from auto-tests
folterj Mar 8, 2024
616a7ab
Testing test
folterj Mar 8, 2024
1b88913
Reverted to using channel_axis, improved testing
folterj Mar 15, 2024
79ca561
Expanded ome-tiff testing
folterj Mar 15, 2024
9ec84b4
Added test case
folterj Mar 19, 2024
21237ac
Added metadata to testing and generic file load function
folterj Mar 19, 2024
75a51a5
Rearrange code
GenevieveBuckley Mar 25, 2024
1d14fa9
Replace metadata test cases with simplified setup
GenevieveBuckley Mar 26, 2024
381249e
Ensure both zarr & dask could raise an ImportError if not installed here
GenevieveBuckley Mar 26, 2024
24d2f1e
Resolve merge conflicts
GenevieveBuckley Mar 26, 2024
7cbd9be
Minor updates
GenevieveBuckley Mar 26, 2024
2a65610
Remove duplicate test file
GenevieveBuckley Mar 26, 2024
9787318
isort imports (minor formatting fix)
GenevieveBuckley Mar 26, 2024
ebaa2d9
Formatting code with black
GenevieveBuckley Mar 26, 2024
c05ea50
napari not strictly required as a test dependency
GenevieveBuckley Mar 26, 2024
5e21cee
Remove dask dependency, rely only on zarr
GenevieveBuckley Apr 17, 2024
3239a20
Simplify metadata getting
GenevieveBuckley Jun 6, 2024
225d9a2
Remove variable, is re-defined below
GenevieveBuckley Jun 6, 2024
6e2949e
Add unicode micro sign to metadata units list
GenevieveBuckley Jun 6, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ target/

# written by setuptools_scm
*/_version.py
.idea/
1 change: 0 additions & 1 deletion napari_tiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@
# replace the asterisk with named imports
from .napari_tiff_reader import napari_get_reader


__all__ = ["napari_get_reader"]
124 changes: 124 additions & 0 deletions napari_tiff/_tests/test_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import os
import zipfile

import numpy as np
import pytest
import tifffile


def example_data_filepath(tmp_path, original_data):
example_data_filepath = str(tmp_path / "example_data_filepath.tif")
tifffile.imwrite(example_data_filepath, original_data, imagej=False)
return example_data_filepath


def example_data_zipped_filepath(tmp_path, original_data):
example_tiff_filepath = str(tmp_path / "myfile.tif")
tifffile.imwrite(example_tiff_filepath, original_data, imagej=False)
example_zipped_filepath = str(tmp_path / "myfile.zip")
with zipfile.ZipFile(example_zipped_filepath, "w") as myzip:
myzip.write(example_tiff_filepath)
os.remove(example_tiff_filepath) # not needed now the zip file is saved
return example_zipped_filepath


def example_data_tiff(tmp_path, original_data):
example_data_filepath = str(tmp_path / "example_data_tiff.tif")
tifffile.imwrite(example_data_filepath, original_data, imagej=False)
return tifffile.TiffFile(example_data_filepath)


def example_data_imagej(tmp_path, original_data):
example_data_filepath = str(tmp_path / "example_data_imagej.tif")
tifffile.imwrite(example_data_filepath, original_data, imagej=True)
return tifffile.TiffFile(example_data_filepath)


def example_data_ometiff(tmp_path, original_data):
example_data_filepath = str(tmp_path / "example_data_ometiff.ome.tif")
tifffile.imwrite(example_data_filepath, original_data, imagej=False)
return tifffile.TiffFile(example_data_filepath)


@pytest.fixture(scope="session")
def imagej_hyperstack_image(tmp_path_factory):
"""ImageJ hyperstack tiff image.

Write a 10 fps time series of volumes with xyz voxel size 2.6755x2.6755x3.9474
micron^3 to an ImageJ hyperstack formatted TIFF file:
"""
filename = tmp_path_factory.mktemp("data") / "imagej_hyperstack.tif"

volume = np.random.randn(6, 57, 256, 256).astype("float32")
image_labels = [f"{i}" for i in range(volume.shape[0] * volume.shape[1])]
metadata = {
"spacing": 3.947368,
"unit": "um",
"finterval": 1 / 10,
"fps": 10.0,
"axes": "TZYX",
"Labels": image_labels,
}
tifffile.imwrite(
filename,
volume,
imagej=True,
resolution=(1.0 / 2.6755, 1.0 / 2.6755),
metadata=metadata,
)
return (filename, metadata)


@pytest.fixture
def example_data_multiresolution(tmp_path):
"""Example multi-resolution tiff file.

Write a multi-dimensional, multi-resolution (pyramidal), multi-series OME-TIFF
file with metadata. Sub-resolution images are written to SubIFDs. Limit
parallel encoding to 2 threads.

This example code reproduced from tifffile.py, see:
https://github.com/cgohlke/tifffile/blob/2b5a5208008594976d4627bcf01355fc08837592/tifffile/tifffile.py#L649-L688
"""
example_data_filepath = str(tmp_path / "test-pyramid.ome.tif")
data = np.random.randint(0, 255, (8, 2, 512, 512, 3), "uint8")
subresolutions = 2 # so 3 resolution levels in total
pixelsize = 0.29 # micrometer
with tifffile.TiffWriter(example_data_filepath, bigtiff=True) as tif:
metadata = {
"axes": "TCYXS",
"SignificantBits": 8,
"TimeIncrement": 0.1,
"TimeIncrementUnit": "s",
"PhysicalSizeX": pixelsize,
"PhysicalSizeXUnit": "µm",
"PhysicalSizeY": pixelsize,
"PhysicalSizeYUnit": "µm",
"Channel": {"Name": ["Channel 1", "Channel 2"]},
"Plane": {"PositionX": [0.0] * 16, "PositionXUnit": ["µm"] * 16},
}
options = dict(
photometric="rgb",
tile=(128, 128),
compression="jpeg",
resolutionunit="CENTIMETER",
maxworkers=2,
)
tif.write(
data,
subifds=subresolutions,
resolution=(1e4 / pixelsize, 1e4 / pixelsize),
metadata=metadata,
**options,
)
# write pyramid levels to the two subifds
# in production use resampling to generate sub-resolution images
for level in range(subresolutions):
mag = 2 ** (level + 1)
tif.write(
data[..., ::mag, ::mag, :],
subfiletype=1,
resolution=(1e4 / mag / pixelsize, 1e4 / mag / pixelsize),
**options,
)
return tifffile.TiffFile(example_data_filepath)
99 changes: 59 additions & 40 deletions napari_tiff/_tests/test_tiff_metadata.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,77 @@
import numpy
import numpy as np
import pytest
from tifffile import imwrite, TiffFile

from napari_tiff.napari_tiff_reader import imagej_reader


@pytest.fixture(scope="session")
def imagej_hyperstack_image(tmp_path_factory):
"""ImageJ hyperstack tiff image.

Write a 10 fps time series of volumes with xyz voxel size 2.6755x2.6755x3.9474
micron^3 to an ImageJ hyperstack formatted TIFF file:
"""
filename = tmp_path_factory.mktemp("data") / "imagej_hyperstack.tif"

volume = numpy.random.randn(6, 57, 256, 256).astype('float32')
image_labels = [f'{i}' for i in range(volume.shape[0] * volume.shape[1])]
metadata = {
'spacing': 3.947368,
'unit': 'um',
'finterval': 1/10,
'fps': 10.0,
'axes': 'TZYX',
'Labels': image_labels,
}
imwrite(
filename,
volume,
imagej=True,
resolution=(1./2.6755, 1./2.6755),
metadata=metadata,
)
return (filename, metadata)
from tifffile import TiffFile, imwrite, xml2dict

from napari_tiff._tests.test_data import (
example_data_imagej,
example_data_ometiff,
imagej_hyperstack_image,
)
from napari_tiff.napari_tiff_metadata import get_extra_metadata
from napari_tiff.napari_tiff_reader import tifffile_reader


@pytest.mark.parametrize(
"data_fixture, original_data, metadata_type",
[
(
example_data_ometiff,
np.random.randint(0, 255, size=(20, 20)).astype(np.uint8),
"ome_metadata",
),
(
example_data_imagej,
np.random.randint(0, 255, size=(20, 20)).astype(np.uint8),
"imagej_metadata",
),
],
)
def test_metadata_dict(tmp_path, data_fixture, original_data, metadata_type):
"""Check the 'metadata' dict stored with the layer data contains expected values."""
test_data = data_fixture(tmp_path, original_data)
result_metadata = tifffile_reader(test_data)[0][1]
# check metadata against TiffFile source metadata
expected_metadata = getattr(test_data, metadata_type)
if isinstance(expected_metadata, str):
expected_metadata = xml2dict(expected_metadata)
assert result_metadata.get("metadata").get(metadata_type) == expected_metadata
# check metadata in layer is identical to the extra metadata dictionary result
extra_metadata_dict = get_extra_metadata(test_data)
assert result_metadata.get("metadata") == extra_metadata_dict


def test_imagej_hyperstack_metadata(imagej_hyperstack_image):
"""Test metadata from imagej hyperstack tiff is passed to napari layer."""
imagej_hyperstack_filename, expected_metadata = imagej_hyperstack_image

with TiffFile(imagej_hyperstack_filename) as tif:
layer_data_list = imagej_reader(tif)
layer_data_list = tifffile_reader(tif)

assert isinstance(layer_data_list, list) and len(layer_data_list) > 0
layer_data_tuple = layer_data_list[0]
assert isinstance(layer_data_tuple, tuple) and len(layer_data_tuple) == 3

napari_layer_metadata = layer_data_tuple[1]
assert napari_layer_metadata.get('scale') == (1.0, 3.947368, 2.675500000484335, 2.675500000484335)
assert napari_layer_metadata.get("scale") == (
1.0,
3.947368,
2.675500000484335,
2.675500000484335,
)
assert layer_data_tuple[0].shape == (6, 57, 256, 256) # image volume shape

napari_layer_imagej_metadata = napari_layer_metadata.get('metadata').get('imagej_metadata')
assert napari_layer_imagej_metadata.get('slices') == 57 # calculated automatically when file is written
assert napari_layer_imagej_metadata.get('frames') == 6 # calculated automatically when file is written
expected_metadata.pop('axes') # 'axes' is stored as a tiff series attribute, not in the imagej_metadata property
for (key, val) in expected_metadata.items():
napari_layer_imagej_metadata = napari_layer_metadata.get("metadata").get(
"imagej_metadata"
)
assert (
napari_layer_imagej_metadata.get("slices") == 57
) # calculated automatically when file is written
assert (
napari_layer_imagej_metadata.get("frames") == 6
) # calculated automatically when file is written
expected_metadata.pop(
"axes"
) # 'axes' is stored as a tiff series attribute, not in the imagej_metadata property
for key, val in expected_metadata.items():
assert key in napari_layer_imagej_metadata
assert napari_layer_imagej_metadata.get(key) == val
Loading
Loading