napari-tmidas 0.1.6__py3-none-any.whl → 0.1.7__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 +3 -0
- napari_tmidas/_crop_anything.py +1113 -0
- napari_tmidas/_file_conversion.py +488 -256
- napari_tmidas/_file_selector.py +267 -101
- napari_tmidas/_label_inspection.py +10 -0
- napari_tmidas/_roi_colocalization.py +1175 -0
- napari_tmidas/_version.py +2 -2
- napari_tmidas/napari.yaml +10 -0
- napari_tmidas/processing_functions/basic.py +83 -0
- napari_tmidas/processing_functions/colocalization.py +242 -0
- napari_tmidas/processing_functions/skimage_filters.py +17 -32
- {napari_tmidas-0.1.6.dist-info → napari_tmidas-0.1.7.dist-info}/METADATA +44 -14
- napari_tmidas-0.1.7.dist-info/RECORD +29 -0
- napari_tmidas-0.1.6.dist-info/RECORD +0 -26
- {napari_tmidas-0.1.6.dist-info → napari_tmidas-0.1.7.dist-info}/WHEEL +0 -0
- {napari_tmidas-0.1.6.dist-info → napari_tmidas-0.1.7.dist-info}/entry_points.txt +0 -0
- {napari_tmidas-0.1.6.dist-info → napari_tmidas-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {napari_tmidas-0.1.6.dist-info → napari_tmidas-0.1.7.dist-info}/top_level.txt +0 -0
napari_tmidas/_file_selector.py
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Batch Image Processing with Napari
|
|
3
|
+
----------------------------------
|
|
4
|
+
This module provides a collection of functions for batch processing of image files.
|
|
5
|
+
It includes a Napari widget for selecting files and processing functions, and a
|
|
6
|
+
custom widget for displaying and processing the selected files.
|
|
7
|
+
|
|
8
|
+
New functions can be added to the processing registry by decorating them with
|
|
9
|
+
`@register_batch_processing_function`. Each function should accept an image array
|
|
10
|
+
as the first argument, and any additional keyword arguments for parameters.
|
|
11
|
+
"""
|
|
12
|
+
|
|
1
13
|
import concurrent.futures
|
|
2
|
-
import contextlib
|
|
3
14
|
import os
|
|
4
15
|
import sys
|
|
5
16
|
from typing import Any, Dict, List
|
|
@@ -58,6 +69,12 @@ class ProcessedFilesTableWidget(QTableWidget):
|
|
|
58
69
|
self.current_original_image = None
|
|
59
70
|
self.current_processed_image = None
|
|
60
71
|
|
|
72
|
+
# For tracking multi-output files
|
|
73
|
+
self.multi_output_files = {}
|
|
74
|
+
|
|
75
|
+
# Connect the cellDoubleClicked signal
|
|
76
|
+
self.cellDoubleClicked.connect(self._handle_cell_double_click)
|
|
77
|
+
|
|
61
78
|
def add_initial_files(self, file_list: List[str]):
|
|
62
79
|
"""
|
|
63
80
|
Add initial files to the table
|
|
@@ -65,6 +82,7 @@ class ProcessedFilesTableWidget(QTableWidget):
|
|
|
65
82
|
# Clear existing rows
|
|
66
83
|
self.setRowCount(0)
|
|
67
84
|
self.file_pairs.clear()
|
|
85
|
+
self.multi_output_files.clear()
|
|
68
86
|
|
|
69
87
|
# Add files
|
|
70
88
|
for filepath in file_list:
|
|
@@ -87,65 +105,154 @@ class ProcessedFilesTableWidget(QTableWidget):
|
|
|
87
105
|
"row": row,
|
|
88
106
|
}
|
|
89
107
|
|
|
90
|
-
def update_processed_files(self, processing_info:
|
|
108
|
+
def update_processed_files(self, processing_info: List[Dict]):
|
|
91
109
|
"""
|
|
92
110
|
Update table with processed files
|
|
93
111
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
Args:
|
|
113
|
+
processing_info: List of dictionaries containing:
|
|
114
|
+
{
|
|
115
|
+
'original_file': original filepath,
|
|
116
|
+
'processed_file': processed filepath (single output)
|
|
117
|
+
- OR -
|
|
118
|
+
'processed_files': list of processed filepaths (multi-output)
|
|
119
|
+
}
|
|
98
120
|
"""
|
|
99
121
|
for item in processing_info:
|
|
100
122
|
original_file = item["original_file"]
|
|
101
|
-
processed_file = item["processed_file"]
|
|
102
|
-
|
|
103
|
-
# Find the corresponding row
|
|
104
|
-
if original_file in self.file_pairs:
|
|
105
|
-
row = self.file_pairs[original_file]["row"]
|
|
106
|
-
|
|
107
|
-
# Update processed file column
|
|
108
|
-
processed_item = QTableWidgetItem(
|
|
109
|
-
os.path.basename(processed_file)
|
|
110
|
-
)
|
|
111
|
-
processed_item.setData(Qt.UserRole, processed_file)
|
|
112
|
-
self.setItem(row, 1, processed_item)
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
# Handle single processed file case
|
|
125
|
+
if "processed_file" in item:
|
|
126
|
+
processed_file = item["processed_file"]
|
|
127
|
+
|
|
128
|
+
# Find the corresponding row
|
|
129
|
+
if original_file in self.file_pairs:
|
|
130
|
+
row = self.file_pairs[original_file]["row"]
|
|
131
|
+
|
|
132
|
+
# Create a single item with the processed file
|
|
133
|
+
file_name = os.path.basename(processed_file)
|
|
134
|
+
processed_item = QTableWidgetItem(file_name)
|
|
135
|
+
processed_item.setData(Qt.UserRole, processed_file)
|
|
136
|
+
processed_item.setToolTip("Double-click to view")
|
|
137
|
+
self.setItem(row, 1, processed_item)
|
|
138
|
+
|
|
139
|
+
# Update file pairs
|
|
140
|
+
self.file_pairs[original_file][
|
|
141
|
+
"processed"
|
|
142
|
+
] = processed_file
|
|
143
|
+
|
|
144
|
+
# Handle multi-file output case
|
|
145
|
+
elif "processed_files" in item and item["processed_files"]:
|
|
146
|
+
processed_files = item["processed_files"]
|
|
147
|
+
|
|
148
|
+
# Store all processed files for this original file
|
|
149
|
+
self.multi_output_files[original_file] = processed_files
|
|
150
|
+
|
|
151
|
+
# Find the corresponding row
|
|
152
|
+
if original_file in self.file_pairs:
|
|
153
|
+
row = self.file_pairs[original_file]["row"]
|
|
154
|
+
|
|
155
|
+
# Create a ComboBox for selecting outputs
|
|
156
|
+
combo = QComboBox()
|
|
157
|
+
for i, file_path in enumerate(processed_files):
|
|
158
|
+
file_name = os.path.basename(file_path)
|
|
159
|
+
combo.addItem(f"Channel {i}: {file_name}", file_path)
|
|
160
|
+
|
|
161
|
+
# Connect the combo box to load the selected processed file
|
|
162
|
+
combo.currentIndexChanged.connect(
|
|
163
|
+
lambda idx, files=processed_files: self._load_processed_image(
|
|
164
|
+
files[idx]
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Add the ComboBox directly to the table cell
|
|
169
|
+
self.setCellWidget(row, 1, combo)
|
|
170
|
+
|
|
171
|
+
# Update file pairs with first file as default
|
|
172
|
+
self.file_pairs[original_file]["processed"] = (
|
|
173
|
+
processed_files[0]
|
|
174
|
+
)
|
|
116
175
|
|
|
117
176
|
def mousePressEvent(self, event):
|
|
118
177
|
"""
|
|
119
|
-
|
|
178
|
+
Handle mouse click events on the table to load appropriate images
|
|
120
179
|
"""
|
|
121
180
|
if event.button() == Qt.LeftButton:
|
|
181
|
+
# Get the item at the click position
|
|
122
182
|
item = self.itemAt(event.pos())
|
|
123
|
-
|
|
183
|
+
column = self.columnAt(event.pos().x())
|
|
184
|
+
row = self.rowAt(event.pos().y())
|
|
185
|
+
|
|
186
|
+
# Load original image when clicking on first column
|
|
187
|
+
if column == 0 and item:
|
|
124
188
|
filepath = item.data(Qt.UserRole)
|
|
125
189
|
if filepath:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
190
|
+
self._load_original_image(filepath)
|
|
191
|
+
|
|
192
|
+
# Load processed image when clicking on second column (for single output files)
|
|
193
|
+
elif column == 1:
|
|
194
|
+
# Check if this cell has a non-combo-box item (single output)
|
|
195
|
+
cell_item = self.item(row, column)
|
|
196
|
+
if cell_item and cell_item.data(Qt.UserRole):
|
|
197
|
+
filepath = cell_item.data(Qt.UserRole)
|
|
198
|
+
if filepath:
|
|
133
199
|
self._load_processed_image(filepath)
|
|
200
|
+
# Combo boxes are handled by their own event handlers
|
|
134
201
|
|
|
135
202
|
super().mousePressEvent(event)
|
|
136
203
|
|
|
204
|
+
def _handle_cell_double_click(self, row, column):
|
|
205
|
+
"""
|
|
206
|
+
Handle double-click events on cells, particularly for single processed files
|
|
207
|
+
"""
|
|
208
|
+
if column == 1:
|
|
209
|
+
item = self.item(row, column)
|
|
210
|
+
if (
|
|
211
|
+
item
|
|
212
|
+
): # This means it's a single processed file, not a combo box
|
|
213
|
+
filepath = item.data(Qt.UserRole)
|
|
214
|
+
if filepath:
|
|
215
|
+
self._load_processed_image(filepath)
|
|
216
|
+
|
|
137
217
|
def _load_original_image(self, filepath: str):
|
|
138
218
|
"""
|
|
139
219
|
Load original image into viewer
|
|
140
220
|
"""
|
|
221
|
+
# Ensure filepath is valid
|
|
222
|
+
if not filepath or not os.path.exists(filepath):
|
|
223
|
+
print(f"Error: File does not exist: {filepath}")
|
|
224
|
+
self.viewer.status = f"Error: File not found: {filepath}"
|
|
225
|
+
return
|
|
226
|
+
|
|
141
227
|
# Remove existing original layer if it exists
|
|
142
228
|
if self.current_original_image is not None:
|
|
143
|
-
|
|
144
|
-
|
|
229
|
+
try:
|
|
230
|
+
# Check if the layer is still in the viewer
|
|
231
|
+
if self.current_original_image in self.viewer.layers:
|
|
232
|
+
self.viewer.layers.remove(self.current_original_image)
|
|
233
|
+
else:
|
|
234
|
+
# If not found by reference, try by name
|
|
235
|
+
layer_names = [layer.name for layer in self.viewer.layers]
|
|
236
|
+
if self.current_original_image.name in layer_names:
|
|
237
|
+
self.viewer.layers.remove(
|
|
238
|
+
self.current_original_image.name
|
|
239
|
+
)
|
|
240
|
+
except (KeyError, ValueError) as e:
|
|
241
|
+
print(
|
|
242
|
+
f"Warning: Could not remove previous original layer: {e}"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Reset the current original image reference
|
|
246
|
+
self.current_original_image = None
|
|
145
247
|
|
|
146
248
|
# Load new image
|
|
147
249
|
try:
|
|
250
|
+
# Display status while loading
|
|
251
|
+
self.viewer.status = f"Loading {os.path.basename(filepath)}..."
|
|
252
|
+
|
|
148
253
|
image = imread(filepath)
|
|
254
|
+
# remove singletons
|
|
255
|
+
image = np.squeeze(image)
|
|
149
256
|
# check if label image by checking file name
|
|
150
257
|
is_label = "labels" in os.path.basename(
|
|
151
258
|
filepath
|
|
@@ -159,6 +266,10 @@ class ProcessedFilesTableWidget(QTableWidget):
|
|
|
159
266
|
self.current_original_image = self.viewer.add_image(
|
|
160
267
|
image, name=f"Original: {os.path.basename(filepath)}"
|
|
161
268
|
)
|
|
269
|
+
|
|
270
|
+
# Update status with success message
|
|
271
|
+
self.viewer.status = f"Loaded {os.path.basename(filepath)}"
|
|
272
|
+
|
|
162
273
|
except (ValueError, TypeError, OSError, tifffile.TiffFileError) as e:
|
|
163
274
|
print(f"Error loading original image {filepath}: {e}")
|
|
164
275
|
self.viewer.status = f"Error processing {filepath}: {e}"
|
|
@@ -166,15 +277,43 @@ class ProcessedFilesTableWidget(QTableWidget):
|
|
|
166
277
|
def _load_processed_image(self, filepath: str):
|
|
167
278
|
"""
|
|
168
279
|
Load processed image into viewer, distinguishing labels by filename pattern
|
|
280
|
+
and ensure it's always shown on top
|
|
169
281
|
"""
|
|
282
|
+
# Ensure filepath is valid
|
|
283
|
+
if not filepath or not os.path.exists(filepath):
|
|
284
|
+
print(f"Error: File does not exist: {filepath}")
|
|
285
|
+
self.viewer.status = f"Error: File not found: {filepath}"
|
|
286
|
+
return
|
|
287
|
+
|
|
170
288
|
# Remove existing processed layer if it exists
|
|
171
289
|
if self.current_processed_image is not None:
|
|
172
|
-
|
|
173
|
-
|
|
290
|
+
try:
|
|
291
|
+
# Check if the layer is still in the viewer
|
|
292
|
+
if self.current_processed_image in self.viewer.layers:
|
|
293
|
+
self.viewer.layers.remove(self.current_processed_image)
|
|
294
|
+
else:
|
|
295
|
+
# If not found by reference, try by name
|
|
296
|
+
layer_names = [layer.name for layer in self.viewer.layers]
|
|
297
|
+
if self.current_processed_image.name in layer_names:
|
|
298
|
+
self.viewer.layers.remove(
|
|
299
|
+
self.current_processed_image.name
|
|
300
|
+
)
|
|
301
|
+
except (KeyError, ValueError) as e:
|
|
302
|
+
print(
|
|
303
|
+
f"Warning: Could not remove previous processed layer: {e}"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Reset the current processed image reference
|
|
307
|
+
self.current_processed_image = None
|
|
174
308
|
|
|
175
309
|
# Load new image
|
|
176
310
|
try:
|
|
311
|
+
# Display status while loading
|
|
312
|
+
self.viewer.status = f"Loading {os.path.basename(filepath)}..."
|
|
313
|
+
|
|
177
314
|
image = imread(filepath)
|
|
315
|
+
# remove singletons
|
|
316
|
+
image = np.squeeze(image)
|
|
178
317
|
filename = os.path.basename(filepath)
|
|
179
318
|
|
|
180
319
|
# Check if filename contains label indicators
|
|
@@ -194,7 +333,21 @@ class ProcessedFilesTableWidget(QTableWidget):
|
|
|
194
333
|
image, name=f"Processed: {filename}"
|
|
195
334
|
)
|
|
196
335
|
|
|
197
|
-
|
|
336
|
+
# Move the processed layer to the top of the stack
|
|
337
|
+
# Get the index of the current processed layer
|
|
338
|
+
layer_index = self.viewer.layers.index(
|
|
339
|
+
self.current_processed_image
|
|
340
|
+
)
|
|
341
|
+
# Move it to the top (last position in the list)
|
|
342
|
+
if layer_index < len(self.viewer.layers) - 1:
|
|
343
|
+
self.viewer.layers.move(
|
|
344
|
+
layer_index, len(self.viewer.layers) - 1
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Update status with success message
|
|
348
|
+
self.viewer.status = f"Loaded {filename} (moved to top layer)"
|
|
349
|
+
|
|
350
|
+
except (ValueError, TypeError, OSError, tifffile.TiffFileError) as e:
|
|
198
351
|
print(f"Error loading processed image {filepath}: {e}")
|
|
199
352
|
self.viewer.status = f"Error processing {filepath}: {e}"
|
|
200
353
|
|
|
@@ -351,7 +504,6 @@ def _add_browse_button_to_selector(file_selector_widget):
|
|
|
351
504
|
h_layout.addWidget(browse_button)
|
|
352
505
|
|
|
353
506
|
# Replace the input field with our container
|
|
354
|
-
# parent = input_folder_widget.parentWidget()
|
|
355
507
|
layout_index = parent_layout.indexOf(input_folder_widget)
|
|
356
508
|
parent_layout.removeWidget(input_folder_widget)
|
|
357
509
|
parent_layout.insertWidget(layout_index, container_widget)
|
|
@@ -404,6 +556,7 @@ class ProcessingWorker(QThread):
|
|
|
404
556
|
self.input_suffix = input_suffix
|
|
405
557
|
self.output_suffix = output_suffix
|
|
406
558
|
self.stop_requested = False
|
|
559
|
+
self.thread_count = max(1, (os.cpu_count() or 4) - 1) # Default value
|
|
407
560
|
|
|
408
561
|
def run(self):
|
|
409
562
|
"""Process files in a separate thread"""
|
|
@@ -411,8 +564,10 @@ class ProcessingWorker(QThread):
|
|
|
411
564
|
processed_files_info = []
|
|
412
565
|
total_files = len(self.file_list)
|
|
413
566
|
|
|
414
|
-
# Create a thread pool for concurrent processing
|
|
415
|
-
with concurrent.futures.ThreadPoolExecutor(
|
|
567
|
+
# Create a thread pool for concurrent processing with specified thread count
|
|
568
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
569
|
+
max_workers=self.thread_count
|
|
570
|
+
) as executor:
|
|
416
571
|
# Submit tasks
|
|
417
572
|
future_to_file = {
|
|
418
573
|
executor.submit(self.process_file, filepath): filepath
|
|
@@ -457,35 +612,85 @@ class ProcessingWorker(QThread):
|
|
|
457
612
|
# Apply processing with parameters
|
|
458
613
|
processed_image = self.processing_func(image, **self.param_values)
|
|
459
614
|
|
|
460
|
-
# Generate new filename
|
|
615
|
+
# Generate new filename base
|
|
461
616
|
filename = os.path.basename(filepath)
|
|
462
617
|
name, ext = os.path.splitext(filename)
|
|
463
|
-
|
|
464
|
-
name.replace(self.input_suffix, "") + self.output_suffix
|
|
618
|
+
new_filename_base = (
|
|
619
|
+
name.replace(self.input_suffix, "") + self.output_suffix
|
|
465
620
|
)
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
621
|
+
|
|
622
|
+
# Check if the processed image is a stacked array
|
|
623
|
+
if processed_image.ndim > image.ndim:
|
|
624
|
+
# Save each channel as a separate image
|
|
625
|
+
processed_files = []
|
|
626
|
+
for i in range(processed_image.shape[0]):
|
|
627
|
+
channel_filename = f"{new_filename_base}_channel_{i}{ext}"
|
|
628
|
+
channel_filepath = os.path.join(
|
|
629
|
+
self.output_folder, channel_filename
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
if (
|
|
633
|
+
"labels" in channel_filename
|
|
634
|
+
or "semantic" in channel_filename
|
|
635
|
+
):
|
|
636
|
+
tifffile.imwrite(
|
|
637
|
+
channel_filepath,
|
|
638
|
+
processed_image[i].astype(np.uint32),
|
|
639
|
+
compression="zlib",
|
|
640
|
+
)
|
|
641
|
+
else:
|
|
642
|
+
# First remove singletons
|
|
643
|
+
channel_image = np.squeeze(processed_image[i])
|
|
644
|
+
tifffile.imwrite(
|
|
645
|
+
channel_filepath,
|
|
646
|
+
channel_image.astype(image_dtype),
|
|
647
|
+
compression="zlib",
|
|
648
|
+
)
|
|
649
|
+
processed_files.append(channel_filepath)
|
|
650
|
+
|
|
651
|
+
# Return processing info
|
|
652
|
+
return {
|
|
653
|
+
"original_file": filepath,
|
|
654
|
+
"processed_files": processed_files,
|
|
655
|
+
}
|
|
476
656
|
else:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
compression="zlib",
|
|
657
|
+
# Save as a single image (original behavior)
|
|
658
|
+
new_filepath = os.path.join(
|
|
659
|
+
self.output_folder, new_filename_base + ext
|
|
481
660
|
)
|
|
482
661
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
662
|
+
if (
|
|
663
|
+
"labels" in new_filename_base
|
|
664
|
+
or "semantic" in new_filename_base
|
|
665
|
+
):
|
|
666
|
+
tifffile.imwrite(
|
|
667
|
+
new_filepath,
|
|
668
|
+
processed_image.astype(np.uint32),
|
|
669
|
+
compression="zlib",
|
|
670
|
+
)
|
|
671
|
+
else:
|
|
672
|
+
tifffile.imwrite(
|
|
673
|
+
new_filepath,
|
|
674
|
+
processed_image.astype(image_dtype),
|
|
675
|
+
compression="zlib",
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
# Return processing info
|
|
679
|
+
return {
|
|
680
|
+
"original_file": filepath,
|
|
681
|
+
"processed_file": new_filepath,
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
except Exception as e:
|
|
685
|
+
# Log the error and re-raise to be caught by the executor
|
|
686
|
+
print(f"Error processing {filepath}: {e}")
|
|
488
687
|
raise
|
|
688
|
+
finally:
|
|
689
|
+
# Explicit cleanup to help with memory management
|
|
690
|
+
if "image" in locals():
|
|
691
|
+
del image
|
|
692
|
+
if "processed_image" in locals():
|
|
693
|
+
del processed_image
|
|
489
694
|
|
|
490
695
|
def stop(self):
|
|
491
696
|
"""Request worker to stop processing"""
|
|
@@ -517,14 +722,6 @@ class FileResultsWidget(QWidget):
|
|
|
517
722
|
layout = QVBoxLayout()
|
|
518
723
|
self.setLayout(layout)
|
|
519
724
|
|
|
520
|
-
# Input folder widgets
|
|
521
|
-
self.input_folder_widget = QLineEdit(self.input_folder)
|
|
522
|
-
layout.addWidget(self.input_folder_widget)
|
|
523
|
-
|
|
524
|
-
browse_button = QPushButton("Browse...")
|
|
525
|
-
browse_button.clicked.connect(self.browse_folder)
|
|
526
|
-
layout.addWidget(browse_button)
|
|
527
|
-
|
|
528
725
|
# Create table of files
|
|
529
726
|
self.table = ProcessedFilesTableWidget(viewer)
|
|
530
727
|
self.table.add_initial_files(file_list)
|
|
@@ -623,40 +820,6 @@ class FileResultsWidget(QWidget):
|
|
|
623
820
|
# Container for tracking processed files during batch operation
|
|
624
821
|
self.processed_files_info = []
|
|
625
822
|
|
|
626
|
-
def browse_folder(self):
|
|
627
|
-
"""
|
|
628
|
-
Open a file dialog to select a folder
|
|
629
|
-
"""
|
|
630
|
-
folder = QFileDialog.getExistingDirectory(
|
|
631
|
-
self, "Select Folder", self.input_folder
|
|
632
|
-
)
|
|
633
|
-
if folder:
|
|
634
|
-
self.input_folder = folder
|
|
635
|
-
self.input_folder_widget.setText(folder)
|
|
636
|
-
self.validate_selected_folder(folder)
|
|
637
|
-
|
|
638
|
-
def validate_selected_folder(self, folder: str):
|
|
639
|
-
"""
|
|
640
|
-
Validate the selected folder and update the file list
|
|
641
|
-
"""
|
|
642
|
-
if not os.path.isdir(folder):
|
|
643
|
-
self.viewer.status = f"Invalid input folder: {folder}"
|
|
644
|
-
return
|
|
645
|
-
|
|
646
|
-
# Find matching files
|
|
647
|
-
matching_files = [
|
|
648
|
-
os.path.join(folder, f)
|
|
649
|
-
for f in os.listdir(folder)
|
|
650
|
-
if f.endswith(self.input_suffix)
|
|
651
|
-
]
|
|
652
|
-
|
|
653
|
-
# Update table with new files
|
|
654
|
-
self.file_list = matching_files
|
|
655
|
-
self.table.add_initial_files(matching_files)
|
|
656
|
-
|
|
657
|
-
# Update viewer status
|
|
658
|
-
self.viewer.status = f"Found {len(matching_files)} files"
|
|
659
|
-
|
|
660
823
|
def update_function_info(self, function_name: str):
|
|
661
824
|
"""
|
|
662
825
|
Update the function description and parameters when a new function is selected
|
|
@@ -754,6 +917,9 @@ class FileResultsWidget(QWidget):
|
|
|
754
917
|
output_suffix,
|
|
755
918
|
)
|
|
756
919
|
|
|
920
|
+
# Set the thread count from the UI
|
|
921
|
+
self.worker.thread_count = self.thread_count.value()
|
|
922
|
+
|
|
757
923
|
# Connect signals
|
|
758
924
|
self.worker.progress_updated.connect(self.update_progress)
|
|
759
925
|
self.worker.file_processed.connect(self.file_processed)
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Batch Label Inspection for Napari
|
|
3
|
+
---------------------------------
|
|
4
|
+
This module provides a widget for Napari that allows users to inspect image-label pairs in a folder.
|
|
5
|
+
The widget loads image-label pairs from a folder and displays them in the Napari viewer.
|
|
6
|
+
Users can make and save changes to the labels, and proceed to the next pair.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
1
11
|
import os
|
|
2
12
|
import sys
|
|
3
13
|
|