Repository: scalableminds/webknossos-libs

Issue:
pims_reader for imagej tifffiles with z-slices
## Detailed Description
ImageJ writes tifffiles where z-slices are not represented as pages, but via a different memory-layout. The current [`TiffStack_tifffile` reader](https://github.com/soft-matter/pims/blob/main/pims/tiff_stack.py) handles those only as a single 2D image (as other tools do as well). However, they can be recognized and read by using:
```python
import tifffile

path = "…"
a = tifffile.TiffFile(path)
a.is_imagej  # True
a.imagej_metadata  # contains metadata

tifffile.memmap(path).shape  # correct shape, memmapped np array
```

Therefore we could add a custom reader for ImageJ tiff-stacks. Alternatively, we could recognize those and fall back to the bioformats reader, which supports those already.


Discussion:

Resolving patch:
diff --git a/webknossos/webknossos/dataset/_utils/pims_imagej_tiff_reader.py b/webknossos/webknossos/dataset/_utils/pims_imagej_tiff_reader.py
new file mode 100644
--- /dev/null
+++ b/webknossos/webknossos/dataset/_utils/pims_imagej_tiff_reader.py
@@ -0,0 +1,57 @@
+from os import PathLike
+from pathlib import Path
+from typing import Set
+
+import numpy as np
+
+try:
+    from pims import FramesSequenceND
+except ImportError as e:
+    raise RuntimeError(
+        "Cannot import pims, please install it e.g. using 'webknossos[all]'"
+    ) from e
+
+try:
+    import tifffile
+except ImportError as e:
+    raise RuntimeError(
+        "Cannot import tifffile, please install it e.g. using 'webknossos[tifffile]'"
+    ) from e
+
+
+class PimsImagejTiffReader(FramesSequenceND):
+    @classmethod
+    def class_exts(cls) -> Set[str]:
+        return {"tif", "tiff"}
+
+    # class_priority is used in pims to pick the reader with the highest priority.
+    # Default is 10, and bioformats priority is 2.
+    # See http://soft-matter.github.io/pims/v0.6.1/custom_readers.html#plugging-into-pims-s-open-function
+    class_priority = 20
+
+    def __init__(self, path: PathLike) -> None:
+        super().__init__()
+        path = Path(path)
+        tiff = tifffile.TiffFile(path)
+        assert tiff.is_imagej, f"{path} is not an ImageJ Tiff"
+        channels = tiff.imagej_metadata["channels"]
+        z = tiff.imagej_metadata["images"] / channels
+
+        self.memmap = tifffile.memmap(path)
+        # shape should be zcyx
+        assert len(self.memmap.shape) == 4
+        assert self.memmap.shape[0] == z
+        assert self.memmap.shape[1] == channels
+
+        self._init_axis("x", self.memmap.shape[3])
+        self._init_axis("y", self.memmap.shape[2])
+        self._init_axis("z", z)
+        self._init_axis("c", channels)
+        self._register_get_frame(self._get_frame, "cyx")
+
+    @property
+    def pixel_type(self) -> np.dtype:
+        return self.memmap.dtype
+
+    def _get_frame(self, **ind: int) -> np.ndarray:
+        return self.memmap[ind["z"]]
diff --git a/webknossos/webknossos/dataset/_utils/pims_images.py b/webknossos/webknossos/dataset/_utils/pims_images.py
--- a/webknossos/webknossos/dataset/_utils/pims_images.py
+++ b/webknossos/webknossos/dataset/_utils/pims_images.py
@@ -34,6 +34,11 @@
     import webknossos.dataset._utils.pims_dm_readers
 except ImportError:
     pass
+
+try:
+    import webknossos.dataset._utils.pims_imagej_tiff_reader
+except ImportError:
+    pass
 # pylint: enable=unused-import
 
 from webknossos.dataset.mag_view import MagView
diff --git a/webknossos/webknossos/dataset/dataset.py b/webknossos/webknossos/dataset/dataset.py
--- a/webknossos/webknossos/dataset/dataset.py
+++ b/webknossos/webknossos/dataset/dataset.py
@@ -1149,11 +1149,17 @@ def add_layer_from_images(
                     is_segmentation=category == "segmentation",
                     **pims_open_kwargs,
                 )
+            if dtype is None:
+                current_dtype = np.dtype(pims_images.dtype)
+                if current_dtype.byteorder == ">":
+                    current_dtype = current_dtype.newbyteorder("<")
+            else:
+                current_dtype = np.dtype(dtype)
             layer = self.add_layer(
                 layer_name=layer_name + layer_name_suffix,
                 category=category,
                 data_format=data_format,
-                dtype_per_channel=pims_images.dtype if dtype is None else dtype,
+                dtype_per_channel=current_dtype,
                 num_channels=pims_images.num_channels,
                 **add_layer_kwargs,  # type: ignore[arg-type]
             )
@@ -1198,7 +1204,7 @@ def add_layer_from_images(
                 pims_images.copy_to_view,
                 mag_view=mag_view,
                 is_segmentation=category == "segmentation",
-                dtype=dtype,
+                dtype=current_dtype,
             )
 
             args = []


Test patch: