napari-tmidas 0.2.1__py3-none-any.whl → 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- napari_tmidas/__init__.py +35 -5
- napari_tmidas/_crop_anything.py +1458 -499
- napari_tmidas/_env_manager.py +76 -0
- napari_tmidas/_file_conversion.py +1646 -1131
- napari_tmidas/_file_selector.py +1464 -223
- napari_tmidas/_label_inspection.py +83 -8
- napari_tmidas/_processing_worker.py +309 -0
- napari_tmidas/_reader.py +6 -10
- napari_tmidas/_registry.py +15 -14
- napari_tmidas/_roi_colocalization.py +1221 -84
- napari_tmidas/_tests/test_crop_anything.py +123 -0
- napari_tmidas/_tests/test_env_manager.py +89 -0
- napari_tmidas/_tests/test_file_selector.py +90 -0
- napari_tmidas/_tests/test_grid_view_overlay.py +193 -0
- napari_tmidas/_tests/test_init.py +98 -0
- napari_tmidas/_tests/test_intensity_label_filter.py +222 -0
- napari_tmidas/_tests/test_label_inspection.py +86 -0
- napari_tmidas/_tests/test_processing_basic.py +500 -0
- napari_tmidas/_tests/test_processing_worker.py +142 -0
- napari_tmidas/_tests/test_regionprops_analysis.py +547 -0
- napari_tmidas/_tests/test_registry.py +135 -0
- napari_tmidas/_tests/test_scipy_filters.py +168 -0
- napari_tmidas/_tests/test_skimage_filters.py +259 -0
- napari_tmidas/_tests/test_split_channels.py +217 -0
- napari_tmidas/_tests/test_spotiflow.py +87 -0
- napari_tmidas/_tests/test_tyx_display_fix.py +142 -0
- napari_tmidas/_tests/test_ui_utils.py +68 -0
- napari_tmidas/_tests/test_widget.py +30 -0
- napari_tmidas/_tests/test_windows_basic.py +66 -0
- napari_tmidas/_ui_utils.py +57 -0
- napari_tmidas/_version.py +16 -3
- napari_tmidas/_widget.py +41 -4
- napari_tmidas/processing_functions/basic.py +557 -20
- napari_tmidas/processing_functions/careamics_env_manager.py +72 -99
- napari_tmidas/processing_functions/cellpose_env_manager.py +415 -112
- napari_tmidas/processing_functions/cellpose_segmentation.py +132 -191
- napari_tmidas/processing_functions/colocalization.py +513 -56
- napari_tmidas/processing_functions/grid_view_overlay.py +703 -0
- napari_tmidas/processing_functions/intensity_label_filter.py +422 -0
- napari_tmidas/processing_functions/regionprops_analysis.py +1280 -0
- napari_tmidas/processing_functions/sam2_env_manager.py +53 -69
- napari_tmidas/processing_functions/sam2_mp4.py +274 -195
- napari_tmidas/processing_functions/scipy_filters.py +403 -8
- napari_tmidas/processing_functions/skimage_filters.py +424 -212
- napari_tmidas/processing_functions/spotiflow_detection.py +949 -0
- napari_tmidas/processing_functions/spotiflow_env_manager.py +591 -0
- napari_tmidas/processing_functions/timepoint_merger.py +334 -86
- napari_tmidas/processing_functions/trackastra_tracking.py +24 -5
- {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/METADATA +92 -39
- napari_tmidas-0.2.4.dist-info/RECORD +63 -0
- napari_tmidas/_tests/__init__.py +0 -0
- napari_tmidas-0.2.1.dist-info/RECORD +0 -38
- {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/WHEEL +0 -0
- {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/entry_points.txt +0 -0
- {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {napari_tmidas-0.2.1.dist-info → napari_tmidas-0.2.4.dist-info}/top_level.txt +0 -0
|
@@ -12,11 +12,49 @@ import os
|
|
|
12
12
|
import sys
|
|
13
13
|
|
|
14
14
|
import numpy as np
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
from
|
|
19
|
-
|
|
15
|
+
|
|
16
|
+
# Lazy imports for optional heavy dependencies
|
|
17
|
+
try:
|
|
18
|
+
from magicgui import magicgui
|
|
19
|
+
|
|
20
|
+
_HAS_MAGICGUI = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
# Create stub decorator
|
|
23
|
+
def magicgui(*args, **kwargs):
|
|
24
|
+
def decorator(func):
|
|
25
|
+
return func
|
|
26
|
+
|
|
27
|
+
if len(args) == 1 and callable(args[0]) and not kwargs:
|
|
28
|
+
return args[0]
|
|
29
|
+
return decorator
|
|
30
|
+
|
|
31
|
+
_HAS_MAGICGUI = False
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from napari.layers import Labels
|
|
35
|
+
from napari.viewer import Viewer
|
|
36
|
+
|
|
37
|
+
_HAS_NAPARI = True
|
|
38
|
+
except ImportError:
|
|
39
|
+
Labels = None
|
|
40
|
+
Viewer = None
|
|
41
|
+
_HAS_NAPARI = False
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
from qtpy.QtWidgets import QFileDialog, QMessageBox, QPushButton
|
|
45
|
+
|
|
46
|
+
_HAS_QTPY = True
|
|
47
|
+
except ImportError:
|
|
48
|
+
QFileDialog = QMessageBox = QPushButton = None
|
|
49
|
+
_HAS_QTPY = False
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
from skimage.io import imread # , imsave
|
|
53
|
+
|
|
54
|
+
_HAS_SKIMAGE = True
|
|
55
|
+
except ImportError:
|
|
56
|
+
imread = None
|
|
57
|
+
_HAS_SKIMAGE = False
|
|
20
58
|
|
|
21
59
|
sys.path.append("src/napari_tmidas")
|
|
22
60
|
|
|
@@ -27,6 +65,43 @@ class LabelInspector:
|
|
|
27
65
|
self.image_label_pairs = []
|
|
28
66
|
self.current_index = 0
|
|
29
67
|
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
# Internal helpers
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
def _can_show_message(self) -> bool:
|
|
72
|
+
"""Return True if it's (probably) safe to show a QMessageBox.
|
|
73
|
+
|
|
74
|
+
On Windows CI (headless) creating a modal dialog without a running
|
|
75
|
+
QApplication or with a mocked viewer can cause access violations.
|
|
76
|
+
We suppress dialogs when:
|
|
77
|
+
* No QApplication instance exists
|
|
78
|
+
* Running under pytest (detected via env var)
|
|
79
|
+
* The provided viewer is a mock (has no 'window' attr)
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
from qtpy.QtWidgets import QApplication
|
|
83
|
+
|
|
84
|
+
if QApplication.instance() is None:
|
|
85
|
+
return False
|
|
86
|
+
except (ImportError, RuntimeError):
|
|
87
|
+
return False
|
|
88
|
+
if "PYTEST_CURRENT_TEST" in os.environ:
|
|
89
|
+
return False
|
|
90
|
+
return hasattr(self.viewer, "window")
|
|
91
|
+
|
|
92
|
+
def _show_message(self, level: str, title: str, text: str):
|
|
93
|
+
"""Safely show a QMessageBox if environment allows, otherwise noop."""
|
|
94
|
+
if not self._can_show_message():
|
|
95
|
+
return
|
|
96
|
+
try:
|
|
97
|
+
if level == "warning":
|
|
98
|
+
QMessageBox.warning(None, title, text)
|
|
99
|
+
else:
|
|
100
|
+
QMessageBox.information(None, title, text)
|
|
101
|
+
except (RuntimeError, ValueError, OSError):
|
|
102
|
+
# Never let common GUI/runtime issues crash tests
|
|
103
|
+
pass
|
|
104
|
+
|
|
30
105
|
def load_image_label_pairs(self, folder_path: str, label_suffix: str):
|
|
31
106
|
"""
|
|
32
107
|
Load image-label pairs from a folder.
|
|
@@ -47,8 +122,8 @@ class LabelInspector:
|
|
|
47
122
|
|
|
48
123
|
if not potential_label_files:
|
|
49
124
|
self.viewer.status = f"No files found with suffix '{label_suffix}'"
|
|
50
|
-
|
|
51
|
-
|
|
125
|
+
self._show_message(
|
|
126
|
+
"warning",
|
|
52
127
|
"No Label Files Found",
|
|
53
128
|
f"No files containing '{label_suffix}' were found in {folder_path}.",
|
|
54
129
|
)
|
|
@@ -128,7 +203,7 @@ class LabelInspector:
|
|
|
128
203
|
for file, issue in format_issues:
|
|
129
204
|
msg += f"- {file}: {issue}\n"
|
|
130
205
|
|
|
131
|
-
|
|
206
|
+
self._show_message("info", "Loading Report", msg)
|
|
132
207
|
|
|
133
208
|
def _load_current_pair(self):
|
|
134
209
|
"""
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Processing worker for batch image processing.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import concurrent.futures
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, List, Union
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
# Lazy imports for optional heavy dependencies
|
|
12
|
+
try:
|
|
13
|
+
import tifffile
|
|
14
|
+
|
|
15
|
+
_HAS_TIFFFILE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
tifffile = None
|
|
18
|
+
_HAS_TIFFFILE = False
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from qtpy.QtCore import QThread, Signal
|
|
22
|
+
|
|
23
|
+
_HAS_QTPY = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
# Create stubs to allow class definitions
|
|
26
|
+
class QThread:
|
|
27
|
+
def __init__(self):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def run(self):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def Signal(*args):
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
_HAS_QTPY = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ProcessingWorker(QThread):
|
|
40
|
+
"""
|
|
41
|
+
Worker thread for processing images in the background
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Signals to communicate with the main thread
|
|
45
|
+
progress_updated = Signal(int)
|
|
46
|
+
file_processed = Signal(dict)
|
|
47
|
+
processing_finished = Signal()
|
|
48
|
+
error_occurred = Signal(str, str) # filepath, error message
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
file_list,
|
|
53
|
+
processing_func,
|
|
54
|
+
param_values,
|
|
55
|
+
output_folder,
|
|
56
|
+
input_suffix,
|
|
57
|
+
output_suffix,
|
|
58
|
+
):
|
|
59
|
+
super().__init__()
|
|
60
|
+
self.file_list = file_list
|
|
61
|
+
self.processing_func = processing_func
|
|
62
|
+
self.param_values = param_values
|
|
63
|
+
self.output_folder = output_folder
|
|
64
|
+
self.input_suffix = input_suffix
|
|
65
|
+
self.output_suffix = output_suffix
|
|
66
|
+
self.stop_requested = False
|
|
67
|
+
self.thread_count = max(1, (os.cpu_count() or 4) - 1) # Default value
|
|
68
|
+
|
|
69
|
+
def stop(self):
|
|
70
|
+
"""Request the worker to stop processing"""
|
|
71
|
+
self.stop_requested = True
|
|
72
|
+
|
|
73
|
+
def run(self):
|
|
74
|
+
"""Process files in a separate thread"""
|
|
75
|
+
# Track processed files
|
|
76
|
+
processed_files_info = []
|
|
77
|
+
total_files = len(self.file_list)
|
|
78
|
+
|
|
79
|
+
# Create a thread pool for concurrent processing with specified thread count
|
|
80
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
81
|
+
max_workers=self.thread_count
|
|
82
|
+
) as executor:
|
|
83
|
+
# Submit tasks
|
|
84
|
+
future_to_file = {
|
|
85
|
+
executor.submit(self.process_file, filepath): filepath
|
|
86
|
+
for filepath in self.file_list
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Process as they complete
|
|
90
|
+
for i, future in enumerate(
|
|
91
|
+
concurrent.futures.as_completed(future_to_file)
|
|
92
|
+
):
|
|
93
|
+
# Check if cancellation was requested
|
|
94
|
+
if self.stop_requested:
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
filepath = future_to_file[future]
|
|
98
|
+
try:
|
|
99
|
+
result = future.result()
|
|
100
|
+
# Only process result if it's not None (folder functions may return None)
|
|
101
|
+
if result is not None:
|
|
102
|
+
processed_files_info.append(result)
|
|
103
|
+
self.file_processed.emit(result)
|
|
104
|
+
except (
|
|
105
|
+
ValueError,
|
|
106
|
+
TypeError,
|
|
107
|
+
OSError,
|
|
108
|
+
tifffile.TiffFileError,
|
|
109
|
+
) as e:
|
|
110
|
+
self.error_occurred.emit(filepath, str(e))
|
|
111
|
+
|
|
112
|
+
# Update progress
|
|
113
|
+
self.progress_updated.emit(int((i + 1) / total_files * 100))
|
|
114
|
+
|
|
115
|
+
# Signal that processing is complete
|
|
116
|
+
self.processing_finished.emit()
|
|
117
|
+
|
|
118
|
+
def process_file(self, filepath):
|
|
119
|
+
"""Process a single file with support for large TIFF and Zarr files"""
|
|
120
|
+
try:
|
|
121
|
+
# Load the image using the unified loader
|
|
122
|
+
image_data = load_image_file(filepath)
|
|
123
|
+
|
|
124
|
+
# Handle multi-layer data from OME-Zarr - extract first layer for processing
|
|
125
|
+
if isinstance(image_data, list):
|
|
126
|
+
print(
|
|
127
|
+
f"Processing first layer of multi-layer file: {filepath}"
|
|
128
|
+
)
|
|
129
|
+
# Take the first image layer
|
|
130
|
+
for data, _add_kwargs, layer_type in image_data:
|
|
131
|
+
if layer_type == "image":
|
|
132
|
+
image = data
|
|
133
|
+
break
|
|
134
|
+
else:
|
|
135
|
+
# No image layer found, take first available
|
|
136
|
+
image = image_data[0][0]
|
|
137
|
+
else:
|
|
138
|
+
image = image_data
|
|
139
|
+
|
|
140
|
+
# Store original dtype for saving
|
|
141
|
+
if hasattr(image, "dtype"):
|
|
142
|
+
image_dtype = image.dtype
|
|
143
|
+
else:
|
|
144
|
+
image_dtype = np.float32
|
|
145
|
+
|
|
146
|
+
# Check if this is a folder-processing function that shouldn't save individual files
|
|
147
|
+
function_name = getattr(
|
|
148
|
+
self.processing_func, "__name__", "unknown"
|
|
149
|
+
)
|
|
150
|
+
is_folder_function = function_name in [
|
|
151
|
+
"merge_timepoints",
|
|
152
|
+
"track_objects",
|
|
153
|
+
"create_grid_overlay", # Grid overlay processes all files at once
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
# Only print verbose output for non-folder functions
|
|
157
|
+
if not is_folder_function:
|
|
158
|
+
print(
|
|
159
|
+
f"Original image shape: {image.shape if hasattr(image, 'shape') else 'unknown'}, dtype: {image_dtype}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Apply the processing function with parameters
|
|
163
|
+
if self.param_values:
|
|
164
|
+
processed_image = self.processing_func(
|
|
165
|
+
image, **self.param_values
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
processed_image = self.processing_func(image)
|
|
169
|
+
|
|
170
|
+
# Handle functions that return multiple outputs (e.g., channel splitting)
|
|
171
|
+
if (
|
|
172
|
+
isinstance(processed_image, (list, tuple))
|
|
173
|
+
and len(processed_image) > 1
|
|
174
|
+
):
|
|
175
|
+
# Multiple outputs - save each as separate file
|
|
176
|
+
processed_files = []
|
|
177
|
+
base_name = os.path.splitext(os.path.basename(filepath))[0]
|
|
178
|
+
|
|
179
|
+
# Check if this is a layer subdivision function (returns 3 outputs)
|
|
180
|
+
if (
|
|
181
|
+
len(processed_image) == 3
|
|
182
|
+
and self.output_suffix == "_layer"
|
|
183
|
+
):
|
|
184
|
+
layer_names = [
|
|
185
|
+
"_inner",
|
|
186
|
+
"_middle",
|
|
187
|
+
"_outer",
|
|
188
|
+
]
|
|
189
|
+
for img, layer_name in zip(processed_image, layer_names):
|
|
190
|
+
if not isinstance(img, np.ndarray):
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
# Generate output filename with layer name
|
|
194
|
+
output_filename = f"{base_name}{layer_name}.tif"
|
|
195
|
+
output_path = os.path.join(
|
|
196
|
+
self.output_folder, output_filename
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Save as uint32 to ensure Napari auto-detects as labels
|
|
200
|
+
save_image_file(img, output_path, np.uint32)
|
|
201
|
+
processed_files.append(output_path)
|
|
202
|
+
else:
|
|
203
|
+
# Default behavior for other multi-output functions (e.g., channel splitting)
|
|
204
|
+
for idx, img in enumerate(processed_image):
|
|
205
|
+
if not isinstance(img, np.ndarray):
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# Generate output filename
|
|
209
|
+
output_filename = (
|
|
210
|
+
f"{base_name}_ch{idx + 1}{self.output_suffix}"
|
|
211
|
+
)
|
|
212
|
+
output_path = os.path.join(
|
|
213
|
+
self.output_folder, output_filename
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Save the processed image
|
|
217
|
+
save_image_file(img, output_path, image_dtype)
|
|
218
|
+
processed_files.append(output_path)
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
"original_file": filepath,
|
|
222
|
+
"processed_files": processed_files,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
elif processed_image is not None and not is_folder_function:
|
|
226
|
+
# Single output - save as single file
|
|
227
|
+
base_name = os.path.splitext(os.path.basename(filepath))[0]
|
|
228
|
+
output_filename = f"{base_name}{self.output_suffix}"
|
|
229
|
+
output_path = os.path.join(self.output_folder, output_filename)
|
|
230
|
+
|
|
231
|
+
# Save the processed image
|
|
232
|
+
save_image_file(processed_image, output_path, image_dtype)
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
"original_file": filepath,
|
|
236
|
+
"processed_file": output_path,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
else:
|
|
240
|
+
# Folder function or no output to save
|
|
241
|
+
return {
|
|
242
|
+
"original_file": filepath,
|
|
243
|
+
"processed_file": None,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
print(f"Error processing {filepath}: {e}")
|
|
248
|
+
import traceback
|
|
249
|
+
|
|
250
|
+
traceback.print_exc()
|
|
251
|
+
raise
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def load_image_file(filepath: str) -> Union[np.ndarray, List, Any]:
|
|
255
|
+
"""
|
|
256
|
+
Load image from file, supporting both TIFF and Zarr formats with proper metadata handling
|
|
257
|
+
"""
|
|
258
|
+
# This is a placeholder - the actual implementation would be moved from _file_selector.py
|
|
259
|
+
# For now, return a dummy array
|
|
260
|
+
return np.random.rand(100, 100)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def is_label_image(image: np.ndarray) -> bool:
|
|
264
|
+
"""
|
|
265
|
+
Determine if an image should be treated as a label image based on its dtype.
|
|
266
|
+
|
|
267
|
+
This function uses the same logic as Napari's guess_labels() function,
|
|
268
|
+
checking if the dtype is one of the integer types commonly used for labels.
|
|
269
|
+
"""
|
|
270
|
+
if hasattr(image, "dtype"):
|
|
271
|
+
return image.dtype in (np.int32, np.uint32, np.int64, np.uint64)
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def save_image_file(image: np.ndarray, filepath: str, dtype=None):
|
|
276
|
+
"""
|
|
277
|
+
Save image to file with proper format detection.
|
|
278
|
+
|
|
279
|
+
Label images are saved as uint32 to ensure napari recognizes them as labels.
|
|
280
|
+
Napari automatically detects int32/uint32/int64/uint64 dtypes as labels.
|
|
281
|
+
"""
|
|
282
|
+
if not _HAS_TIFFFILE:
|
|
283
|
+
raise ImportError("tifffile is required to save images")
|
|
284
|
+
|
|
285
|
+
# Calculate approx file size in GB
|
|
286
|
+
size_gb = image.size * image.itemsize / (1024**3)
|
|
287
|
+
|
|
288
|
+
# For very large files, use BigTIFF format
|
|
289
|
+
use_bigtiff = size_gb > 2.0
|
|
290
|
+
|
|
291
|
+
# Determine save dtype
|
|
292
|
+
if dtype is not None:
|
|
293
|
+
# Use explicitly provided dtype
|
|
294
|
+
save_dtype = dtype
|
|
295
|
+
elif is_label_image(image):
|
|
296
|
+
# Input is already a label dtype, preserve as uint32
|
|
297
|
+
# uint32 is the standard for label images and is automatically
|
|
298
|
+
# recognized by napari
|
|
299
|
+
save_dtype = np.uint32
|
|
300
|
+
else:
|
|
301
|
+
# Use image's dtype
|
|
302
|
+
save_dtype = image.dtype
|
|
303
|
+
|
|
304
|
+
tifffile.imwrite(
|
|
305
|
+
filepath,
|
|
306
|
+
image.astype(save_dtype),
|
|
307
|
+
compression="zlib",
|
|
308
|
+
bigtiff=use_bigtiff,
|
|
309
|
+
)
|
napari_tmidas/_reader.py
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
This module
|
|
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.
|
|
7
3
|
"""
|
|
8
4
|
|
|
9
5
|
import numpy as np
|
|
@@ -29,12 +25,12 @@ def napari_get_reader(path):
|
|
|
29
25
|
# so we are only going to look at the first file.
|
|
30
26
|
path = path[0]
|
|
31
27
|
|
|
32
|
-
#
|
|
33
|
-
if
|
|
34
|
-
return
|
|
28
|
+
# Support .npy files
|
|
29
|
+
if path.endswith(".npy"):
|
|
30
|
+
return reader_function
|
|
35
31
|
|
|
36
|
-
#
|
|
37
|
-
return
|
|
32
|
+
# if we know we cannot read the file, we immediately return None.
|
|
33
|
+
return None
|
|
38
34
|
|
|
39
35
|
|
|
40
36
|
def reader_function(path):
|
napari_tmidas/_registry.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Registry for batch processing functions.
|
|
4
4
|
"""
|
|
5
|
+
import threading
|
|
5
6
|
from typing import Any, Dict, List, Optional
|
|
6
7
|
|
|
7
8
|
|
|
@@ -11,6 +12,7 @@ class BatchProcessingRegistry:
|
|
|
11
12
|
"""
|
|
12
13
|
|
|
13
14
|
_processing_functions = {}
|
|
15
|
+
_lock = threading.RLock() # Add thread lock
|
|
14
16
|
|
|
15
17
|
@classmethod
|
|
16
18
|
def register(
|
|
@@ -43,26 +45,25 @@ class BatchProcessingRegistry:
|
|
|
43
45
|
parameters = {}
|
|
44
46
|
|
|
45
47
|
def decorator(func):
|
|
46
|
-
cls.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
with cls._lock: # Thread-safe registration
|
|
49
|
+
cls._processing_functions[name] = {
|
|
50
|
+
"func": func,
|
|
51
|
+
"suffix": suffix,
|
|
52
|
+
"description": description,
|
|
53
|
+
"parameters": parameters,
|
|
54
|
+
}
|
|
52
55
|
return func
|
|
53
56
|
|
|
54
57
|
return decorator
|
|
55
58
|
|
|
56
59
|
@classmethod
|
|
57
60
|
def get_function_info(cls, name: str) -> Optional[dict]:
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return cls._processing_functions.get(name)
|
|
61
|
+
"""Thread-safe retrieval"""
|
|
62
|
+
with cls._lock:
|
|
63
|
+
return cls._processing_functions.get(name)
|
|
62
64
|
|
|
63
65
|
@classmethod
|
|
64
66
|
def list_functions(cls) -> List[str]:
|
|
65
|
-
"""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return list(cls._processing_functions.keys())
|
|
67
|
+
"""Thread-safe listing, returns alphabetically sorted list"""
|
|
68
|
+
with cls._lock:
|
|
69
|
+
return sorted(cls._processing_functions.keys())
|