Skip to content

Commit 818baab

Browse files
markbadernormanrz
andauthored
Tiffreader for nd data (#1043)
* WIP change axis recognition of tifffiles. * Add basic custom tiff reader. * Revert changes on _assume_color_channel method. * Adapt test_repo_images. * Test out _register_get_frame changes. * Work on fixing image conversion for images without z axis. * Update expected_bbox function to deliver a mag1 bbox. * Working on fixing tiff_reader. * reverting some changes at the pims image reader class. * Working on data extraction from tiff reader to desired Pims Sequence. * Use memmap for tiffreader to avoid OOM issue. * Implement np memmap for pims tiff reader because nd ome tiffs are not memmappable with tifffile.memmap. * Adapt calculation of num_channels and set default axes for get_frame. * Update test parameters for test multiple multitiffs. * Update changelog and update error message for unsupported conversion to WKW. * Update webknossos/webknossos/dataset/_utils/pims_images.py Co-authored-by: Norman Rzepka <[email protected]> * Started to implement requested changes. * Fix issue with zarr array creation and add zarr to supported dataformats for nd data. * Add an entry for merge-fallback in cli docs. * Add 2 additional testdatasets and adapt logging behaviour. * Remove user warning from conversion test. * Run linter. * Add pytest.warn() to cli_test.py. * Revert some changes that are not relevant for this PR. * Add comment for chunk shape calculation. * Update changelog. --------- Co-authored-by: Norman Rzepka <[email protected]>
1 parent 31fe934 commit 818baab

File tree

11 files changed

+287
-83
lines changed

11 files changed

+287
-83
lines changed

webknossos/Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ For upgrade instructions, please check the respective _Breaking Changes_ section
1313
[Commits](https://github.com/scalableminds/webknossos-libs/compare/v0.14.22...HEAD)
1414

1515
### Breaking Changes
16+
- Preferring a custom tiff reader over the default PIMS reader to convert tiff files. This change enables the recognition of axis information and the support of tifffiles with more than 3 dimensions. However, it also leads to changed behavior when converting tiff files. Tiffs with axes other than c, x, y, and z, with a shape bigger than 1, are no longer supported for conversion to WKW. Please convert these files to Zarr or Zarr3 Datasets instead. [#1043](https://github.com/scalableminds/webknossos-libs/pull/1043)
1617

1718
### Added
1819
- Added a pixel level heuristic for distinguishing color and segmentation layers when importing image data with the `from_images` or `add_layer_from_images` method. [#1007](https://github.com/scalableminds/webknossos-libs/pull/1007)

webknossos/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,5 @@ Excerpts for testing purposes have been sampled from:
6161
* Dow Jacobo Hossain Siletti Hudspeth (2018). **Connectomics of the zebrafish's lateral-line neuromast reveals wiring and miswiring in a simple microcircuit.** eLife. [DOI:10.7554/eLife.33988](https://elifesciences.org/articles/33988)
6262
* Zheng Lauritzen Perlman Robinson Nichols Milkie Torrens Price Fisher Sharifi Calle-Schuler Kmecova Ali Karsh Trautman Bogovic Hanslovsky Jefferis Kazhdan Khairy Saalfeld Fetter Bock (2018). **A Complete Electron Microscopy Volume of the Brain of Adult Drosophila melanogaster.** Cell. [DOI:10.1016/j.cell.2018.06.019](https://www.cell.com/cell/fulltext/S0092-8674(18)30787-6). License: [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/)
6363
* Bosch Ackels Pacureanu et al (2022). **Functional and multiscale 3D structural investigation of brain tissue through correlative in vivo physiology, synchrotron microtomography and volume electron microscopy.** Nature Communications. [DOI:10.1038/s41467-022-30199-6](https://www.nature.com/articles/s41467-022-30199-6)
64-
* Hanke, M., Baumgartner, F. J., Ibe, P., Kaule, F. R., Pollmann, S., Speck, O., Zinke, W. & Stadler, J. (2014). **A high-resolution 7-Tesla fMRI dataset from complex natural stimulation with an audio movie. Scientific Data, 1:140003.** [DOI:10.1038/sdata.2014.3](http://www.nature.com/articles/sdata20143)
64+
* Hanke, M., Baumgartner, F. J., Ibe, P., Kaule, F. R., Pollmann, S., Speck, O., Zinke, W. & Stadler, J. (2014). **A high-resolution 7-Tesla fMRI dataset from complex natural stimulation with an audio movie. Scientific Data, 1:140003.** [DOI:10.1038/sdata.2014.3](http://www.nature.com/articles/sdata20143)
65+
* Sample OME-TIFF files (c) by the OME Consortium [https://downloads.openmicroscopy.org/images/OME-TIFF/2016-06/bioformats-artificial/](https://downloads.openmicroscopy.org/images/OME-TIFF/2016-06/bioformats-artificial/)

webknossos/examples/convert_4d_tiff.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ def main() -> None:
1010
"testoutput/4D_series",
1111
voxel_size=(10, 10, 10),
1212
data_format="zarr3",
13-
use_bioformats=True,
1413
)
1514

1615
# Access the first color layer and the Mag 1 view of this layer
Binary file not shown.
74.3 KB
Binary file not shown.

webknossos/tests/dataset/test_add_layer_from_images.py

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ def test_compare_nd_tifffile(tmp_path: Path) -> None:
5050
layer_name="color",
5151
category="color",
5252
topleft=(2, 55, 100, 100),
53-
use_bioformats=True,
5453
data_format="zarr3",
5554
chunk_shape=(8, 8, 8),
5655
chunks_per_shard=(8, 8, 8),
@@ -69,13 +68,14 @@ def test_compare_nd_tifffile(tmp_path: Path) -> None:
6968

7069

7170
REPO_IMAGES_ARGS: List[
72-
Tuple[Union[str, List[Path]], Dict[str, Any], str, int, Tuple[int, int, int]]
71+
Tuple[Union[str, List[Path]], Dict[str, Any], str, int, int, Tuple[int, ...]]
7372
] = [
7473
(
7574
"testdata/tiff/test.*.tiff",
7675
{"category": "segmentation"},
7776
"uint8",
7877
1,
78+
1,
7979
(265, 265, 257),
8080
),
8181
(
@@ -87,34 +87,39 @@ def test_compare_nd_tifffile(tmp_path: Path) -> None:
8787
{},
8888
"uint8",
8989
1,
90+
1,
9091
(265, 265, 3),
9192
),
9293
(
9394
"testdata/rgb_tiff/test_rgb.tif",
9495
{"mag": 2},
9596
"uint8",
96-
3,
97-
(64, 64, 2),
97+
1,
98+
1,
99+
(64, 64, 6),
98100
),
99101
(
100102
"testdata/rgb_tiff",
101-
{"mag": 2, "channel": 1, "dtype": "uint32"},
103+
{"mag": 2, "channel": 0, "dtype": "uint32"},
102104
"uint32",
103105
1,
104-
(64, 64, 2),
106+
1,
107+
(64, 64, 6),
105108
),
106109
(
107110
"testdata/temca2/*/*/*.jpg",
108111
{"flip_x": True, "batch_size": 2048},
109112
"uint8",
110113
1,
114+
1,
111115
(1024, 1024, 12),
112116
),
113117
(
114118
"testdata/temca2",
115119
{"flip_z": True, "batch_size": 2048},
116120
"uint8",
117121
1,
122+
1,
118123
# The topmost folder contains an extra image,
119124
# which is included here as well, but not in
120125
# the glob pattern above. Therefore z is +1.
@@ -125,25 +130,73 @@ def test_compare_nd_tifffile(tmp_path: Path) -> None:
125130
{"flip_y": True},
126131
"uint8",
127132
1,
133+
1,
128134
(2970, 2521, 4),
129135
),
130-
("testdata/various_tiff_formats/test_CS.tif", {}, "uint8", 3, (128, 128, 320)),
131-
("testdata/various_tiff_formats/test_C.tif", {}, "uint8", 1, (128, 128, 320)),
136+
(
137+
"testdata/various_tiff_formats/test_CS.tif",
138+
{"data_format": "zarr3", "allow_multiple_layers": True},
139+
"uint8",
140+
1,
141+
5,
142+
(3, 64, 128, 128),
143+
),
144+
(
145+
"testdata/various_tiff_formats/test_C.tif",
146+
{"allow_multiple_layers": True},
147+
"uint8",
148+
1,
149+
5,
150+
(128, 128, 64),
151+
),
132152
# same as test_C.tif above, but as a single file in a folder:
133-
("testdata/single_multipage_tiff_folder", {}, "uint8", 1, (128, 128, 320)),
134-
("testdata/various_tiff_formats/test_I.tif", {}, "uint32", 1, (64, 128, 64)),
135-
("testdata/various_tiff_formats/test_S.tif", {}, "uint16", 3, (128, 128, 64)),
153+
(
154+
"testdata/single_multipage_tiff_folder",
155+
{"allow_multiple_layers": True},
156+
"uint8",
157+
1,
158+
5,
159+
(128, 128, 64),
160+
),
161+
("testdata/various_tiff_formats/test_I.tif", {}, "uint32", 1, 1, (64, 128, 64)),
162+
(
163+
"testdata/various_tiff_formats/test_S.tif",
164+
{"data_format": "zarr3"},
165+
"uint16",
166+
1,
167+
1,
168+
(3, 64, 128, 128),
169+
),
170+
(
171+
"testdata/4D/single_channel/single-channel.ome.tiff",
172+
{},
173+
"int8",
174+
1,
175+
1,
176+
(439, 167, 1),
177+
),
178+
(
179+
"testdata/4D/multi_channel_z_series/multi-channel-z-series.ome.tif",
180+
{"allow_multiple_layers": True},
181+
"int8",
182+
1,
183+
3,
184+
(439, 167, 5),
185+
),
136186
]
137187

138188

139-
@pytest.mark.parametrize("path, kwargs, dtype, num_channels, size", REPO_IMAGES_ARGS)
189+
@pytest.mark.parametrize(
190+
"path, kwargs, dtype, num_channels, num_layers, size", REPO_IMAGES_ARGS
191+
)
140192
def test_repo_images(
141193
tmp_path: Path,
142194
path: str,
143195
kwargs: Dict,
144196
dtype: str,
145197
num_channels: int,
146-
size: Tuple[int, int, int],
198+
num_layers: int,
199+
size: Tuple[int, ...],
147200
) -> wk.Dataset:
148201
with wk.utils.get_executor_for_args(None) as executor:
149202
ds = wk.Dataset(tmp_path, (1, 1, 1))
@@ -157,7 +210,8 @@ def test_repo_images(
157210
)
158211
assert layer.dtype_per_channel == np.dtype(dtype)
159212
assert layer.num_channels == num_channels
160-
assert layer.bounding_box == wk.BoundingBox(topleft=(0, 0, 0), size=size)
213+
assert len(ds.layers) == num_layers
214+
assert layer.bounding_box.size.to_tuple() == size
161215
if isinstance(layer, wk.SegmentationLayer):
162216
assert layer.largest_segment_id is not None
163217
assert layer.largest_segment_id > 0
@@ -396,7 +450,7 @@ def test_test_images(
396450
else:
397451
assert l_bio.dtype_per_channel == np.dtype(dtype)
398452
assert l_bio.num_channels == num_channels
399-
assert l_bio.bounding_box == wk.BoundingBox(topleft=(0, 0, 0), size=size)
453+
assert l_bio.bounding_box.size.to_tuple() == size
400454
l_normal = ds.add_layer_from_images(
401455
path,
402456
layer_name="normal_" + layer_name,
@@ -407,7 +461,7 @@ def test_test_images(
407461
)
408462
assert l_normal.dtype_per_channel == np.dtype(dtype)
409463
assert l_normal.num_channels == num_channels
410-
assert l_normal.bounding_box == wk.BoundingBox(topleft=(0, 0, 0), size=size)
464+
assert l_normal.bounding_box.size.to_tuple() == size
411465
if l_bio is not None:
412466
assert np.array_equal(
413467
l_bio.get_finest_mag().read(), l_normal.get_finest_mag().read()

webknossos/tests/dataset/test_from_images.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,24 @@ def test_multiple_multitiffs(tmp_path: Path) -> None:
4444
TESTDATA_DIR / "various_tiff_formats",
4545
tmp_path,
4646
(1, 1, 1),
47+
data_format="zarr3",
4748
layer_name="tiffs",
4849
)
49-
assert len(ds.layers) == 4
50+
assert len(ds.layers) == 12
5051

5152
expected_dtype_channels_size_per_layer = {
52-
"tiffs_test_CS.tif": ("uint8", 3, (128, 128, 320)),
53-
"tiffs_test_C.tif": ("uint8", 1, (128, 128, 320)),
53+
"tiffs_test_CS.tif__channel0": ("uint8", 1, (3, 64, 128, 128)),
54+
"tiffs_test_CS.tif__channel1": ("uint8", 1, (3, 64, 128, 128)),
55+
"tiffs_test_CS.tif__channel2": ("uint8", 1, (3, 64, 128, 128)),
56+
"tiffs_test_CS.tif__channel3": ("uint8", 1, (3, 64, 128, 128)),
57+
"tiffs_test_CS.tif__channel4": ("uint8", 1, (3, 64, 128, 128)),
58+
"tiffs_test_C.tif__channel0": ("uint8", 1, (128, 128, 64)),
59+
"tiffs_test_C.tif__channel1": ("uint8", 1, (128, 128, 64)),
60+
"tiffs_test_C.tif__channel2": ("uint8", 1, (128, 128, 64)),
61+
"tiffs_test_C.tif__channel3": ("uint8", 1, (128, 128, 64)),
62+
"tiffs_test_C.tif__channel4": ("uint8", 1, (128, 128, 64)),
5463
"tiffs_test_I.tif": ("uint32", 1, (64, 128, 64)),
55-
"tiffs_test_S.tif": ("uint16", 3, (128, 128, 64)),
64+
"tiffs_test_S.tif": ("uint16", 1, (3, 64, 128, 128)),
5665
}
5766

5867
for layer_name, layer in ds.layers.items():

webknossos/webknossos/dataset/_array.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,11 @@ def create(cls, path: Path, array_info: ArrayInfo) -> "ZarritaArray":
593593
ArrayV2.create(
594594
store=path,
595595
shape=(array_info.shape),
596-
chunks=(array_info.num_channels,) + array_info.chunk_shape.to_tuple(),
596+
chunks=(array_info.num_channels,)
597+
+ tuple(
598+
getattr(array_info.chunk_shape, axis, 1)
599+
for axis in array_info.dimension_names[1:]
600+
), # The chunk shape consists of the number of channels, the x, y, and z dimensions of a chunk, and 1 for all other dimensions.
597601
dtype=array_info.voxel_type,
598602
compressor=(
599603
{"id": "blosc", "cname": "zstd", "clevel": 5}

webknossos/webknossos/dataset/_utils/pims_images.py

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
from webknossos.geometry.bounding_box import BoundingBox
2828
from webknossos.geometry.nd_bounding_box import NDBoundingBox
2929

30-
# pylint: disable=unused-import
3130
try:
3231
from .pims_czi_reader import PimsCziReader
3332
except ImportError:
@@ -48,6 +47,11 @@
4847
except ImportError:
4948
pass
5049

50+
try:
51+
from .pims_tiff_reader import PimsTiffReader # noqa: F401 unused-import
52+
except ImportError:
53+
pass
54+
5155

5256
from ...geometry.vec_int import VecInt
5357
from ..mag_view import MagView
@@ -120,7 +124,7 @@ def __init__(
120124

121125
## attributes that will be set in __init__()
122126
# _bundle_axes
123-
self._iter_axes = None
127+
self._iter_axes: List[str] = []
124128
self._iter_loop_size = None
125129
self._possible_layers = {}
126130

@@ -216,18 +220,15 @@ def __init__(
216220
if len(images.shape) == 2:
217221
# Assume yx
218222
self._bundle_axes = ["y", "x"]
219-
self._iter_axes = []
220223
elif len(images.shape) == 3:
221224
# Assume yxc, cyx or zyx
222225
if _assume_color_channel(images.shape[2], images.dtype):
223226
self._bundle_axes = ["y", "x", "c"]
224-
self._iter_axes = []
225227
elif images.shape[0] == 1 or (
226228
_allow_channels_first
227229
and _assume_color_channel(images.shape[0], images.dtype)
228230
):
229231
self._bundle_axes = ["c", "y", "x"]
230-
self._iter_axes = []
231232
else:
232233
self._bundle_axes = ["y", "x"]
233234
self._iter_axes = ["z"]
@@ -277,18 +278,16 @@ def __init__(
277278
#########################
278279

279280
with self._open_images() as images:
280-
try:
281-
c_index = self._bundle_axes.index("c")
282-
if isinstance(images, list):
283-
images_shape = (len(images),) + cast(
284-
pims.FramesSequence, images[0]
285-
).shape
281+
if "c" in self._bundle_axes:
282+
if isinstance(images, pims.FramesSequenceND):
283+
self.num_channels = images.sizes.get("c", 1)
284+
elif isinstance(images, list):
285+
self.num_channels = cast(pims.FramesSequence, images[0]).shape[
286+
self._bundle_axes.index("c")
287+
]
286288
else:
287-
images_shape = images.shape # pylint: disable=no-member
288-
289-
self.num_channels = images_shape[c_index + 1]
290-
291-
except ValueError:
289+
self.num_channels = images.shape[self._bundle_axes.index("c") + 1]
290+
else:
292291
self.num_channels = 1
293292

294293
self._first_n_channels = None
@@ -302,7 +301,7 @@ def __init__(
302301
self._possible_layers["channel"] = [0, 1]
303302
self.num_channels = 1
304303
self._channel = 0
305-
elif self.num_channels > 3:
304+
elif self.num_channels >= 3:
306305
self._possible_layers["channel"] = list(range(0, self.num_channels))
307306
self.num_channels = 3
308307
self._first_n_channels = 3
@@ -483,13 +482,15 @@ def _open_images(
483482
images.bundle_axes = self._bundle_axes
484483
images.iter_axes = self._iter_axes
485484
else:
486-
if self._timepoint is not None:
487-
images = images[self._timepoint]
488-
if self._iter_axes and "t" in self._iter_axes:
489-
self._iter_axes.remove("t")
490-
if self._iter_axes == []:
491-
# add outer list to wrap 2D images as 3D-like structure
492-
images = [images]
485+
if hasattr(self, "_bundle_axes"):
486+
# first part of __init__() has happened
487+
if self._timepoint is not None:
488+
images = images[self._timepoint]
489+
if "t" in self._iter_axes:
490+
self._iter_axes.remove("t")
491+
if not self._iter_axes:
492+
# add outer list to wrap 2D images as 3D-like structure
493+
images = [images]
493494
yield images
494495

495496
def copy_to_view(
@@ -519,7 +520,7 @@ def copy_to_view(
519520
max_value = 0
520521

521522
with self._open_images() as images:
522-
if self._iter_axes is not None and self._iter_loop_size is not None:
523+
if self._iter_axes and self._iter_loop_size is not None:
523524
# select the range of images that represents one xyz combination in the mag_view
524525
lower_bounds = sum(
525526
self._iter_loop_size[axis_name]
@@ -529,7 +530,7 @@ def copy_to_view(
529530
upper_bounds = lower_bounds + mag_view.bounding_box.get_shape("z")
530531
images = images[lower_bounds:upper_bounds]
531532
if self._flip_z:
532-
images = images[::-1] # pylint: disable=unsubscriptable-object
533+
images = images[::-1]
533534

534535
with mag_view.get_buffered_slice_writer(
535536
# Previously only z_start and its end were important, now the slice writer needs to know
@@ -600,7 +601,7 @@ def expected_bbox(self) -> NDBoundingBox:
600601
).shape
601602

602603
else:
603-
images_shape = images.shape # pylint: disable=no-member
604+
images_shape = images.shape
604605
if len(images_shape) == 3:
605606
axes = ("z", "y", "x")
606607
else:
@@ -633,10 +634,7 @@ def expected_bbox(self) -> NDBoundingBox:
633634
axes_names = (self._iter_axes or []) + [
634635
axis for axis in self._bundle_axes if axis != "c"
635636
]
636-
axes_sizes = [
637-
images.sizes[axis] # pylint: disable=no-member
638-
for axis in axes_names
639-
]
637+
axes_sizes = [images.sizes[axis] for axis in axes_names]
640638
axes_index = list(range(1, len(axes_names) + 1))
641639
topleft = VecInt.zeros(tuple(axes_names))
642640

0 commit comments

Comments
 (0)