Skip to content

Commit

Permalink
Merge pull request #24 from folterj/dev3
Browse files Browse the repository at this point in the history
Add support for ome pyramid (multi-resolution) files
  • Loading branch information
GenevieveBuckley authored Jun 7, 2024
2 parents 815f7bf + 6e2949e commit 4ddc195
Show file tree
Hide file tree
Showing 9 changed files with 729 additions and 414 deletions.
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

0 comments on commit 4ddc195

Please sign in to comment.