Skip to content

Commit 7007878

Browse files
committed
added filters; optional cellpose-sam diameter for ROI outside pretrained range (7.5-120)
1 parent 0372cc5 commit 7007878

3 files changed

Lines changed: 384 additions & 0 deletions

File tree

src/napari_tmidas/_tests/test_skimage_filters.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22
import numpy as np
33

44
from napari_tmidas.processing_functions.skimage_filters import (
5+
adaptive_threshold_bright,
6+
h_maxima_transform,
57
invert_image,
8+
percentile_threshold,
9+
rolling_ball_background,
610
simple_thresholding,
11+
white_tophat,
712
)
813

914

@@ -67,3 +72,101 @@ def test_simple_thresholding_different_thresholds(self):
6772
assert (
6873
np.sum(result_high == 255) < np.prod(result_high.shape) * 0.3
6974
) # Most pixels below 200
75+
76+
77+
class TestBrightRegionExtraction:
78+
"""Test suite for bright region extraction functions"""
79+
80+
def test_white_tophat_basic(self):
81+
"""Test white top-hat transform extracts bright features"""
82+
# Create image with bright spot on dark background
83+
image = np.zeros((100, 100), dtype=np.uint8)
84+
image[40:60, 40:60] = 200 # Bright square
85+
image[45:55, 45:55] = 255 # Brighter center
86+
87+
result = white_tophat(image, footprint_size=15)
88+
89+
# Result should have bright features extracted
90+
assert result.shape == image.shape
91+
assert result.max() > 0 # Should have some bright regions
92+
assert result.sum() < image.sum() # Background removed
93+
94+
def test_percentile_threshold_original(self):
95+
"""Test percentile thresholding with original values"""
96+
# Create image with gradient
97+
image = np.arange(0, 256, dtype=np.uint8).reshape(16, 16)
98+
99+
result = percentile_threshold(
100+
image, percentile=90, output_type="original"
101+
)
102+
103+
# Only top 10% should remain
104+
assert result.shape == image.shape
105+
assert np.sum(result > 0) < image.size * 0.15 # Allow some margin
106+
assert result.max() == image.max() # Original max value preserved
107+
108+
def test_percentile_threshold_binary(self):
109+
"""Test percentile thresholding with binary output"""
110+
image = np.random.randint(0, 256, size=(50, 50), dtype=np.uint8)
111+
112+
result = percentile_threshold(
113+
image, percentile=80, output_type="binary"
114+
)
115+
116+
# Should be binary
117+
assert result.dtype == np.uint8
118+
assert set(np.unique(result)).issubset({0, 255})
119+
120+
def test_rolling_ball_background_subtraction(self):
121+
"""Test rolling ball background subtraction"""
122+
# Create image with uneven background and bright spot
123+
x, y = np.meshgrid(np.arange(100), np.arange(100))
124+
background = (50 + 30 * np.sin(x / 20) + 30 * np.sin(y / 20)).astype(
125+
np.uint8
126+
)
127+
image = background.copy()
128+
image[40:60, 40:60] += 150 # Add bright feature
129+
130+
result = rolling_ball_background(image, radius=30)
131+
132+
# Background should be reduced
133+
assert result.shape == image.shape
134+
# Center of bright spot should be brighter in result than in corners
135+
assert result[50, 50] > result[10, 10]
136+
137+
def test_h_maxima_transform(self):
138+
"""Test H-maxima transform suppresses small peaks"""
139+
# Create image with peaks of different heights
140+
image = np.zeros((100, 100), dtype=np.uint8)
141+
image[20, 20] = 100 # Small peak
142+
image[50, 50] = 200 # Large peak
143+
image[80, 80] = 80 # Very small peak
144+
145+
result = h_maxima_transform(image, h=50.0)
146+
147+
# Should suppress small peaks, keep large ones
148+
assert result.shape == image.shape
149+
# Large peak should remain prominent
150+
assert result[50, 50] > result[20, 20]
151+
152+
def test_adaptive_threshold_bright(self):
153+
"""Test adaptive thresholding with bright bias"""
154+
# Create image with varying brightness
155+
image = np.random.randint(0, 256, size=(100, 100), dtype=np.uint8)
156+
157+
result = adaptive_threshold_bright(image, block_size=35, offset=-10.0)
158+
159+
# Should be binary
160+
assert result.dtype == np.uint8
161+
assert set(np.unique(result)).issubset({0, 255})
162+
assert result.shape == image.shape
163+
164+
def test_adaptive_threshold_even_blocksize(self):
165+
"""Test that even block size is handled correctly"""
166+
image = np.random.randint(0, 256, size=(50, 50), dtype=np.uint8)
167+
168+
# Should handle even block size by making it odd
169+
result = adaptive_threshold_bright(image, block_size=34, offset=0)
170+
171+
assert result.shape == image.shape
172+
assert result.dtype == np.uint8

src/napari_tmidas/processing_functions/cellpose_segmentation.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ def transpose_dimensions(img, dim_order):
112112
# "max": 3,
113113
# "description": "Second channel: 0=none, 1=green, 2=red, 3=blue",
114114
# },
115+
"diameter": {
116+
"type": float,
117+
"default": 0.0,
118+
"min": 0.0,
119+
"max": 200.0,
120+
"description": "Optional. Only required if your ROI diameter is outside the range 7.5–120. Set to 0 to leave unset (recommended for most users). Cellpose-SAM is trained for diameters in this range.",
121+
},
115122
"flow_threshold": {
116123
"type": float,
117124
"default": 0.4,
@@ -167,6 +174,7 @@ def cellpose_segmentation(
167174
flow3D_smooth: int = 0,
168175
tile_norm_blocksize: int = 128,
169176
batch_size: int = 32,
177+
diameter: float = 0.0,
170178
_source_filepath: str = None, # Hidden parameter for zarr optimization
171179
) -> np.ndarray:
172180
"""
@@ -203,6 +211,8 @@ def cellpose_segmentation(
203211
numpy.ndarray
204212
Segmented image with instance labels
205213
"""
214+
# Diameter parameter guidance:
215+
# Cellpose-SAM is trained for ROI diameters 7.5–120. Only set diameter if your images have objects outside this range (e.g., diameter <7.5 or >120). Otherwise, leave as None.
206216
# Cellpose 4 handles normalization internally via percentile-based normalization
207217
# It accepts uint8, uint16, float32, float64 - no pre-conversion needed!
208218
# The normalize=True parameter (default) will convert to float and normalize
@@ -282,6 +292,7 @@ def cellpose_segmentation(
282292
"anisotropy": anisotropy,
283293
"normalize": {"tile_norm_blocksize": tile_norm_blocksize},
284294
"batch_size": batch_size,
295+
"diameter": diameter,
285296
"use_gpu": True, # Let cellpose environment detect GPU
286297
"do_3D": is_3d,
287298
"z_axis": 0 if is_3d else None,
@@ -299,6 +310,7 @@ def cellpose_segmentation(
299310
"anisotropy": anisotropy,
300311
"normalize": {"tile_norm_blocksize": tile_norm_blocksize},
301312
"batch_size": batch_size,
313+
"diameter": diameter,
302314
"use_gpu": True, # Let cellpose environment detect GPU
303315
"do_3D": is_3d,
304316
"z_axis": 0 if is_3d else None,

0 commit comments

Comments
 (0)