Skip to content

Commit ef16ad2

Browse files
authored
fix: multichannel export as tiff (#565)
1 parent b93328c commit ef16ad2

7 files changed

Lines changed: 98 additions & 26 deletions

File tree

panseg/core/image.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -509,15 +509,8 @@ def _get_data_channel_layout(
509509
data = dp.normalize_01(data)
510510
return data
511511

512-
def _get_data(self, normalize_01: bool = True) -> np.ndarray:
513-
"""Get the data if the layout is not multichannel."""
514-
data = self._data
515-
if normalize_01:
516-
data = dp.normalize_01(data)
517-
return data
518-
519512
def get_data(
520-
self, channel: int | None = None, normalize_01: bool = True
513+
self, channel: int | None = None, normalize_01: bool = False
521514
) -> np.ndarray:
522515
"""Returns the data of the image.
523516
@@ -527,13 +520,15 @@ def get_data(
527520
normalize_01 (bool): Normalize the data between 0 and 1, if the
528521
image is a Label image, the data is not normalized.
529522
"""
523+
data = self._data
530524
if self.image_type == ImageType.LABEL:
531-
return self._data
525+
return data
532526

533527
if self.channel_axis is not None:
534-
return self._get_data_channel_layout(channel, normalize_01)
535-
536-
return self._get_data(normalize_01)
528+
data = self._get_data_channel_layout(channel, normalize_01)
529+
elif normalize_01:
530+
data = dp.normalize_01(data)
531+
return data
537532

538533
@property
539534
def scale(self) -> tuple[float, ...]:
@@ -728,6 +723,8 @@ def import_image(
728723
original_voxel_size=voxel_size,
729724
source_file_name=path.stem,
730725
)
726+
if image_properties.image_type == ImageType.IMAGE:
727+
data = dp.normalize_01(data)
731728

732729
return PanSegImage(data=data, properties=image_properties)
733730

panseg/io/tiff.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ def create_tiff(
198198

199199
elif layout == "CZYX":
200200
assert stack.ndim == 4, "Stack dimensions must be in CZYX order"
201-
c, z, y, x = stack.shape
201+
stack = np.transpose(stack, (1, 0, 2, 3))
202+
z, c, y, x = stack.shape
202203
stack = stack.reshape(1, z, c, y, x, 1)
203204

204205
else:

panseg/tasks/io_tasks.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from pathlib import Path
22
from typing import Optional
33

4+
import numpy as np
5+
46
from panseg.core.image import PanSegImage, import_image, save_image
57
from panseg.tasks import task_tracker
68
from panseg.tasks.workflow_handler import RunTimeInputSchema, Task_message
@@ -110,9 +112,10 @@ def export_image_task(
110112
def merge_channels_task(**kwargs) -> PanSegImage:
111113
"""Merge an arbitrary number of PanSegImages
112114
113-
Pass each image as a named argument, the name doesn't matter.
115+
Pass each image as a named argument.
116+
The images get sorted according to the key
114117
"""
115-
images: list[PanSegImage] = list(kwargs.values())
118+
images: list[PanSegImage] = [kwargs[k] for k in sorted(kwargs.keys())]
116119
image = images[0].derive_new(images[0].get_data(), images[0].name + "_merged")
117120
for im in images[1:]:
118121
image = image.merge_with(im)

tests/core/test_image.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ def test_panseg_image_to_napari_layer_tuple():
249249

250250
assert isinstance(layer_tuple, tuple)
251251
layer_tuple = tuple(layer_tuple)
252-
np.testing.assert_allclose(layer_tuple[0], ps_image.get_data(normalize_01=True))
252+
np.testing.assert_allclose(layer_tuple[0], ps_image.get_data(normalize_01=False))
253253
assert "metadata" in layer_tuple[1]
254254
assert layer_tuple[2] == ps_image.image_type.value
255255

tests/functionals/dataprocessing/test_image_math.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,7 @@
99
process_images,
1010
subtract_images,
1111
)
12-
13-
14-
def normalize_01(image: np.ndarray) -> np.ndarray:
15-
min_val, max_val = image.min(), image.max()
16-
if max_val - min_val == 0:
17-
return np.zeros_like(image)
18-
return (image - min_val) / (max_val - min_val)
12+
from panseg.functionals.dataprocessing.dataprocessing import normalize_01
1913

2014

2115
@pytest.mark.parametrize(

tests/headless/test_headless.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import numpy as np
44
import yaml
5+
from numpy.testing import assert_allclose
56

67
from panseg.core.image import PanSegImage
78
from panseg.headless.headless import run_headless_workflow
@@ -101,6 +102,8 @@ def test_create_workflow_channels(tmp_path):
101102
**{f"image_{i}": ps for i, ps in enumerate(ps_1s + [ps_2])}
102103
)
103104
assert isinstance(ps_3, PanSegImage)
105+
np.testing.assert_allclose(ps_1s[0].get_data(), ps_3.get_data()[0])
106+
np.testing.assert_allclose(ps_2.get_data(), ps_3.get_data()[4])
104107
export_image_task(
105108
image=ps_3,
106109
export_directory=path_tiff.parent,

tests/tasks/test_io_tasks.py

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
PanSegImage,
88
SemanticType,
99
)
10+
from panseg.functionals.dataprocessing.dataprocessing import normalize_01
1011
from panseg.io.voxelsize import VoxelSize
1112
from panseg.tasks.io_tasks import (
1213
export_image_task,
@@ -17,7 +18,79 @@
1718

1819

1920
@pytest.mark.parametrize(
20-
"shape, layout, export_format",
21+
"shape,layout,export_format",
22+
[
23+
((2, 64, 64), ImageLayout.CYX, "tiff"),
24+
((2, 64, 64), ImageLayout.CYX, "h5"),
25+
((2, 64, 64), ImageLayout.CYX, "zarr"),
26+
((2, 64, 32, 32), ImageLayout.CZYX, "h5"),
27+
((2, 64, 32, 32), ImageLayout.CZYX, "tiff"),
28+
((2, 64, 32, 32), ImageLayout.CZYX, "zarr"),
29+
# ((64, 64), ImageLayout.YX, "tiff"),
30+
# ((64, 64), ImageLayout.YX, "h5"),
31+
# ((64, 64), ImageLayout.YX, "zarr"),
32+
],
33+
)
34+
def test_image_io_round_trip_multichannel(tmp_path, shape, layout, export_format):
35+
mock_data = normalize_01(np.random.rand(*shape).astype("float32"))
36+
37+
property = ImageProperties(
38+
name="test",
39+
voxel_size=VoxelSize(voxels_size=(1.0, 1.0, 1.0), unit="um"),
40+
semantic_type=SemanticType.RAW,
41+
image_layout=layout,
42+
original_voxel_size=VoxelSize(voxels_size=(1.0, 1.0, 1.0), unit="um"),
43+
source_file_name="test",
44+
)
45+
image = PanSegImage(data=mock_data, properties=property)
46+
47+
export_image_task(
48+
image=image,
49+
export_directory=tmp_path,
50+
name_pattern="test",
51+
key="raw",
52+
export_format=export_format,
53+
data_type="float32",
54+
)
55+
56+
if export_format == "tiff":
57+
file_path = tmp_path / "test.tiff"
58+
key = None
59+
# tiff alwayes saved as ZCYX
60+
if layout == ImageLayout.CZYX:
61+
layout = ImageLayout.ZCYX
62+
63+
elif export_format == "h5":
64+
file_path = tmp_path / "test.h5"
65+
key = "raw"
66+
else:
67+
file_path = tmp_path / "test.zarr"
68+
key = "raw"
69+
70+
imported_image = import_image_task(
71+
input_path=file_path,
72+
key=key,
73+
image_name="test_import",
74+
semantic_type="raw",
75+
stack_layout=layout.name,
76+
m_slicing=None,
77+
)
78+
assert isinstance(imported_image, list)
79+
80+
for i in [0, 1]:
81+
original_data = image.get_data()[i]
82+
imported_data = imported_image[i].get_data()
83+
84+
assert np.allclose(original_data, imported_data)
85+
assert original_data.max() <= 1.0 # check if the normalization is applied
86+
assert imported_data.max() <= 1.0
87+
88+
assert image.voxel_size == imported_image[i].voxel_size
89+
assert image.semantic_type == imported_image[i].semantic_type
90+
91+
92+
@pytest.mark.parametrize(
93+
"shape,layout,export_format",
2194
[
2295
((32, 64, 64), ImageLayout.ZYX, "tiff"),
2396
((32, 64, 64), ImageLayout.ZYX, "h5"),
@@ -69,7 +142,8 @@ def test_image_io_round_trip(tmp_path, shape, layout, export_format):
69142
)
70143
assert isinstance(imported_image, PanSegImage)
71144

72-
original_data = image.get_data()
145+
# would be normalized during import
146+
original_data = image.get_data(normalize_01=True)
73147
imported_data = imported_image.get_data()
74148

75149
assert np.allclose(original_data, imported_data)
@@ -133,7 +207,7 @@ def test_label_io_round_trip(tmp_path, shape, layout, export_format):
133207
imported_image = import_image_task(
134208
input_path=file_path,
135209
key=key,
136-
image_name="tesi_import",
210+
image_name="test_import",
137211
semantic_type="segmentation",
138212
stack_layout=layout.name,
139213
m_slicing=None,

0 commit comments

Comments
 (0)