Skip to content

Commit a3f2c39

Browse files
committed
2 parents 272d671 + b3d53c7 commit a3f2c39

4 files changed

Lines changed: 86 additions & 138 deletions

File tree

src/napari_tmidas/_file_selector.py

Lines changed: 43 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -175,36 +175,26 @@ def Signal(*args):
175175
)
176176

177177

178-
def is_label_image(filename: str) -> bool:
178+
def is_label_image(image: np.ndarray) -> bool:
179179
"""
180-
Determine if a file should be treated as a label image based on its filename.
180+
Determine if an image should be treated as a label image based on its dtype.
181181
182-
This function checks for common suffixes used by processing functions that
183-
produce label or binary mask outputs.
182+
This function uses the same logic as Napari's guess_labels() function,
183+
checking if the dtype is one of the integer types commonly used for labels.
184184
185185
Parameters:
186186
-----------
187-
filename : str
188-
The filename (or full path) to check
187+
image : np.ndarray
188+
The image array to check
189189
190190
Returns:
191191
--------
192192
bool
193-
True if the file should be treated as labels, False otherwise
193+
True if the image dtype suggests it's a label image, False otherwise
194194
"""
195-
label_suffixes = [
196-
"labels",
197-
"semantic",
198-
"_binary",
199-
"_inverted",
200-
"_thresh",
201-
"_otsu_semantic",
202-
"_otsu_labels",
203-
"_instance",
204-
"_rm_small",
205-
]
206-
filename_lower = filename.lower()
207-
return any(suffix in filename_lower for suffix in label_suffixes)
195+
if hasattr(image, "dtype"):
196+
return image.dtype in (np.int32, np.uint32, np.int64, np.uint64)
197+
return False
208198

209199

210200
def load_zarr_with_napari_ome_zarr(
@@ -873,8 +863,8 @@ def _load_original_image(self, filepath: str):
873863

874864
# Single channel image
875865
base_filename = os.path.basename(filepath)
876-
# check if label image by checking file name for label-related suffixes
877-
is_label = is_label_image(base_filename)
866+
# check if label image by checking image dtype
867+
is_label = is_label_image(image)
878868

879869
if is_label:
880870
if hasattr(image, "astype"):
@@ -1241,8 +1231,8 @@ def _load_processed_image(self, filepath: str):
12411231

12421232
# Single channel processed image
12431233
filename = os.path.basename(filepath)
1244-
# Check if filename contains label indicators
1245-
is_label = is_label_image(filename)
1234+
# Check if image dtype indicates labels
1235+
is_label = is_label_image(image)
12461236

12471237
# Add the layer using the appropriate method
12481238
if is_label:
@@ -1671,7 +1661,11 @@ def process_file(self, filepath):
16711661
len(processed_result) == 3
16721662
and self.output_suffix == "_layer"
16731663
):
1674-
layer_names = ["_inner", "_middle", "_outer"]
1664+
layer_names = [
1665+
"_inner",
1666+
"_middle",
1667+
"_outer",
1668+
]
16751669
for idx, (img, layer_name) in enumerate(
16761670
zip(processed_result, layer_names)
16771671
):
@@ -1705,37 +1699,19 @@ def process_file(self, filepath):
17051699
# For very large files, use BigTIFF format
17061700
use_bigtiff = size_gb > 2.0
17071701

1708-
# Check if this is a label image
1709-
is_label = is_label_image(output_filename)
1710-
1711-
if is_label:
1712-
# Choose appropriate integer type based on data range
1713-
if data_max <= 255:
1714-
save_dtype = np.uint8
1715-
elif data_max <= 65535:
1716-
save_dtype = np.uint16
1717-
else:
1718-
save_dtype = np.uint32
1702+
# Layer subdivision outputs should always be saved as uint32
1703+
# to ensure Napari auto-detects them as labels
1704+
save_dtype = np.uint32
17191705

1720-
print(
1721-
f"Label image detected, saving as {save_dtype.__name__} with bigtiff={use_bigtiff}"
1722-
)
1723-
tifffile.imwrite(
1724-
output_path,
1725-
img.astype(save_dtype),
1726-
compression="zlib",
1727-
bigtiff=use_bigtiff,
1728-
)
1729-
else:
1730-
print(
1731-
f"Regular image, saving with dtype {image_dtype} and bigtiff={use_bigtiff}"
1732-
)
1733-
tifffile.imwrite(
1734-
output_path,
1735-
img.astype(image_dtype),
1736-
compression="zlib",
1737-
bigtiff=use_bigtiff,
1738-
)
1706+
print(
1707+
f"Saving layer {layer_name} as {save_dtype.__name__} with bigtiff={use_bigtiff}"
1708+
)
1709+
tifffile.imwrite(
1710+
output_path,
1711+
img.astype(save_dtype),
1712+
compression="zlib",
1713+
bigtiff=use_bigtiff,
1714+
)
17391715

17401716
processed_files.append(output_path)
17411717
else:
@@ -1771,17 +1747,13 @@ def process_file(self, filepath):
17711747
# For very large files, use BigTIFF format
17721748
use_bigtiff = size_gb > 2.0
17731749

1774-
# Check if this is a label image
1775-
is_label = is_label_image(output_filename)
1750+
# Check if this is a label image based on dtype
1751+
is_label = is_label_image(img)
17761752

17771753
if is_label:
1778-
# Choose appropriate integer type based on data range
1779-
if data_max <= 255:
1780-
save_dtype = np.uint8
1781-
elif data_max <= 65535:
1782-
save_dtype = np.uint16
1783-
else:
1784-
save_dtype = np.uint32
1754+
# For labels, always use uint32 to ensure Napari recognizes them
1755+
# Napari auto-detects labels based on dtype (int32/uint32/int64/uint64)
1756+
save_dtype = np.uint32
17851757

17861758
print(
17871759
f"Label image detected, saving as {save_dtype.__name__} with bigtiff={use_bigtiff}"
@@ -1906,17 +1878,13 @@ def process_file(self, filepath):
19061878
# For very large files, use BigTIFF format
19071879
use_bigtiff = size_gb > 2.0
19081880

1909-
# Check if this is a label image by checking filename
1910-
is_label = is_label_image(channel_filename)
1881+
# Check if this is a label image based on dtype
1882+
is_label = is_label_image(channel_image)
19111883

19121884
if is_label:
1913-
# Choose appropriate integer type based on data range
1914-
if data_max <= 255:
1915-
save_dtype = np.uint8
1916-
elif data_max <= 65535:
1917-
save_dtype = np.uint16
1918-
else:
1919-
save_dtype = np.uint32
1885+
# For labels, always use uint32 to ensure Napari recognizes them
1886+
# Napari auto-detects labels based on dtype (int32/uint32/int64/uint64)
1887+
save_dtype = np.uint32
19201888

19211889
print(
19221890
f"Label image detected, saving as {save_dtype.__name__} with bigtiff={use_bigtiff}"
@@ -1970,8 +1938,8 @@ def process_file(self, filepath):
19701938
)
19711939
print(f"Data range: {data_min} to {data_max}")
19721940

1973-
# Check if this is a label image by checking filename
1974-
is_label = is_label_image(new_filename_base)
1941+
# Check if this is a label image based on dtype
1942+
is_label = is_label_image(processed_image)
19751943

19761944
if is_label:
19771945
save_dtype = np.uint32

src/napari_tmidas/_processing_worker.py

Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,11 @@ def process_file(self, filepath):
178178
len(processed_image) == 3
179179
and self.output_suffix == "_layer"
180180
):
181-
layer_names = ["_inner", "_middle", "_outer"]
181+
layer_names = [
182+
"_inner",
183+
"_middle",
184+
"_outer",
185+
]
182186
for img, layer_name in zip(processed_image, layer_names):
183187
if not isinstance(img, np.ndarray):
184188
continue
@@ -189,8 +193,8 @@ def process_file(self, filepath):
189193
self.output_folder, output_filename
190194
)
191195

192-
# Save the processed image
193-
save_image_file(img, output_path, image_dtype)
196+
# Save as uint32 to ensure Napari auto-detects as labels
197+
save_image_file(img, output_path, np.uint32)
194198
processed_files.append(output_path)
195199
else:
196200
# Default behavior for other multi-output functions (e.g., channel splitting)
@@ -253,23 +257,16 @@ def load_image_file(filepath: str) -> Union[np.ndarray, List, Any]:
253257
return np.random.rand(100, 100)
254258

255259

256-
def is_label_image(filename: str) -> bool:
257-
"""Check if filename indicates a label image"""
258-
label_suffixes = [
259-
"labels",
260-
"semantic",
261-
"_binary",
262-
"_inverted",
263-
"_thresh",
264-
"_mask",
265-
"_seg",
266-
"segmentation",
267-
"_inner",
268-
"_middle",
269-
"_outer",
270-
]
271-
filename_lower = filename.lower()
272-
return any(suffix in filename_lower for suffix in label_suffixes)
260+
def is_label_image(image: np.ndarray) -> bool:
261+
"""
262+
Determine if an image should be treated as a label image based on its dtype.
263+
264+
This function uses the same logic as Napari's guess_labels() function,
265+
checking if the dtype is one of the integer types commonly used for labels.
266+
"""
267+
if hasattr(image, "dtype"):
268+
return image.dtype in (np.int32, np.uint32, np.int64, np.uint64)
269+
return False
273270

274271

275272
def save_image_file(image: np.ndarray, filepath: str, dtype=None):
@@ -285,33 +282,20 @@ def save_image_file(image: np.ndarray, filepath: str, dtype=None):
285282
# For very large files, use BigTIFF format
286283
use_bigtiff = size_gb > 2.0
287284

288-
# Check data range
289-
data_max = np.max(image) if image.size > 0 else 0
290-
291-
# Check if this is a label image
292-
is_label = is_label_image(filepath)
293-
294-
if is_label:
295-
# Choose appropriate integer type based on data range
296-
if data_max <= 255:
297-
save_dtype = np.uint8
298-
elif data_max <= 65535:
299-
save_dtype = np.uint16
300-
else:
301-
save_dtype = np.uint32
302-
303-
tifffile.imwrite(
304-
filepath,
305-
image.astype(save_dtype),
306-
compression="zlib",
307-
bigtiff=use_bigtiff,
308-
)
285+
# Determine save dtype
286+
if dtype is not None:
287+
# Use explicitly provided dtype
288+
save_dtype = dtype
289+
elif is_label_image(image):
290+
# Input is already a label dtype, preserve as uint32
291+
save_dtype = np.uint32
309292
else:
310-
# Use provided dtype or image's dtype
311-
save_dtype = dtype if dtype is not None else image.dtype
312-
tifffile.imwrite(
313-
filepath,
314-
image.astype(save_dtype),
315-
compression="zlib",
316-
bigtiff=use_bigtiff,
317-
)
293+
# Use image's dtype
294+
save_dtype = image.dtype
295+
296+
tifffile.imwrite(
297+
filepath,
298+
image.astype(save_dtype),
299+
compression="zlib",
300+
bigtiff=use_bigtiff,
301+
)

src/napari_tmidas/_reader.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
"""
2-
This module is an example of a barebones numpy reader plugin for napari.
3-
4-
It implements the Reader specification, but your plugin may choose to
5-
implement multiple readers or even other plugin contributions. see:
6-
https://napari.org/stable/plugins/guides.html?#readers
2+
This module implements a reader plugin for napari.
73
"""
84

95
import numpy as np
@@ -29,12 +25,12 @@ def napari_get_reader(path):
2925
# so we are only going to look at the first file.
3026
path = path[0]
3127

32-
# if we know we cannot read the file, we immediately return None.
33-
if not path.endswith(".npy"):
34-
return None
28+
# Support .npy files
29+
if path.endswith(".npy"):
30+
return reader_function
3531

36-
# otherwise we return the *function* that can read ``path``.
37-
return reader_function
32+
# if we know we cannot read the file, we immediately return None.
33+
return None
3834

3935

4036
def reader_function(path):

src/napari_tmidas/processing_functions/scipy_filters.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,17 @@ def resize_labels(
9393
@BatchProcessingRegistry.register(
9494
name="Subdivide Labels into 3 Layers",
9595
suffix="_layer",
96-
description="Subdivide each labeled object into 3 concentric layers of equal thickness (inner core, middle shell, outer shell). Each layer is saved as a separate file with _inner, _middle, and _outer suffixes.",
96+
description="Subdivide each labeled object into 3 concentric layers of equal thickness (inner core, middle shell, outer shell). Each layer is saved as a separate file with _inner_labels, _middle_labels, and _outer_labels suffixes.",
9797
parameters={},
9898
)
9999
def subdivide_labels_3layers(label_image: np.ndarray) -> tuple:
100100
"""
101101
Subdivide labeled objects into 3 concentric layers of equal thickness.
102102
103103
Creates three separate label images representing:
104-
- Layer 1 (innermost): Core region at ~33% scale (saved as *_inner.tif)
105-
- Layer 2 (middle): Shell between ~33% and ~67% scale (saved as *_middle.tif)
106-
- Layer 3 (outermost): Shell between ~67% and 100% scale (saved as *_outer.tif)
104+
- Layer 1 (innermost): Core region at ~33% scale (saved as *_inner_labels.tif)
105+
- Layer 2 (middle): Shell between ~33% and ~67% scale (saved as *_middle_labels.tif)
106+
- Layer 3 (outermost): Shell between ~67% and 100% scale (saved as *_outer_labels.tif)
107107
108108
Parameters
109109
----------

0 commit comments

Comments
 (0)