Skip to content

Commit 1986ec3

Browse files
authored
from_images: python-native czi support and timepoint and channel multiplexing (#822)
* add python-native czi support and timepoint and channel multiplexing across layers * added truncate_rgba_to_rgb arg and fixed imageIO reader channel handling * changelog link to PR * readd largest_segment_id * apply PR feedback part 1 * apply PR feedback part 2: add comments, better var names, minor refactoring * add more comments * add max_layers argument
1 parent 1d4831d commit 1986ec3

File tree

7 files changed

+585
-195
lines changed

7 files changed

+585
-195
lines changed

webknossos/Changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ For upgrade instructions, please check the respective *Breaking Changes* section
1313
[Commits](https://github.com/scalableminds/webknossos-libs/compare/v0.10.24...HEAD)
1414

1515
### Breaking Changes
16+
- `Dataset.from_images` now adds a layer per timepoint and per channel (if the data doesn't have 1 or 3 channels). [#822](https://github.com/scalableminds/webknossos-libs/pull/822)
1617

1718
### Added
19+
- Added python-native CZI support for `Dataset.from_images` or `dataset.add_layer_from_images`, without using bioformats. [#822](https://github.com/scalableminds/webknossos-libs/pull/822)
20+
- `dataset.add_layer_from_images` can add a layer per timepoint and per channel when passing `allow_multiple_layers=True`. [#822](https://github.com/scalableminds/webknossos-libs/pull/822)
1821

1922
### Changed
2023

webknossos/poetry.lock

Lines changed: 154 additions & 82 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webknossos/pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,15 @@ imagecodecs = { version = ">=2021.11.20", optional = true }
6060
JPype1 = { version = "^1.3.0", optional = true }
6161
pims = { version = "^0.6.0", optional = true }
6262
tifffile = { version = ">=2021.11.2", optional = true }
63+
pylibCZIrw = { version = "^3.2", optional = true }
6364

6465
[tool.poetry.extras]
6566
pims = ["pims"]
6667
tifffile = ["pims", "tifffile"]
6768
imagecodecs = ["pims", "imagecodecs"]
6869
bioformats = ["pims","JPype1"]
69-
all = ["pims","tifffile","imagecodecs","JPype1",]
70+
czi = ["pims","pylibCZIrw"]
71+
all = ["pims","tifffile","imagecodecs","JPype1","pylibCZIrw",]
7072

7173
[tool.poetry.dev-dependencies]
7274
# autoflake

webknossos/tests/dataset/test_add_layer_from_images.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from pathlib import Path
2+
from shutil import copy
23
from tempfile import NamedTemporaryFile, TemporaryDirectory
34
from time import gmtime, strftime
45
from typing import Any, Dict, List, Tuple, Union
5-
from zipfile import ZipFile
6+
from zipfile import BadZipFile, ZipFile
67

78
import httpx
89
import numpy as np
@@ -118,7 +119,7 @@ def test_repo_images(
118119
return ds
119120

120121

121-
def download_and_unpack(url: str, out_path: Path) -> None:
122+
def download_and_unpack(url: str, out_path: Path, filename: str) -> None:
122123
with NamedTemporaryFile() as download_file:
123124
with httpx.stream("GET", url) as response:
124125
total = int(response.headers["Content-Length"])
@@ -130,8 +131,12 @@ def download_and_unpack(url: str, out_path: Path) -> None:
130131
progress.update(
131132
download_task, completed=response.num_bytes_downloaded
132133
)
133-
with ZipFile(download_file, "r") as zip_file:
134-
zip_file.extractall(out_path)
134+
try:
135+
with ZipFile(download_file, "r") as zip_file:
136+
zip_file.extractall(out_path)
137+
except BadZipFile:
138+
out_path.mkdir(parents=True, exist_ok=True)
139+
copy(download_file.name, out_path / filename)
135140

136141

137142
BIOFORMATS_ARGS = [
@@ -142,6 +147,7 @@ def download_and_unpack(url: str, out_path: Path) -> None:
142147
"uint8",
143148
3,
144149
(320, 240, 108),
150+
1,
145151
),
146152
(
147153
"https://samples.scif.io/wtembryo.zip",
@@ -150,6 +156,7 @@ def download_and_unpack(url: str, out_path: Path) -> None:
150156
"uint8",
151157
3,
152158
(240, 320, 1),
159+
1,
153160
),
154161
(
155162
"https://samples.scif.io/HEART.zip",
@@ -158,6 +165,7 @@ def download_and_unpack(url: str, out_path: Path) -> None:
158165
"uint8",
159166
1,
160167
(512, 512, 30),
168+
1,
161169
),
162170
(
163171
"https://samples.scif.io/sdub.zip",
@@ -166,6 +174,16 @@ def download_and_unpack(url: str, out_path: Path) -> None:
166174
"uint8",
167175
1,
168176
(192, 128, 9),
177+
1,
178+
),
179+
(
180+
"https://samples.scif.io/sdub.zip",
181+
"sdub*.pic",
182+
{"allow_multiple_layers": True},
183+
"uint8",
184+
1,
185+
(192, 128, 9),
186+
12,
169187
),
170188
(
171189
"https://samples.scif.io/test-avi.zip",
@@ -174,12 +192,13 @@ def download_and_unpack(url: str, out_path: Path) -> None:
174192
"uint8",
175193
3,
176194
(206, 218, 36),
195+
1,
177196
),
178197
]
179198

180199

181200
@pytest.mark.parametrize(
182-
"url, filename, kwargs, dtype, num_channels, size", BIOFORMATS_ARGS
201+
"url, filename, kwargs, dtype, num_channels, size, num_layers", BIOFORMATS_ARGS
183202
)
184203
def test_bioformats(
185204
tmp_path: Path,
@@ -189,9 +208,10 @@ def test_bioformats(
189208
dtype: str,
190209
num_channels: int,
191210
size: Tuple[int, int, int],
211+
num_layers: int,
192212
) -> wk.Dataset:
193213
unzip_path = tmp_path / "unzip"
194-
download_and_unpack(url, unzip_path)
214+
download_and_unpack(url, unzip_path, filename)
195215
ds = wk.Dataset(tmp_path / "ds", (1, 1, 1))
196216
with wk.utils.get_executor_for_args(None) as executor:
197217
l = ds.add_layer_from_images(
@@ -205,10 +225,21 @@ def test_bioformats(
205225
assert l.dtype_per_channel == np.dtype(dtype)
206226
assert l.num_channels == num_channels
207227
assert l.bounding_box == wk.BoundingBox(topleft=(0, 0, 0), size=size)
228+
assert len(ds.layers) == num_layers
208229
return ds
209230

210231

211232
TEST_IMAGES_ARGS = [
233+
(
234+
# published with CC0 license, taken from
235+
# https://doi.org/10.6084/m9.figshare.c.3727411_D391.v1
236+
"https://figshare.com/ndownloader/files/8909407",
237+
"embedded_NCI_mono_matrigelcollagen_docetaxel_day10_sample10.czi",
238+
{},
239+
"uint16",
240+
1,
241+
(512, 512, 30),
242+
),
212243
(
213244
"https://samples.scif.io/test-gif.zip",
214245
"scifio-test.gif",
@@ -257,7 +288,7 @@ def test_test_images(
257288
size: Tuple[int, int, int],
258289
) -> wk.Dataset:
259290
unzip_path = tmp_path / "unzip"
260-
download_and_unpack(url, unzip_path)
291+
download_and_unpack(url, unzip_path, filename)
261292
ds = wk.Dataset(tmp_path / "ds", (1, 1, 1))
262293
with wk.utils.get_executor_for_args(None) as executor:
263294
l_bio = ds.add_layer_from_images(
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
from contextlib import contextmanager
2+
from os import PathLike
3+
from pathlib import Path
4+
from typing import Iterator, List, Set
5+
6+
import numpy as np
7+
8+
try:
9+
from pims import FramesSequenceND
10+
except ImportError as e:
11+
raise RuntimeError(
12+
"Cannot import pims, please install it e.g. using 'webknossos[all]'"
13+
) from e
14+
15+
try:
16+
from pylibCZIrw import czi as pyczi
17+
except ImportError as e:
18+
raise RuntimeError(
19+
"Cannot import pylibCZIrw, please install it e.g. using 'webknossos[czi]'"
20+
) from e
21+
22+
PIXEL_TYPE_TO_DTYPE = {
23+
"Gray8": "<u1",
24+
"Gray16": "<u2",
25+
"Gray32Float": "<f4",
26+
"Bgr24": "<u1",
27+
"Bgr48": "<u2",
28+
"Bgr96Float": "<f4",
29+
"Bgra32": "<4u1",
30+
"Gray64ComplexFloat": "<F8",
31+
"Bgr192ComplexFloat": "<F8",
32+
"Gray32": "<i4",
33+
"Gray64": "<i8",
34+
}
35+
36+
37+
class PimsCziReader(FramesSequenceND):
38+
@classmethod
39+
def class_exts(cls) -> Set[str]:
40+
return {".czi"}
41+
42+
# class_priority is used in pims to pick the reader with the highest priority.
43+
# Default is 10, and bioformats priority (which is the only other reader supporting czi) is 2.
44+
# See http://soft-matter.github.io/pims/v0.6.1/custom_readers.html#plugging-into-pims-s-open-function
45+
class_priority = 20
46+
47+
def __init__(self, path: PathLike, czi_channel: int = 0) -> None:
48+
self.path = Path(path)
49+
self.czi_channel = czi_channel
50+
super().__init__()
51+
with self.czi_file() as czi_file:
52+
for axis, (
53+
start,
54+
length,
55+
) in czi_file.total_bounding_box.items():
56+
axis = axis.lower()
57+
if axis == "c":
58+
continue
59+
assert start == 0
60+
if axis not in "xy" and length == 1:
61+
# not propagating axes of length one
62+
continue
63+
self._init_axis(axis, length)
64+
czi_pixel_type = czi_file.get_channel_pixel_type(self.czi_channel)
65+
if czi_pixel_type.startswith("Bgra"):
66+
self._init_axis("c", 4)
67+
elif czi_pixel_type.startswith("Bgr"):
68+
self._init_axis("c", 3)
69+
elif czi_pixel_type.startswith("Gray"):
70+
self._init_axis("c", 1)
71+
elif czi_pixel_type == "Invalid":
72+
raise ValueError(
73+
f"czi_channel {self.czi_channel} does not exist in {self.path}"
74+
)
75+
else:
76+
raise ValueError(
77+
f"Got unsupported czi pixel-type {czi_pixel_type} in {self.path}"
78+
)
79+
80+
self._register_get_frame(self.get_frame_2D, "yxc")
81+
82+
@contextmanager
83+
def czi_file(self) -> Iterator[pyczi.CziReader]:
84+
with pyczi.open_czi(str(self.path)) as czi_file:
85+
yield czi_file
86+
87+
def available_czi_channels(self) -> List[int]:
88+
with self.czi_file() as czi_file:
89+
return sorted(czi_file.pixel_types.keys())
90+
91+
@property # potential @cached_property for py3.8+
92+
def pixel_type(self) -> np.dtype:
93+
with self.czi_file() as czi_file:
94+
return np.dtype(
95+
PIXEL_TYPE_TO_DTYPE[czi_file.get_channel_pixel_type(self.czi_channel)]
96+
)
97+
98+
def get_frame_2D(self, **ind: int) -> np.ndarray:
99+
plane = {k.upper(): v for k, v in ind.items()}
100+
101+
# safe-guard against x/y in ind argument,
102+
# we always read the whole slice here:
103+
plane.pop("X", None)
104+
plane.pop("Y", None)
105+
106+
plane["C"] = self.czi_channel
107+
with self.czi_file() as czi_file:
108+
a = czi_file.read(plane=plane)
109+
num_channels = a.shape[-1]
110+
if num_channels == 3:
111+
# convert from bgr to rgb
112+
a = np.flip(a, axis=-1)
113+
elif num_channels == 4:
114+
# convert from bgra to rgba
115+
a_red = a[:, :, 2].copy(order="K")
116+
a[:, :, 2] = a[:, :, 0]
117+
a[:, :, 0] = a_red
118+
return a

0 commit comments

Comments
 (0)