-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #24 from folterj/dev3
Add support for ome pyramid (multi-resolution) files
- Loading branch information
Showing
9 changed files
with
729 additions
and
414 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,3 +77,4 @@ target/ | |
|
||
# written by setuptools_scm | ||
*/_version.py | ||
.idea/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.