napari-tmidas 0.1.1__py3-none-any.whl → 0.1.3__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 +4 -10
- napari_tmidas/_file_selector.py +518 -0
- napari_tmidas/_label_inspection.py +160 -0
- napari_tmidas/_registry.py +68 -0
- napari_tmidas/_version.py +2 -2
- napari_tmidas/napari.yaml +10 -21
- napari_tmidas/processing_functions/__init__.py +61 -0
- napari_tmidas/processing_functions/basic.py +60 -0
- napari_tmidas/processing_functions/scipy_filters.py +57 -0
- napari_tmidas/processing_functions/skimage_filters.py +113 -0
- {napari_tmidas-0.1.1.dist-info → napari_tmidas-0.1.3.dist-info}/METADATA +49 -16
- napari_tmidas-0.1.3.dist-info/RECORD +25 -0
- {napari_tmidas-0.1.1.dist-info → napari_tmidas-0.1.3.dist-info}/WHEEL +1 -1
- napari_tmidas-0.1.1.dist-info/RECORD +0 -18
- {napari_tmidas-0.1.1.dist-info → napari_tmidas-0.1.3.dist-info}/LICENSE +0 -0
- {napari_tmidas-0.1.1.dist-info → napari_tmidas-0.1.3.dist-info}/entry_points.txt +0 -0
- {napari_tmidas-0.1.1.dist-info → napari_tmidas-0.1.3.dist-info}/top_level.txt +0 -0
napari_tmidas/__init__.py
CHANGED
|
@@ -3,14 +3,10 @@ try:
|
|
|
3
3
|
except ImportError:
|
|
4
4
|
__version__ = "unknown"
|
|
5
5
|
|
|
6
|
+
|
|
7
|
+
from ._label_inspection import label_inspector_widget
|
|
6
8
|
from ._reader import napari_get_reader
|
|
7
9
|
from ._sample_data import make_sample_data
|
|
8
|
-
from ._widget import (
|
|
9
|
-
ExampleQWidget,
|
|
10
|
-
ImageThreshold,
|
|
11
|
-
threshold_autogenerate_widget,
|
|
12
|
-
threshold_magic_widget,
|
|
13
|
-
)
|
|
14
10
|
from ._writer import write_multiple, write_single_image
|
|
15
11
|
|
|
16
12
|
__all__ = (
|
|
@@ -18,8 +14,6 @@ __all__ = (
|
|
|
18
14
|
"write_single_image",
|
|
19
15
|
"write_multiple",
|
|
20
16
|
"make_sample_data",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"threshold_autogenerate_widget",
|
|
24
|
-
"threshold_magic_widget",
|
|
17
|
+
"file_selector",
|
|
18
|
+
"label_inspector_widget",
|
|
25
19
|
)
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
import napari
|
|
7
|
+
import numpy as np
|
|
8
|
+
import tifffile
|
|
9
|
+
from magicgui import magicgui
|
|
10
|
+
from qtpy.QtCore import Qt
|
|
11
|
+
from qtpy.QtWidgets import (
|
|
12
|
+
QComboBox,
|
|
13
|
+
QDoubleSpinBox,
|
|
14
|
+
QFormLayout,
|
|
15
|
+
QHeaderView,
|
|
16
|
+
QLabel,
|
|
17
|
+
QLineEdit,
|
|
18
|
+
QPushButton,
|
|
19
|
+
QSpinBox,
|
|
20
|
+
QTableWidget,
|
|
21
|
+
QTableWidgetItem,
|
|
22
|
+
QVBoxLayout,
|
|
23
|
+
QWidget,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Import registry and processing functions
|
|
27
|
+
from napari_tmidas._registry import BatchProcessingRegistry
|
|
28
|
+
|
|
29
|
+
sys.path.append("src/napari_tmidas")
|
|
30
|
+
from napari_tmidas.processing_functions import (
|
|
31
|
+
discover_and_load_processing_functions,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ProcessedFilesTableWidget(QTableWidget):
|
|
36
|
+
"""
|
|
37
|
+
Custom table widget with lazy loading and processing capabilities
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, viewer: napari.Viewer):
|
|
41
|
+
super().__init__()
|
|
42
|
+
self.viewer = viewer
|
|
43
|
+
|
|
44
|
+
# Configure table
|
|
45
|
+
self.setColumnCount(2)
|
|
46
|
+
self.setHorizontalHeaderLabels(["Original Files", "Processed Files"])
|
|
47
|
+
self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
|
48
|
+
|
|
49
|
+
# Track file mappings
|
|
50
|
+
self.file_pairs = {}
|
|
51
|
+
|
|
52
|
+
# Currently loaded images
|
|
53
|
+
self.current_original_image = None
|
|
54
|
+
self.current_processed_image = None
|
|
55
|
+
|
|
56
|
+
def add_initial_files(self, file_list: List[str]):
|
|
57
|
+
"""
|
|
58
|
+
Add initial files to the table
|
|
59
|
+
"""
|
|
60
|
+
# Clear existing rows
|
|
61
|
+
self.setRowCount(0)
|
|
62
|
+
self.file_pairs.clear()
|
|
63
|
+
|
|
64
|
+
# Add files
|
|
65
|
+
for filepath in file_list:
|
|
66
|
+
row = self.rowCount()
|
|
67
|
+
self.insertRow(row)
|
|
68
|
+
|
|
69
|
+
# Original file item
|
|
70
|
+
original_item = QTableWidgetItem(os.path.basename(filepath))
|
|
71
|
+
original_item.setData(Qt.UserRole, filepath)
|
|
72
|
+
self.setItem(row, 0, original_item)
|
|
73
|
+
|
|
74
|
+
# Initially empty processed file column
|
|
75
|
+
processed_item = QTableWidgetItem("")
|
|
76
|
+
self.setItem(row, 1, processed_item)
|
|
77
|
+
|
|
78
|
+
# Store file pair
|
|
79
|
+
self.file_pairs[filepath] = {
|
|
80
|
+
"original": filepath,
|
|
81
|
+
"processed": None,
|
|
82
|
+
"row": row,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def update_processed_files(self, processing_info: dict):
|
|
86
|
+
"""
|
|
87
|
+
Update table with processed files
|
|
88
|
+
|
|
89
|
+
processing_info: {
|
|
90
|
+
'original_file': original filepath,
|
|
91
|
+
'processed_file': processed filepath
|
|
92
|
+
}
|
|
93
|
+
"""
|
|
94
|
+
for item in processing_info:
|
|
95
|
+
original_file = item["original_file"]
|
|
96
|
+
processed_file = item["processed_file"]
|
|
97
|
+
|
|
98
|
+
# Find the corresponding row
|
|
99
|
+
if original_file in self.file_pairs:
|
|
100
|
+
row = self.file_pairs[original_file]["row"]
|
|
101
|
+
|
|
102
|
+
# Update processed file column
|
|
103
|
+
processed_item = QTableWidgetItem(
|
|
104
|
+
os.path.basename(processed_file)
|
|
105
|
+
)
|
|
106
|
+
processed_item.setData(Qt.UserRole, processed_file)
|
|
107
|
+
self.setItem(row, 1, processed_item)
|
|
108
|
+
|
|
109
|
+
# Update file pairs
|
|
110
|
+
self.file_pairs[original_file]["processed"] = processed_file
|
|
111
|
+
|
|
112
|
+
def mousePressEvent(self, event):
|
|
113
|
+
"""
|
|
114
|
+
Load image when clicked
|
|
115
|
+
"""
|
|
116
|
+
if event.button() == Qt.LeftButton:
|
|
117
|
+
item = self.itemAt(event.pos())
|
|
118
|
+
if item:
|
|
119
|
+
filepath = item.data(Qt.UserRole)
|
|
120
|
+
if filepath:
|
|
121
|
+
# Determine which column was clicked
|
|
122
|
+
column = self.columnAt(event.pos().x())
|
|
123
|
+
if column == 0:
|
|
124
|
+
# Original image clicked
|
|
125
|
+
self._load_original_image(filepath)
|
|
126
|
+
elif column == 1 and filepath:
|
|
127
|
+
# Processed image clicked
|
|
128
|
+
self._load_processed_image(filepath)
|
|
129
|
+
|
|
130
|
+
super().mousePressEvent(event)
|
|
131
|
+
|
|
132
|
+
def _load_original_image(self, filepath: str):
|
|
133
|
+
"""
|
|
134
|
+
Load original image into viewer
|
|
135
|
+
"""
|
|
136
|
+
# Remove existing original layer if it exists
|
|
137
|
+
if self.current_original_image is not None:
|
|
138
|
+
with contextlib.suppress(KeyError):
|
|
139
|
+
self.viewer.layers.remove(self.current_original_image)
|
|
140
|
+
|
|
141
|
+
# Load new image
|
|
142
|
+
try:
|
|
143
|
+
image = tifffile.imread(filepath)
|
|
144
|
+
self.current_original_image = self.viewer.add_image(
|
|
145
|
+
image, name=f"Original: {os.path.basename(filepath)}"
|
|
146
|
+
)
|
|
147
|
+
except (ValueError, TypeError, OSError, tifffile.TiffFileError) as e:
|
|
148
|
+
print(f"Error loading original image {filepath}: {e}")
|
|
149
|
+
self.viewer.status = f"Error processing {filepath}: {e}"
|
|
150
|
+
|
|
151
|
+
def _load_processed_image(self, filepath: str):
|
|
152
|
+
"""
|
|
153
|
+
Load processed image into viewer, distinguishing labels by filename pattern
|
|
154
|
+
"""
|
|
155
|
+
# Remove existing processed layer if it exists
|
|
156
|
+
if self.current_processed_image is not None:
|
|
157
|
+
with contextlib.suppress(KeyError):
|
|
158
|
+
self.viewer.layers.remove(self.current_processed_image)
|
|
159
|
+
|
|
160
|
+
# Load new image
|
|
161
|
+
try:
|
|
162
|
+
image = tifffile.imread(filepath)
|
|
163
|
+
filename = os.path.basename(filepath)
|
|
164
|
+
|
|
165
|
+
# Check if filename contains label indicators
|
|
166
|
+
is_label = "labels" in filename or "semantic" in filename
|
|
167
|
+
|
|
168
|
+
# Add the layer using the appropriate method
|
|
169
|
+
if is_label:
|
|
170
|
+
# Ensure it's an appropriate dtype for labels
|
|
171
|
+
if not np.issubdtype(image.dtype, np.integer):
|
|
172
|
+
image = image.astype(np.uint32)
|
|
173
|
+
|
|
174
|
+
self.current_processed_image = self.viewer.add_labels(
|
|
175
|
+
image, name=f"Labels: {filename}"
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
self.current_processed_image = self.viewer.add_image(
|
|
179
|
+
image, name=f"Processed: {filename}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
except (ValueError, TypeError) as e:
|
|
183
|
+
print(f"Error loading processed image {filepath}: {e}")
|
|
184
|
+
self.viewer.status = f"Error processing {filepath}: {e}"
|
|
185
|
+
|
|
186
|
+
def _load_image(self, filepath: str):
|
|
187
|
+
"""
|
|
188
|
+
Legacy method kept for compatibility
|
|
189
|
+
"""
|
|
190
|
+
self._load_original_image(filepath)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ParameterWidget(QWidget):
|
|
194
|
+
"""
|
|
195
|
+
Widget to display and edit processing function parameters
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(self, parameters: Dict[str, Dict[str, Any]]):
|
|
199
|
+
super().__init__()
|
|
200
|
+
|
|
201
|
+
self.parameters = parameters
|
|
202
|
+
self.param_widgets = {}
|
|
203
|
+
|
|
204
|
+
layout = QFormLayout()
|
|
205
|
+
self.setLayout(layout)
|
|
206
|
+
|
|
207
|
+
# Create widgets for each parameter
|
|
208
|
+
for param_name, param_info in parameters.items():
|
|
209
|
+
param_type = param_info.get("type")
|
|
210
|
+
default_value = param_info.get("default")
|
|
211
|
+
min_value = param_info.get("min")
|
|
212
|
+
max_value = param_info.get("max")
|
|
213
|
+
description = param_info.get("description", "")
|
|
214
|
+
|
|
215
|
+
# Create appropriate widget based on parameter type
|
|
216
|
+
if param_type is int:
|
|
217
|
+
widget = QSpinBox()
|
|
218
|
+
if min_value is not None:
|
|
219
|
+
widget.setMinimum(min_value)
|
|
220
|
+
if max_value is not None:
|
|
221
|
+
widget.setMaximum(max_value)
|
|
222
|
+
if default_value is not None:
|
|
223
|
+
widget.setValue(default_value)
|
|
224
|
+
elif param_type is float:
|
|
225
|
+
widget = QDoubleSpinBox()
|
|
226
|
+
if min_value is not None:
|
|
227
|
+
widget.setMinimum(min_value)
|
|
228
|
+
if max_value is not None:
|
|
229
|
+
widget.setMaximum(max_value)
|
|
230
|
+
widget.setDecimals(3)
|
|
231
|
+
if default_value is not None:
|
|
232
|
+
widget.setValue(default_value)
|
|
233
|
+
else:
|
|
234
|
+
# Default to text input for other types
|
|
235
|
+
widget = QLineEdit(
|
|
236
|
+
str(default_value) if default_value is not None else ""
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Add widget to layout with label
|
|
240
|
+
layout.addRow(f"{param_name} ({description}):", widget)
|
|
241
|
+
self.param_widgets[param_name] = widget
|
|
242
|
+
|
|
243
|
+
def get_parameter_values(self) -> Dict[str, Any]:
|
|
244
|
+
"""
|
|
245
|
+
Get current parameter values from widgets
|
|
246
|
+
"""
|
|
247
|
+
values = {}
|
|
248
|
+
for param_name, widget in self.param_widgets.items():
|
|
249
|
+
param_type = self.parameters[param_name]["type"]
|
|
250
|
+
|
|
251
|
+
if isinstance(widget, (QSpinBox, QDoubleSpinBox)):
|
|
252
|
+
values[param_name] = widget.value()
|
|
253
|
+
else:
|
|
254
|
+
# For text inputs, try to convert to the appropriate type
|
|
255
|
+
try:
|
|
256
|
+
values[param_name] = param_type(widget.text())
|
|
257
|
+
except (ValueError, TypeError):
|
|
258
|
+
# Fall back to string if conversion fails
|
|
259
|
+
values[param_name] = widget.text()
|
|
260
|
+
|
|
261
|
+
return values
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@magicgui(
|
|
265
|
+
call_button="Find and Index Image Files",
|
|
266
|
+
input_folder={"label": "Select Folder"},
|
|
267
|
+
input_suffix={"label": "File Suffix (Example: _labels.tif)", "value": ""},
|
|
268
|
+
)
|
|
269
|
+
def file_selector(
|
|
270
|
+
viewer: napari.Viewer, input_folder: str, input_suffix: str = "_labels.tif"
|
|
271
|
+
) -> List[str]:
|
|
272
|
+
"""
|
|
273
|
+
Find files in a specified input folder with a given suffix and prepare for batch processing.
|
|
274
|
+
"""
|
|
275
|
+
# Validate input_folder
|
|
276
|
+
if not os.path.isdir(input_folder):
|
|
277
|
+
viewer.status = f"Invalid input folder: {input_folder}"
|
|
278
|
+
return []
|
|
279
|
+
|
|
280
|
+
# Find matching files
|
|
281
|
+
matching_files = [
|
|
282
|
+
os.path.join(input_folder, f)
|
|
283
|
+
for f in os.listdir(input_folder)
|
|
284
|
+
if f.endswith(input_suffix)
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
# Create a results widget with batch processing option
|
|
288
|
+
results_widget = FileResultsWidget(
|
|
289
|
+
viewer,
|
|
290
|
+
matching_files,
|
|
291
|
+
input_folder=input_folder,
|
|
292
|
+
input_suffix=input_suffix,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Add the results widget to the Napari viewer
|
|
296
|
+
viewer.window.add_dock_widget(
|
|
297
|
+
results_widget, name="Matching Files", area="right"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Update viewer status
|
|
301
|
+
viewer.status = f"Found {len(matching_files)} files"
|
|
302
|
+
|
|
303
|
+
return matching_files
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class FileResultsWidget(QWidget):
|
|
307
|
+
"""
|
|
308
|
+
Custom widget to display matching files and enable batch processing
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
def __init__(
|
|
312
|
+
self,
|
|
313
|
+
viewer: napari.Viewer,
|
|
314
|
+
file_list: List[str],
|
|
315
|
+
input_folder: str,
|
|
316
|
+
input_suffix: str,
|
|
317
|
+
):
|
|
318
|
+
super().__init__()
|
|
319
|
+
|
|
320
|
+
# Store viewer and file list
|
|
321
|
+
self.viewer = viewer
|
|
322
|
+
self.file_list = file_list
|
|
323
|
+
self.input_folder = input_folder
|
|
324
|
+
self.input_suffix = input_suffix
|
|
325
|
+
|
|
326
|
+
# Load all processing functions
|
|
327
|
+
print("Calling discover_and_load_processing_functions")
|
|
328
|
+
discover_and_load_processing_functions()
|
|
329
|
+
# print what is found by discover_and_load_processing_functions
|
|
330
|
+
print("Available processing functions:")
|
|
331
|
+
for func_name in BatchProcessingRegistry.list_functions():
|
|
332
|
+
print(func_name)
|
|
333
|
+
|
|
334
|
+
# Create main layout
|
|
335
|
+
layout = QVBoxLayout()
|
|
336
|
+
self.setLayout(layout)
|
|
337
|
+
|
|
338
|
+
# Create table of files
|
|
339
|
+
self.table = ProcessedFilesTableWidget(viewer)
|
|
340
|
+
self.table.add_initial_files(file_list)
|
|
341
|
+
|
|
342
|
+
# Add table to layout
|
|
343
|
+
layout.addWidget(self.table)
|
|
344
|
+
|
|
345
|
+
# Create processing function selector
|
|
346
|
+
processing_layout = QVBoxLayout()
|
|
347
|
+
processing_label = QLabel("Select Processing Function:")
|
|
348
|
+
processing_layout.addWidget(processing_label)
|
|
349
|
+
|
|
350
|
+
self.processing_selector = QComboBox()
|
|
351
|
+
self.processing_selector.addItems(
|
|
352
|
+
BatchProcessingRegistry.list_functions()
|
|
353
|
+
)
|
|
354
|
+
processing_layout.addWidget(self.processing_selector)
|
|
355
|
+
|
|
356
|
+
# Add description label
|
|
357
|
+
self.function_description = QLabel("")
|
|
358
|
+
processing_layout.addWidget(self.function_description)
|
|
359
|
+
|
|
360
|
+
# Create parameters section (will be populated when function is selected)
|
|
361
|
+
self.parameters_widget = QWidget()
|
|
362
|
+
processing_layout.addWidget(self.parameters_widget)
|
|
363
|
+
|
|
364
|
+
# Connect function selector to update parameters
|
|
365
|
+
self.processing_selector.currentTextChanged.connect(
|
|
366
|
+
self.update_function_info
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Optional output folder selector
|
|
370
|
+
output_layout = QVBoxLayout()
|
|
371
|
+
output_label = QLabel("Output Folder (optional):")
|
|
372
|
+
output_layout.addWidget(output_label)
|
|
373
|
+
|
|
374
|
+
self.output_folder = QLineEdit()
|
|
375
|
+
self.output_folder.setPlaceholderText(
|
|
376
|
+
"Leave blank to use source folder"
|
|
377
|
+
)
|
|
378
|
+
output_layout.addWidget(self.output_folder)
|
|
379
|
+
|
|
380
|
+
layout.addLayout(processing_layout)
|
|
381
|
+
layout.addLayout(output_layout)
|
|
382
|
+
|
|
383
|
+
# Add batch processing button
|
|
384
|
+
self.batch_button = QPushButton("Start Batch Processing")
|
|
385
|
+
self.batch_button.clicked.connect(self.start_batch_processing)
|
|
386
|
+
layout.addWidget(self.batch_button)
|
|
387
|
+
|
|
388
|
+
# Initialize parameters for the first function
|
|
389
|
+
if self.processing_selector.count() > 0:
|
|
390
|
+
self.update_function_info(self.processing_selector.currentText())
|
|
391
|
+
|
|
392
|
+
def update_function_info(self, function_name: str):
|
|
393
|
+
"""
|
|
394
|
+
Update the function description and parameters when a new function is selected
|
|
395
|
+
"""
|
|
396
|
+
function_info = BatchProcessingRegistry.get_function_info(
|
|
397
|
+
function_name
|
|
398
|
+
)
|
|
399
|
+
if not function_info:
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
# Update description
|
|
403
|
+
description = function_info.get("description", "")
|
|
404
|
+
self.function_description.setText(description)
|
|
405
|
+
|
|
406
|
+
# Update parameters
|
|
407
|
+
parameters = function_info.get("parameters", {})
|
|
408
|
+
|
|
409
|
+
# Remove old parameters widget if it exists
|
|
410
|
+
if hasattr(self, "param_widget_instance"):
|
|
411
|
+
self.parameters_widget.layout().removeWidget(
|
|
412
|
+
self.param_widget_instance
|
|
413
|
+
)
|
|
414
|
+
self.param_widget_instance.deleteLater()
|
|
415
|
+
|
|
416
|
+
# Create new layout if needed
|
|
417
|
+
if self.parameters_widget.layout() is None:
|
|
418
|
+
self.parameters_widget.setLayout(QVBoxLayout())
|
|
419
|
+
|
|
420
|
+
# Create and add new parameters widget
|
|
421
|
+
if parameters:
|
|
422
|
+
self.param_widget_instance = ParameterWidget(parameters)
|
|
423
|
+
self.parameters_widget.layout().addWidget(
|
|
424
|
+
self.param_widget_instance
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
# Create empty widget if no parameters
|
|
428
|
+
self.param_widget_instance = QLabel(
|
|
429
|
+
"No parameters for this function"
|
|
430
|
+
)
|
|
431
|
+
self.parameters_widget.layout().addWidget(
|
|
432
|
+
self.param_widget_instance
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
def start_batch_processing(self):
|
|
436
|
+
"""
|
|
437
|
+
Initiate batch processing of selected files
|
|
438
|
+
"""
|
|
439
|
+
# Get selected processing function
|
|
440
|
+
selected_function_name = self.processing_selector.currentText()
|
|
441
|
+
function_info = BatchProcessingRegistry.get_function_info(
|
|
442
|
+
selected_function_name
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
if not function_info:
|
|
446
|
+
self.viewer.status = "No processing function selected"
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
processing_func = function_info["func"]
|
|
450
|
+
output_suffix = function_info["suffix"]
|
|
451
|
+
|
|
452
|
+
# Get parameter values if available
|
|
453
|
+
param_values = {}
|
|
454
|
+
if hasattr(self, "param_widget_instance") and hasattr(
|
|
455
|
+
self.param_widget_instance, "get_parameter_values"
|
|
456
|
+
):
|
|
457
|
+
param_values = self.param_widget_instance.get_parameter_values()
|
|
458
|
+
|
|
459
|
+
# Determine output folder
|
|
460
|
+
output_folder = self.output_folder.text().strip()
|
|
461
|
+
if not output_folder:
|
|
462
|
+
output_folder = os.path.dirname(self.file_list[0])
|
|
463
|
+
else:
|
|
464
|
+
# make output folder a subfolder of the input folder
|
|
465
|
+
output_folder = os.path.join(self.input_folder, output_folder)
|
|
466
|
+
|
|
467
|
+
# Ensure output folder exists
|
|
468
|
+
os.makedirs(output_folder, exist_ok=True)
|
|
469
|
+
|
|
470
|
+
# Track processed files
|
|
471
|
+
processed_files_info = []
|
|
472
|
+
|
|
473
|
+
# Process each file
|
|
474
|
+
for filepath in self.file_list:
|
|
475
|
+
try:
|
|
476
|
+
# Load the image
|
|
477
|
+
image = tifffile.imread(filepath)
|
|
478
|
+
|
|
479
|
+
# Apply processing with parameters
|
|
480
|
+
processed_image = processing_func(image, **param_values)
|
|
481
|
+
|
|
482
|
+
# Generate new filename
|
|
483
|
+
filename = os.path.basename(filepath)
|
|
484
|
+
name, ext = os.path.splitext(filename)
|
|
485
|
+
new_filename = (
|
|
486
|
+
name.replace(self.input_suffix, "") + output_suffix + ext
|
|
487
|
+
)
|
|
488
|
+
new_filepath = os.path.join(output_folder, new_filename)
|
|
489
|
+
|
|
490
|
+
# Save processed image
|
|
491
|
+
tifffile.imwrite(new_filepath, processed_image)
|
|
492
|
+
|
|
493
|
+
# Track processed file
|
|
494
|
+
processed_files_info.append(
|
|
495
|
+
{"original_file": filepath, "processed_file": new_filepath}
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
except (
|
|
499
|
+
ValueError,
|
|
500
|
+
TypeError,
|
|
501
|
+
OSError,
|
|
502
|
+
tifffile.TiffFileError,
|
|
503
|
+
) as e:
|
|
504
|
+
self.viewer.status = f"Error processing {filepath}: {e}"
|
|
505
|
+
print(f"Error processing {filepath}: {e}")
|
|
506
|
+
|
|
507
|
+
# Update table with processed files
|
|
508
|
+
self.table.update_processed_files(processed_files_info)
|
|
509
|
+
|
|
510
|
+
# Update viewer status
|
|
511
|
+
self.viewer.status = f"Processed {len(processed_files_info)} files with {selected_function_name}"
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def napari_experimental_provide_dock_widget():
|
|
515
|
+
"""
|
|
516
|
+
Provide the file selector widget to Napari
|
|
517
|
+
"""
|
|
518
|
+
return file_selector
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from magicgui import magicgui
|
|
5
|
+
from napari.layers import Labels
|
|
6
|
+
from napari.viewer import Viewer
|
|
7
|
+
from skimage.io import imread, imsave
|
|
8
|
+
|
|
9
|
+
sys.path.append("src/napari_tmidas")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LabelInspector:
|
|
13
|
+
def __init__(self, viewer: Viewer):
|
|
14
|
+
self.viewer = viewer
|
|
15
|
+
self.image_label_pairs = []
|
|
16
|
+
self.current_index = 0
|
|
17
|
+
|
|
18
|
+
def load_image_label_pairs(
|
|
19
|
+
self, folder_path: str, image_suffix: str, label_suffix: str
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Load image-label pairs from a folder.
|
|
23
|
+
"""
|
|
24
|
+
files = os.listdir(folder_path)
|
|
25
|
+
image_files = [file for file in files if file.endswith(image_suffix)]
|
|
26
|
+
label_files = [file for file in files if file.endswith(label_suffix)]
|
|
27
|
+
|
|
28
|
+
# Modified matching logic
|
|
29
|
+
self.image_label_pairs = []
|
|
30
|
+
for img in image_files:
|
|
31
|
+
img_base = img[: -len(image_suffix)] # Remove image suffix
|
|
32
|
+
for lbl in label_files:
|
|
33
|
+
if lbl.startswith(
|
|
34
|
+
img_base
|
|
35
|
+
): # Check if label starts with image base
|
|
36
|
+
self.image_label_pairs.append(
|
|
37
|
+
(
|
|
38
|
+
os.path.join(folder_path, img),
|
|
39
|
+
os.path.join(folder_path, lbl),
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
break # Match found, move to next image
|
|
43
|
+
|
|
44
|
+
if not self.image_label_pairs:
|
|
45
|
+
self.viewer.status = "No matching image-label pairs found."
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
self.viewer.status = (
|
|
49
|
+
f"Found {len(self.image_label_pairs)} image-label pairs."
|
|
50
|
+
)
|
|
51
|
+
self.current_index = 0
|
|
52
|
+
self._load_current_pair()
|
|
53
|
+
|
|
54
|
+
def _load_current_pair(self):
|
|
55
|
+
"""
|
|
56
|
+
Load the current image-label pair into the Napari viewer.
|
|
57
|
+
"""
|
|
58
|
+
if not self.image_label_pairs:
|
|
59
|
+
self.viewer.status = "No pairs to inspect."
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
image_path, label_path = self.image_label_pairs[self.current_index]
|
|
63
|
+
image = imread(image_path)
|
|
64
|
+
label_image = imread(label_path)
|
|
65
|
+
|
|
66
|
+
# Clear existing layers
|
|
67
|
+
self.viewer.layers.clear()
|
|
68
|
+
|
|
69
|
+
# Add the new layers
|
|
70
|
+
self.viewer.add_image(
|
|
71
|
+
image, name=f"Image ({os.path.basename(image_path)})"
|
|
72
|
+
)
|
|
73
|
+
self.viewer.add_labels(
|
|
74
|
+
label_image, name=f"Labels ({os.path.basename(label_path)})"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def save_current_labels(self):
|
|
78
|
+
"""
|
|
79
|
+
Save the current labels back to the original file.
|
|
80
|
+
"""
|
|
81
|
+
if not self.image_label_pairs:
|
|
82
|
+
self.viewer.status = "No pairs to save."
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
_, label_path = self.image_label_pairs[self.current_index]
|
|
86
|
+
|
|
87
|
+
# Find the labels layer in the viewer
|
|
88
|
+
labels_layer = next(
|
|
89
|
+
(
|
|
90
|
+
layer
|
|
91
|
+
for layer in self.viewer.layers
|
|
92
|
+
if isinstance(layer, Labels)
|
|
93
|
+
),
|
|
94
|
+
None,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if labels_layer is None:
|
|
98
|
+
self.viewer.status = "No labels found."
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# Save the labels layer data to the original file path
|
|
102
|
+
imsave(label_path, labels_layer.data.astype("uint16"))
|
|
103
|
+
self.viewer.status = f"Saved labels to {label_path}."
|
|
104
|
+
|
|
105
|
+
def next_pair(self):
|
|
106
|
+
"""
|
|
107
|
+
Save changes and proceed to the next image-label pair.
|
|
108
|
+
"""
|
|
109
|
+
if not self.image_label_pairs:
|
|
110
|
+
self.viewer.status = "No pairs to inspect."
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# Save current labels before proceeding
|
|
114
|
+
self.save_current_labels()
|
|
115
|
+
|
|
116
|
+
# Check if we're already at the last pair
|
|
117
|
+
if self.current_index >= len(self.image_label_pairs) - 1:
|
|
118
|
+
self.viewer.status = "No more pairs to inspect."
|
|
119
|
+
# should also clear the viewer
|
|
120
|
+
self.viewer.layers.clear()
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# Move to the next pair
|
|
124
|
+
self.current_index += 1
|
|
125
|
+
|
|
126
|
+
# Load the next pair
|
|
127
|
+
self._load_current_pair()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@magicgui(
|
|
131
|
+
call_button="Start Label Inspection",
|
|
132
|
+
folder_path={"label": "Folder Path"},
|
|
133
|
+
image_suffix={"label": "Image Suffix (e.g., .tif)"},
|
|
134
|
+
label_suffix={"label": "Label Suffix (e.g., _labels.tif)"},
|
|
135
|
+
)
|
|
136
|
+
def label_inspector(
|
|
137
|
+
folder_path: str,
|
|
138
|
+
image_suffix: str,
|
|
139
|
+
label_suffix: str,
|
|
140
|
+
viewer: Viewer,
|
|
141
|
+
):
|
|
142
|
+
"""
|
|
143
|
+
MagicGUI widget for starting label inspection.
|
|
144
|
+
"""
|
|
145
|
+
inspector = LabelInspector(viewer)
|
|
146
|
+
inspector.load_image_label_pairs(folder_path, image_suffix, label_suffix)
|
|
147
|
+
|
|
148
|
+
# Add buttons for saving and continuing to the next pair
|
|
149
|
+
@magicgui(call_button="Save Changes and Continue")
|
|
150
|
+
def save_and_continue():
|
|
151
|
+
inspector.next_pair()
|
|
152
|
+
|
|
153
|
+
viewer.window.add_dock_widget(save_and_continue)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def label_inspector_widget():
|
|
157
|
+
"""
|
|
158
|
+
Provide the label inspector widget to Napari
|
|
159
|
+
"""
|
|
160
|
+
return label_inspector
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# napari_tmidas/_registry.py
|
|
2
|
+
"""
|
|
3
|
+
Registry for batch processing functions.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BatchProcessingRegistry:
|
|
9
|
+
"""
|
|
10
|
+
A registry to manage and track available processing functions with parameter support
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
_processing_functions = {}
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def register(
|
|
17
|
+
cls,
|
|
18
|
+
name: str,
|
|
19
|
+
suffix: str = "_processed",
|
|
20
|
+
description: str = "",
|
|
21
|
+
parameters: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Decorator to register processing functions
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
name: Name of the processing function
|
|
28
|
+
suffix: Suffix to append to processed files
|
|
29
|
+
description: Description of what the function does
|
|
30
|
+
parameters: Dictionary of parameters with their metadata
|
|
31
|
+
{
|
|
32
|
+
"param_name": {
|
|
33
|
+
"type": type,
|
|
34
|
+
"default": default_value,
|
|
35
|
+
"min": min_value, # optional, for numeric types
|
|
36
|
+
"max": max_value, # optional, for numeric types
|
|
37
|
+
"description": "Parameter description"
|
|
38
|
+
},
|
|
39
|
+
...
|
|
40
|
+
}
|
|
41
|
+
"""
|
|
42
|
+
if parameters is None:
|
|
43
|
+
parameters = {}
|
|
44
|
+
|
|
45
|
+
def decorator(func):
|
|
46
|
+
cls._processing_functions[name] = {
|
|
47
|
+
"func": func,
|
|
48
|
+
"suffix": suffix,
|
|
49
|
+
"description": description,
|
|
50
|
+
"parameters": parameters,
|
|
51
|
+
}
|
|
52
|
+
return func
|
|
53
|
+
|
|
54
|
+
return decorator
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def get_function_info(cls, name: str) -> Optional[dict]:
|
|
58
|
+
"""
|
|
59
|
+
Retrieve a registered processing function and its metadata
|
|
60
|
+
"""
|
|
61
|
+
return cls._processing_functions.get(name)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def list_functions(cls) -> List[str]:
|
|
65
|
+
"""
|
|
66
|
+
List all registered processing function names
|
|
67
|
+
"""
|
|
68
|
+
return list(cls._processing_functions.keys())
|
napari_tmidas/_version.py
CHANGED
napari_tmidas/napari.yaml
CHANGED
|
@@ -18,18 +18,12 @@ contributions:
|
|
|
18
18
|
- id: napari-tmidas.make_sample_data
|
|
19
19
|
python_name: napari_tmidas._sample_data:make_sample_data
|
|
20
20
|
title: Load sample data from T-MIDAS
|
|
21
|
-
- id: napari-tmidas.
|
|
22
|
-
python_name: napari_tmidas:
|
|
23
|
-
title:
|
|
24
|
-
- id: napari-tmidas.
|
|
25
|
-
python_name: napari_tmidas:
|
|
26
|
-
title:
|
|
27
|
-
- id: napari-tmidas.make_function_widget
|
|
28
|
-
python_name: napari_tmidas:threshold_autogenerate_widget
|
|
29
|
-
title: Make threshold function widget
|
|
30
|
-
- id: napari-tmidas.make_qwidget
|
|
31
|
-
python_name: napari_tmidas:ExampleQWidget
|
|
32
|
-
title: Make example QWidget
|
|
21
|
+
- id: napari-tmidas._label_inspection # hyphen!
|
|
22
|
+
python_name: napari_tmidas._label_inspection:label_inspector_widget # underscore!
|
|
23
|
+
title: Label inspector
|
|
24
|
+
- id: napari-tmidas.file_selector
|
|
25
|
+
python_name: napari_tmidas._file_selector:napari_experimental_provide_dock_widget
|
|
26
|
+
title: File selector
|
|
33
27
|
readers:
|
|
34
28
|
- command: napari-tmidas.get_reader
|
|
35
29
|
accepts_directories: false
|
|
@@ -46,12 +40,7 @@ contributions:
|
|
|
46
40
|
display_name: T-MIDAS
|
|
47
41
|
key: unique_id.1
|
|
48
42
|
widgets:
|
|
49
|
-
- command: napari-tmidas.
|
|
50
|
-
display_name:
|
|
51
|
-
- command: napari-tmidas.
|
|
52
|
-
display_name:
|
|
53
|
-
- command: napari-tmidas.make_function_widget
|
|
54
|
-
autogenerate: true
|
|
55
|
-
display_name: Autogenerate Threshold
|
|
56
|
-
- command: napari-tmidas.make_qwidget
|
|
57
|
-
display_name: Example QWidget
|
|
43
|
+
- command: napari-tmidas.file_selector
|
|
44
|
+
display_name: File selector
|
|
45
|
+
- command: napari-tmidas._label_inspection
|
|
46
|
+
display_name: Label inspector
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# processing_functions/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Package for processing functions that can be registered with the batch processing system.
|
|
4
|
+
"""
|
|
5
|
+
import importlib
|
|
6
|
+
import os
|
|
7
|
+
import pkgutil
|
|
8
|
+
from typing import Dict, List
|
|
9
|
+
|
|
10
|
+
# Keep the registry global
|
|
11
|
+
from napari_tmidas._registry import BatchProcessingRegistry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def discover_and_load_processing_functions() -> List[str]:
|
|
15
|
+
"""
|
|
16
|
+
Discover and load all processing functions from the processing_functions package.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
List of registered function names
|
|
20
|
+
"""
|
|
21
|
+
# Get the current package
|
|
22
|
+
package = __name__
|
|
23
|
+
|
|
24
|
+
# Find all modules in the package
|
|
25
|
+
for _, module_name, is_pkg in pkgutil.iter_modules(
|
|
26
|
+
[os.path.dirname(__file__)]
|
|
27
|
+
):
|
|
28
|
+
if not is_pkg: # Only load non-package modules
|
|
29
|
+
try:
|
|
30
|
+
# Import the module
|
|
31
|
+
importlib.import_module(f"{package}.{module_name}")
|
|
32
|
+
print(f"Loaded processing function module: {module_name}")
|
|
33
|
+
except ImportError as e:
|
|
34
|
+
# Log the error but continue with other modules
|
|
35
|
+
print(f"Failed to import {module_name}: {e}")
|
|
36
|
+
|
|
37
|
+
# Return the list of registered functions
|
|
38
|
+
return BatchProcessingRegistry.list_functions()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_processing_function_info() -> Dict[str, Dict]:
|
|
42
|
+
"""
|
|
43
|
+
Get information about all registered processing functions.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Dictionary of function information
|
|
47
|
+
"""
|
|
48
|
+
return {
|
|
49
|
+
name: {
|
|
50
|
+
"description": BatchProcessingRegistry.get_function_info(name).get(
|
|
51
|
+
"description", ""
|
|
52
|
+
),
|
|
53
|
+
"suffix": BatchProcessingRegistry.get_function_info(name).get(
|
|
54
|
+
"suffix", ""
|
|
55
|
+
),
|
|
56
|
+
"parameters": BatchProcessingRegistry.get_function_info(name).get(
|
|
57
|
+
"parameters", {}
|
|
58
|
+
),
|
|
59
|
+
}
|
|
60
|
+
for name in BatchProcessingRegistry.list_functions()
|
|
61
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# processing_functions/basic.py
|
|
2
|
+
"""
|
|
3
|
+
Basic image processing functions that don't require additional dependencies.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from napari_tmidas._registry import BatchProcessingRegistry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@BatchProcessingRegistry.register(
|
|
11
|
+
name="Min-Max Normalization",
|
|
12
|
+
suffix="_normalized",
|
|
13
|
+
description="Normalize image values to range [0, 1] using min-max scaling",
|
|
14
|
+
)
|
|
15
|
+
def normalize_image(image: np.ndarray) -> np.ndarray:
|
|
16
|
+
"""
|
|
17
|
+
Simple min-max normalization
|
|
18
|
+
"""
|
|
19
|
+
if image.min() == image.max():
|
|
20
|
+
return np.zeros_like(image, dtype=float)
|
|
21
|
+
return (image - image.min()) / (image.max() - image.min())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@BatchProcessingRegistry.register(
|
|
25
|
+
name="Contrast Stretch",
|
|
26
|
+
suffix="_contrast",
|
|
27
|
+
description="Stretch the contrast by clipping percentiles and rescaling",
|
|
28
|
+
parameters={
|
|
29
|
+
"low_percentile": {
|
|
30
|
+
"type": float,
|
|
31
|
+
"default": 2.0,
|
|
32
|
+
"min": 0.0,
|
|
33
|
+
"max": 49.0,
|
|
34
|
+
"description": "Low percentile to clip",
|
|
35
|
+
},
|
|
36
|
+
"high_percentile": {
|
|
37
|
+
"type": float,
|
|
38
|
+
"default": 98.0,
|
|
39
|
+
"min": 51.0,
|
|
40
|
+
"max": 100.0,
|
|
41
|
+
"description": "High percentile to clip",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
def contrast_stretch(
|
|
46
|
+
image: np.ndarray,
|
|
47
|
+
low_percentile: float = 2.0,
|
|
48
|
+
high_percentile: float = 98.0,
|
|
49
|
+
) -> np.ndarray:
|
|
50
|
+
"""
|
|
51
|
+
Stretch contrast by clipping percentiles
|
|
52
|
+
"""
|
|
53
|
+
p_low = np.percentile(image, low_percentile)
|
|
54
|
+
p_high = np.percentile(image, high_percentile)
|
|
55
|
+
|
|
56
|
+
# Clip and normalize
|
|
57
|
+
image_clipped = np.clip(image, p_low, p_high)
|
|
58
|
+
if p_high == p_low:
|
|
59
|
+
return np.zeros_like(image, dtype=float)
|
|
60
|
+
return (image_clipped - p_low) / (p_high - p_low)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# processing_functions/scipy_filters.py
|
|
2
|
+
"""
|
|
3
|
+
Processing functions that depend on SciPy.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from scipy import ndimage
|
|
9
|
+
|
|
10
|
+
SCIPY_AVAILABLE = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
SCIPY_AVAILABLE = False
|
|
13
|
+
print("SciPy not available, some processing functions will be disabled")
|
|
14
|
+
|
|
15
|
+
from napari_tmidas._registry import BatchProcessingRegistry
|
|
16
|
+
|
|
17
|
+
if SCIPY_AVAILABLE:
|
|
18
|
+
|
|
19
|
+
@BatchProcessingRegistry.register(
|
|
20
|
+
name="Gaussian Blur",
|
|
21
|
+
suffix="_blurred",
|
|
22
|
+
description="Apply Gaussian blur to the image",
|
|
23
|
+
parameters={
|
|
24
|
+
"sigma": {
|
|
25
|
+
"type": float,
|
|
26
|
+
"default": 1.0,
|
|
27
|
+
"min": 0.1,
|
|
28
|
+
"max": 10.0,
|
|
29
|
+
"description": "Standard deviation for Gaussian kernel",
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
def gaussian_blur(image: np.ndarray, sigma: float = 1.0) -> np.ndarray:
|
|
34
|
+
"""
|
|
35
|
+
Apply Gaussian blur to the image
|
|
36
|
+
"""
|
|
37
|
+
return ndimage.gaussian_filter(image, sigma=sigma)
|
|
38
|
+
|
|
39
|
+
@BatchProcessingRegistry.register(
|
|
40
|
+
name="Median Filter",
|
|
41
|
+
suffix="_median",
|
|
42
|
+
description="Apply median filter for noise reduction",
|
|
43
|
+
parameters={
|
|
44
|
+
"size": {
|
|
45
|
+
"type": int,
|
|
46
|
+
"default": 3,
|
|
47
|
+
"min": 3,
|
|
48
|
+
"max": 15,
|
|
49
|
+
"description": "Size of the median filter window",
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
def median_filter(image: np.ndarray, size: int = 3) -> np.ndarray:
|
|
54
|
+
"""
|
|
55
|
+
Apply median filter for noise reduction
|
|
56
|
+
"""
|
|
57
|
+
return ndimage.median_filter(image, size=size)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# processing_functions/skimage_filters.py
|
|
2
|
+
"""
|
|
3
|
+
Processing functions that depend on scikit-image.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import skimage.exposure
|
|
9
|
+
import skimage.filters
|
|
10
|
+
|
|
11
|
+
SKIMAGE_AVAILABLE = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
SKIMAGE_AVAILABLE = False
|
|
14
|
+
print(
|
|
15
|
+
"scikit-image not available, some processing functions will be disabled"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from napari_tmidas._registry import BatchProcessingRegistry
|
|
19
|
+
|
|
20
|
+
if SKIMAGE_AVAILABLE:
|
|
21
|
+
|
|
22
|
+
@BatchProcessingRegistry.register(
|
|
23
|
+
name="Adaptive Histogram Equalization",
|
|
24
|
+
suffix="_clahe",
|
|
25
|
+
description="Enhance contrast using Contrast Limited Adaptive Histogram Equalization",
|
|
26
|
+
parameters={
|
|
27
|
+
"kernel_size": {
|
|
28
|
+
"type": int,
|
|
29
|
+
"default": 8,
|
|
30
|
+
"min": 4,
|
|
31
|
+
"max": 64,
|
|
32
|
+
"description": "Size of local region for histogram equalization",
|
|
33
|
+
},
|
|
34
|
+
"clip_limit": {
|
|
35
|
+
"type": float,
|
|
36
|
+
"default": 0.01,
|
|
37
|
+
"min": 0.001,
|
|
38
|
+
"max": 0.1,
|
|
39
|
+
"description": "Clipping limit for contrast enhancement",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
def adaptive_hist_eq(
|
|
44
|
+
image: np.ndarray, kernel_size: int = 8, clip_limit: float = 0.01
|
|
45
|
+
) -> np.ndarray:
|
|
46
|
+
"""
|
|
47
|
+
Apply Contrast Limited Adaptive Histogram Equalization
|
|
48
|
+
"""
|
|
49
|
+
# CLAHE expects image in [0, 1] range
|
|
50
|
+
img_norm = skimage.exposure.rescale_intensity(image, out_range=(0, 1))
|
|
51
|
+
return skimage.exposure.equalize_adapthist(
|
|
52
|
+
img_norm, kernel_size=kernel_size, clip_limit=clip_limit
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@BatchProcessingRegistry.register(
|
|
56
|
+
name="Edge Detection",
|
|
57
|
+
suffix="_edges",
|
|
58
|
+
description="Detect edges using Sobel filter",
|
|
59
|
+
)
|
|
60
|
+
def edge_detection(image: np.ndarray) -> np.ndarray:
|
|
61
|
+
"""
|
|
62
|
+
Detect edges using Sobel filter
|
|
63
|
+
"""
|
|
64
|
+
return skimage.filters.sobel(image)
|
|
65
|
+
|
|
66
|
+
# simple otsu thresholding
|
|
67
|
+
@BatchProcessingRegistry.register(
|
|
68
|
+
name="Otsu Thresholding (semantic)",
|
|
69
|
+
suffix="_otsu_semantic",
|
|
70
|
+
description="Threshold image using Otsu's method",
|
|
71
|
+
)
|
|
72
|
+
def otsu_thresholding(image: np.ndarray) -> np.ndarray:
|
|
73
|
+
"""
|
|
74
|
+
Threshold image using Otsu's method
|
|
75
|
+
"""
|
|
76
|
+
thresh = skimage.filters.threshold_otsu(image)
|
|
77
|
+
return (image > thresh).astype(np.uint32)
|
|
78
|
+
|
|
79
|
+
# instance segmentation
|
|
80
|
+
@BatchProcessingRegistry.register(
|
|
81
|
+
name="Otsu Thresholding (instance)",
|
|
82
|
+
suffix="_otsu_labels",
|
|
83
|
+
description="Threshold image using Otsu's method",
|
|
84
|
+
)
|
|
85
|
+
def otsu_thresholding_instance(image: np.ndarray) -> np.ndarray:
|
|
86
|
+
"""
|
|
87
|
+
Threshold image using Otsu's method
|
|
88
|
+
"""
|
|
89
|
+
thresh = skimage.filters.threshold_otsu(image)
|
|
90
|
+
return skimage.measure.label(image > thresh).astype(np.uint32)
|
|
91
|
+
|
|
92
|
+
# simple thresholding
|
|
93
|
+
@BatchProcessingRegistry.register(
|
|
94
|
+
name="Manual Thresholding (8-bit)",
|
|
95
|
+
suffix="_thresh",
|
|
96
|
+
description="Threshold image using a fixed threshold",
|
|
97
|
+
parameters={
|
|
98
|
+
"threshold": {
|
|
99
|
+
"type": int,
|
|
100
|
+
"default": 128,
|
|
101
|
+
"min": 0,
|
|
102
|
+
"max": 255,
|
|
103
|
+
"description": "Threshold value",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
def simple_thresholding(
|
|
108
|
+
image: np.ndarray, threshold: int = 128
|
|
109
|
+
) -> np.ndarray:
|
|
110
|
+
"""
|
|
111
|
+
Threshold image using a fixed threshold
|
|
112
|
+
"""
|
|
113
|
+
return image > threshold
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: napari-tmidas
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Tissue Microscopy Image Data Analysis Suite
|
|
5
5
|
Author: Marco Meer
|
|
6
6
|
Author-email: marco.meer@pm.me
|
|
@@ -57,6 +57,7 @@ Requires-Dist: numpy
|
|
|
57
57
|
Requires-Dist: magicgui
|
|
58
58
|
Requires-Dist: qtpy
|
|
59
59
|
Requires-Dist: scikit-image
|
|
60
|
+
Requires-Dist: pyqt5
|
|
60
61
|
Provides-Extra: testing
|
|
61
62
|
Requires-Dist: tox; extra == "testing"
|
|
62
63
|
Requires-Dist: pytest; extra == "testing"
|
|
@@ -71,34 +72,54 @@ Requires-Dist: pyqt5; extra == "testing"
|
|
|
71
72
|
[](https://pypi.org/project/napari-tmidas)
|
|
72
73
|
[](https://python.org)
|
|
73
74
|
[](https://github.com/macromeer/napari-tmidas/actions)
|
|
74
|
-
[](https://codecov.io/gh/macromeer/napari-tmidas)
|
|
75
75
|
[](https://napari-hub.org/plugins/napari-tmidas)
|
|
76
|
+
<!-- [](https://codecov.io/gh/macromeer/napari-tmidas) -->
|
|
76
77
|
|
|
77
|
-
Tissue Microscopy Image Data Analysis Suite
|
|
78
|
+
The Tissue Microscopy Image Data Analysis Suite (short: T-MIDAS), is a collection of pipelines for batch image preprocessing, segmentation, regions-of-interest (ROI) analysis and other useful features. This is a work in progress (WIP) and an evolutionary step away from the [terminal / command-line version of T-MIDAS](https://github.com/MercaderLabAnatomy/T-MIDAS).
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
## Installation
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
First install Napari in a virtual environment following the latest [Napari installation instructions](https://github.com/Napari/napari?tab=readme-ov-file#installation).
|
|
82
83
|
|
|
83
|
-
<!--
|
|
84
|
-
Don't miss the full getting started guide to set up your new package:
|
|
85
|
-
https://github.com/napari/napari-plugin-template#getting-started
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
https://napari.org/stable/plugins/index.html
|
|
89
|
-
-->
|
|
85
|
+
After you have activated the environment, you can install `napari-tmidas` via [pip]:
|
|
90
86
|
|
|
91
|
-
|
|
87
|
+
pip install napari-tmidas
|
|
92
88
|
|
|
93
|
-
|
|
89
|
+
To install the latest development version:
|
|
94
90
|
|
|
95
|
-
pip install napari-tmidas
|
|
91
|
+
pip install git+https://github.com/macromeer/napari-tmidas.git
|
|
96
92
|
|
|
93
|
+
## Usage
|
|
97
94
|
|
|
95
|
+
You can find the installed plugin here:
|
|
96
|
+
|
|
97
|
+

|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
### File inspector
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
1. After opening `Plugins > T-MIDAS > File selector`, enter the path to the folder containing the images to be processed (currently supports TIF, later also ZARR). You can also filter for filename suffix.
|
|
102
|
+
|
|
103
|
+

|
|
104
|
+
|
|
105
|
+
2. As a result, a table appears with the found images.
|
|
106
|
+
|
|
107
|
+

|
|
108
|
+
|
|
109
|
+
3. Next, select a processing function, set parameters if applicable and `Start Batch Processing`.
|
|
110
|
+
|
|
111
|
+

|
|
112
|
+
|
|
113
|
+
4. You can click on the images in the table to show them in the viewer. For example first click on one of the `Original Files`, and then the corresponding `Processed File` to see an overlay.
|
|
114
|
+
|
|
115
|
+

|
|
116
|
+
|
|
117
|
+
Note that whenever you click on an `Original File` or `Processed File` in the table, it will replace the one that is currently shown in the viewer. So naturally, you'd first select the original image, and then the processed image to correctly see the image pair that you want to inspect.
|
|
118
|
+
|
|
119
|
+
### Label inspector
|
|
120
|
+
If you have already segmented a folder full of images and now you want to maybe inspect and edit each label image, you can use the `Plugins > T-MIDAS > Label inspector`, which automatically saves your changes to the existing label image once you click the `Save Changes and Continue` button (bottom right).
|
|
121
|
+
|
|
122
|
+

|
|
102
123
|
|
|
103
124
|
|
|
104
125
|
## Contributing
|
|
@@ -128,6 +149,18 @@ If you encounter any problems, please [file an issue] along with a detailed desc
|
|
|
128
149
|
|
|
129
150
|
[file an issue]: https://github.com/macromeer/napari-tmidas/issues
|
|
130
151
|
|
|
152
|
+
----------------------------------
|
|
153
|
+
|
|
154
|
+
This [napari] plugin was generated with [copier] using the [napari-plugin-template].
|
|
155
|
+
|
|
156
|
+
<!--
|
|
157
|
+
Don't miss the full getting started guide to set up your new package:
|
|
158
|
+
https://github.com/napari/napari-plugin-template#getting-started
|
|
159
|
+
|
|
160
|
+
and review the napari docs for plugin developers:
|
|
161
|
+
https://napari.org/stable/plugins/index.html
|
|
162
|
+
-->
|
|
163
|
+
|
|
131
164
|
[napari]: https://github.com/napari/napari
|
|
132
165
|
[tox]: https://tox.readthedocs.io/en/latest/
|
|
133
166
|
[pip]: https://pypi.org/project/pip/
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
napari_tmidas/__init__.py,sha256=Z9mznblUlUsRyH3d4k8SxUo4iXLMwJXURbq41QzhPpo,459
|
|
2
|
+
napari_tmidas/_file_selector.py,sha256=YzjS-XIqLD8826n50KXAc0GfQeXOhvDs41QP3-bTtCU,17471
|
|
3
|
+
napari_tmidas/_label_inspection.py,sha256=0icowMfyNRnaxgTIOi8KHxLXKfpfqtShIl8iVR4wJjc,4819
|
|
4
|
+
napari_tmidas/_reader.py,sha256=A9_hdDxtVkVGmbOsbqgnARCSvpEh7GGPo7ylzmbnu8o,2485
|
|
5
|
+
napari_tmidas/_registry.py,sha256=Oz9HFJh41MKRLeKxRuc7x7yzc-OrmoTdRFnfngFU_XE,2007
|
|
6
|
+
napari_tmidas/_sample_data.py,sha256=khuv1jemz_fCjqNwEKMFf83Ju0EN4S89IKydsUMmUxw,645
|
|
7
|
+
napari_tmidas/_version.py,sha256=NIzzV8ZM0W-CSLuEs1weG4zPrn_-8yr1AwwI1iuS6yo,511
|
|
8
|
+
napari_tmidas/_widget.py,sha256=u9uf9WILAwZg_InhFyjWInY4ej1TV1a59dR8Fe3vNF8,4794
|
|
9
|
+
napari_tmidas/_writer.py,sha256=wbVfHFjjHdybSg37VR4lVmL-kdCkDZsUPDJ66AVLaFQ,1941
|
|
10
|
+
napari_tmidas/napari.yaml,sha256=Xmui3_7pxNxOkIFRWsZWuka56d6PXLQ2rl4XvMDl2aw,1839
|
|
11
|
+
napari_tmidas/_tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
napari_tmidas/_tests/test_reader.py,sha256=gN_2StATLZYUL56X27ImJTVru_qSoFiY4vtgajcx3H0,975
|
|
13
|
+
napari_tmidas/_tests/test_sample_data.py,sha256=D1HU_C3hWpO3mlSW_7Z94xaYHDtxz0XUrMjQoYop9Ag,104
|
|
14
|
+
napari_tmidas/_tests/test_widget.py,sha256=I_d-Cra_CTcS0QdMItg_HMphvhj0XCx81JnFyCHk9lg,2204
|
|
15
|
+
napari_tmidas/_tests/test_writer.py,sha256=4_MlZM9a5So74J16_4tIOJc6pwTOw9R0-oAE_YioIx4,122
|
|
16
|
+
napari_tmidas/processing_functions/__init__.py,sha256=osXY9jSgDsrwFaS6ShPHP0wGRxMuX1mHRN9EDa9l41g,1891
|
|
17
|
+
napari_tmidas/processing_functions/basic.py,sha256=g7tQ25UIxA26n6GBYcHlkSjUbv_lD-7x_Sd-ZvWbzUY,1711
|
|
18
|
+
napari_tmidas/processing_functions/scipy_filters.py,sha256=kKpDAlQQ0ZNbkt77QUWi-Bwolk6MMDvtG_bZJV3MjOo,1612
|
|
19
|
+
napari_tmidas/processing_functions/skimage_filters.py,sha256=RpBywSImAQc_L_0pysA3yAJlHHfZuqu6c3vokDv5p1I,3517
|
|
20
|
+
napari_tmidas-0.1.3.dist-info/LICENSE,sha256=tSjiOqj57exmEIfP2YVPCEeQf0cH49S6HheQR8IiY3g,1485
|
|
21
|
+
napari_tmidas-0.1.3.dist-info/METADATA,sha256=bXMq78n-Y-vWxw1W3zC9PHkG2-4KvIqxoUDXSNV72Yk,8222
|
|
22
|
+
napari_tmidas-0.1.3.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
23
|
+
napari_tmidas-0.1.3.dist-info/entry_points.txt,sha256=fbVjzbJTm4aDMIBtel1Lyqvq-CwXY7wmCOo_zJ-jtRY,60
|
|
24
|
+
napari_tmidas-0.1.3.dist-info/top_level.txt,sha256=63ybdxCZ4SeT13f_Ou4TsivGV_2Gtm_pJOXToAt30_E,14
|
|
25
|
+
napari_tmidas-0.1.3.dist-info/RECORD,,
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
napari_tmidas/__init__.py,sha256=cVpVT4zJ5I9N6YFige0iySIMW3JzE35KKnEy-PFpFx0,592
|
|
2
|
-
napari_tmidas/_reader.py,sha256=A9_hdDxtVkVGmbOsbqgnARCSvpEh7GGPo7ylzmbnu8o,2485
|
|
3
|
-
napari_tmidas/_sample_data.py,sha256=khuv1jemz_fCjqNwEKMFf83Ju0EN4S89IKydsUMmUxw,645
|
|
4
|
-
napari_tmidas/_version.py,sha256=Mmxse1R0ki5tjz9qzU8AQyqUsLt8nTyCAbYQp8R87PU,511
|
|
5
|
-
napari_tmidas/_widget.py,sha256=u9uf9WILAwZg_InhFyjWInY4ej1TV1a59dR8Fe3vNF8,4794
|
|
6
|
-
napari_tmidas/_writer.py,sha256=wbVfHFjjHdybSg37VR4lVmL-kdCkDZsUPDJ66AVLaFQ,1941
|
|
7
|
-
napari_tmidas/napari.yaml,sha256=XRz2siVLEciSqUHv1tlLjX9BB0Sc1jBRB3KyAN2ISgA,2276
|
|
8
|
-
napari_tmidas/_tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
napari_tmidas/_tests/test_reader.py,sha256=gN_2StATLZYUL56X27ImJTVru_qSoFiY4vtgajcx3H0,975
|
|
10
|
-
napari_tmidas/_tests/test_sample_data.py,sha256=D1HU_C3hWpO3mlSW_7Z94xaYHDtxz0XUrMjQoYop9Ag,104
|
|
11
|
-
napari_tmidas/_tests/test_widget.py,sha256=I_d-Cra_CTcS0QdMItg_HMphvhj0XCx81JnFyCHk9lg,2204
|
|
12
|
-
napari_tmidas/_tests/test_writer.py,sha256=4_MlZM9a5So74J16_4tIOJc6pwTOw9R0-oAE_YioIx4,122
|
|
13
|
-
napari_tmidas-0.1.1.dist-info/LICENSE,sha256=tSjiOqj57exmEIfP2YVPCEeQf0cH49S6HheQR8IiY3g,1485
|
|
14
|
-
napari_tmidas-0.1.1.dist-info/METADATA,sha256=TuiFctic_XaF8TxYPQYuLJOlVTKiF_Qxl0Lh6SiYBb4,5850
|
|
15
|
-
napari_tmidas-0.1.1.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
16
|
-
napari_tmidas-0.1.1.dist-info/entry_points.txt,sha256=fbVjzbJTm4aDMIBtel1Lyqvq-CwXY7wmCOo_zJ-jtRY,60
|
|
17
|
-
napari_tmidas-0.1.1.dist-info/top_level.txt,sha256=63ybdxCZ4SeT13f_Ou4TsivGV_2Gtm_pJOXToAt30_E,14
|
|
18
|
-
napari_tmidas-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|