Skip to content

Commit 2299c5f

Browse files
committed
Add filter_label_by_id function and sort processing functions alphabetically
- Add new 'Filter Label by ID' processing function to keep only a specified label ID and set all others to background - Implement using np.where for clean, idiomatic NumPy code - Sort processing functions dropdown alphabetically in BatchProcessingRegistry.list_functions() - Add comprehensive test coverage with 6 test cases for the new filter function
1 parent d03b022 commit 2299c5f

3 files changed

Lines changed: 92 additions & 2 deletions

File tree

src/napari_tmidas/_registry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,6 @@ def get_function_info(cls, name: str) -> Optional[dict]:
6464

6565
@classmethod
6666
def list_functions(cls) -> List[str]:
67-
"""Thread-safe listing"""
67+
"""Thread-safe listing, returns alphabetically sorted list"""
6868
with cls._lock:
69-
return list(cls._processing_functions.keys())
69+
return sorted(cls._processing_functions.keys())

src/napari_tmidas/_tests/test_processing_basic.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from napari_tmidas.processing_functions.basic import (
6+
filter_label_by_id,
67
intersect_label_images,
78
invert_binary_labels,
89
keep_slice_range_by_area,
@@ -129,6 +130,60 @@ def test_invert_binary_labels_empty(self):
129130
assert result.shape == (0, 0)
130131
assert result.dtype == np.uint8
131132

133+
def test_filter_label_by_id_basic(self):
134+
"""Test filtering to keep only one label ID"""
135+
# Create test label image with multiple labels
136+
labels = np.array([[0, 1, 2], [3, 1, 2], [1, 0, 3]], dtype=np.uint32)
137+
138+
# Keep only label 1
139+
result = filter_label_by_id(labels, label_id=1)
140+
141+
# Check result - only label 1 should remain, others become 0
142+
expected = np.array([[0, 1, 0], [0, 1, 0], [1, 0, 0]], dtype=np.uint32)
143+
np.testing.assert_array_equal(result, expected)
144+
assert result.dtype == labels.dtype
145+
146+
def test_filter_label_by_id_default_param(self):
147+
"""Test filtering with default parameter (label_id=1)"""
148+
labels = np.array([[0, 1, 2], [1, 2, 0], [2, 0, 1]], dtype=np.uint32)
149+
result = filter_label_by_id(labels)
150+
expected = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1]], dtype=np.uint32)
151+
np.testing.assert_array_equal(result, expected)
152+
153+
def test_filter_label_by_id_nonexistent(self):
154+
"""Test filtering with label ID that doesn't exist"""
155+
labels = np.array([[1, 2, 3], [2, 3, 1], [3, 1, 2]], dtype=np.uint32)
156+
# Try to keep label 99 which doesn't exist
157+
result = filter_label_by_id(labels, label_id=99)
158+
# All should become background
159+
expected = np.zeros_like(labels)
160+
np.testing.assert_array_equal(result, expected)
161+
162+
def test_filter_label_by_id_3d(self):
163+
"""Test filtering with 3D label image"""
164+
labels = np.array(
165+
[[[1, 2], [3, 1]], [[2, 1], [1, 3]]], dtype=np.uint32
166+
)
167+
result = filter_label_by_id(labels, label_id=2)
168+
expected = np.array(
169+
[[[0, 2], [0, 0]], [[2, 0], [0, 0]]], dtype=np.uint32
170+
)
171+
np.testing.assert_array_equal(result, expected)
172+
173+
def test_filter_label_by_id_all_same(self):
174+
"""Test filtering when all pixels are the target label"""
175+
labels = np.ones((3, 3), dtype=np.uint32) * 5
176+
result = filter_label_by_id(labels, label_id=5)
177+
# All should remain
178+
np.testing.assert_array_equal(result, labels)
179+
180+
def test_filter_label_by_id_all_background(self):
181+
"""Test filtering with all background"""
182+
labels = np.zeros((3, 3), dtype=np.uint32)
183+
result = filter_label_by_id(labels, label_id=1)
184+
# Should remain all zeros
185+
np.testing.assert_array_equal(result, labels)
186+
132187
def test_mirror_labels_double_size_default_axis(self):
133188
"""Mirroring keeps the same shape and mirrors around largest area slice"""
134189
image = np.zeros((4, 2, 2), dtype=np.uint16)

src/napari_tmidas/processing_functions/basic.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,41 @@ def invert_binary_labels(image: np.ndarray) -> np.ndarray:
134134
return result
135135

136136

137+
@BatchProcessingRegistry.register(
138+
name="Filter Label by ID",
139+
suffix="_filtered",
140+
description="Keep only the specified label ID, set all other labels to background (0)",
141+
parameters={
142+
"label_id": {
143+
"type": int,
144+
"default": 1,
145+
"min": 1,
146+
"description": "Label ID to keep (all others become background)",
147+
}
148+
},
149+
)
150+
def filter_label_by_id(image: np.ndarray, label_id: int = 1) -> np.ndarray:
151+
"""
152+
Filter a label image to keep only the specified label ID.
153+
All other label IDs are set to background (0).
154+
155+
Parameters
156+
----------
157+
image : np.ndarray
158+
Input label image
159+
label_id : int
160+
The label ID to keep (default: 1)
161+
162+
Returns
163+
-------
164+
np.ndarray
165+
Filtered label image with only the specified label ID preserved
166+
"""
167+
arr = _to_array(image)
168+
result = np.where(arr == label_id, arr, 0).astype(arr.dtype)
169+
return result
170+
171+
137172
@BatchProcessingRegistry.register(
138173
name="Mirror Labels",
139174
suffix="_mirrored",

0 commit comments

Comments
 (0)