napari-tmidas 0.1.3__py3-none-any.whl → 0.1.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/_file_conversion.py +1477 -0
- napari_tmidas/_file_selector.py +357 -60
- napari_tmidas/_label_inspection.py +87 -26
- napari_tmidas/_version.py +2 -2
- napari_tmidas/napari.yaml +5 -0
- napari_tmidas/processing_functions/basic.py +24 -42
- napari_tmidas/processing_functions/skimage_filters.py +60 -43
- {napari_tmidas-0.1.3.dist-info → napari_tmidas-0.1.4.dist-info}/METADATA +29 -10
- {napari_tmidas-0.1.3.dist-info → napari_tmidas-0.1.4.dist-info}/RECORD +13 -12
- {napari_tmidas-0.1.3.dist-info → napari_tmidas-0.1.4.dist-info}/LICENSE +0 -0
- {napari_tmidas-0.1.3.dist-info → napari_tmidas-0.1.4.dist-info}/WHEEL +0 -0
- {napari_tmidas-0.1.3.dist-info → napari_tmidas-0.1.4.dist-info}/entry_points.txt +0 -0
- {napari_tmidas-0.1.3.dist-info → napari_tmidas-0.1.4.dist-info}/top_level.txt +0 -0
napari_tmidas/_file_selector.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import concurrent.futures
|
|
1
2
|
import contextlib
|
|
2
3
|
import os
|
|
3
4
|
import sys
|
|
@@ -7,14 +8,17 @@ import napari
|
|
|
7
8
|
import numpy as np
|
|
8
9
|
import tifffile
|
|
9
10
|
from magicgui import magicgui
|
|
10
|
-
from qtpy.QtCore import Qt
|
|
11
|
+
from qtpy.QtCore import Qt, QThread, Signal
|
|
11
12
|
from qtpy.QtWidgets import (
|
|
12
13
|
QComboBox,
|
|
13
14
|
QDoubleSpinBox,
|
|
15
|
+
QFileDialog,
|
|
14
16
|
QFormLayout,
|
|
17
|
+
QHBoxLayout,
|
|
15
18
|
QHeaderView,
|
|
16
19
|
QLabel,
|
|
17
20
|
QLineEdit,
|
|
21
|
+
QProgressBar,
|
|
18
22
|
QPushButton,
|
|
19
23
|
QSpinBox,
|
|
20
24
|
QTableWidget,
|
|
@@ -22,6 +26,7 @@ from qtpy.QtWidgets import (
|
|
|
22
26
|
QVBoxLayout,
|
|
23
27
|
QWidget,
|
|
24
28
|
)
|
|
29
|
+
from skimage.io import imread
|
|
25
30
|
|
|
26
31
|
# Import registry and processing functions
|
|
27
32
|
from napari_tmidas._registry import BatchProcessingRegistry
|
|
@@ -140,10 +145,20 @@ class ProcessedFilesTableWidget(QTableWidget):
|
|
|
140
145
|
|
|
141
146
|
# Load new image
|
|
142
147
|
try:
|
|
143
|
-
image =
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
image = imread(filepath)
|
|
149
|
+
# check if label image by checking file name
|
|
150
|
+
is_label = "labels" in os.path.basename(
|
|
151
|
+
filepath
|
|
152
|
+
) or "semantic" in os.path.basename(filepath)
|
|
153
|
+
if is_label:
|
|
154
|
+
image = image.astype(np.uint32)
|
|
155
|
+
self.current_original_image = self.viewer.add_labels(
|
|
156
|
+
image, name=f"Labels: {os.path.basename(filepath)}"
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
self.current_original_image = self.viewer.add_image(
|
|
160
|
+
image, name=f"Original: {os.path.basename(filepath)}"
|
|
161
|
+
)
|
|
147
162
|
except (ValueError, TypeError, OSError, tifffile.TiffFileError) as e:
|
|
148
163
|
print(f"Error loading original image {filepath}: {e}")
|
|
149
164
|
self.viewer.status = f"Error processing {filepath}: {e}"
|
|
@@ -159,7 +174,7 @@ class ProcessedFilesTableWidget(QTableWidget):
|
|
|
159
174
|
|
|
160
175
|
# Load new image
|
|
161
176
|
try:
|
|
162
|
-
image =
|
|
177
|
+
image = imread(filepath)
|
|
163
178
|
filename = os.path.basename(filepath)
|
|
164
179
|
|
|
165
180
|
# Check if filename contains label indicators
|
|
@@ -263,11 +278,15 @@ class ParameterWidget(QWidget):
|
|
|
263
278
|
|
|
264
279
|
@magicgui(
|
|
265
280
|
call_button="Find and Index Image Files",
|
|
266
|
-
input_folder={
|
|
267
|
-
|
|
281
|
+
input_folder={
|
|
282
|
+
"widget_type": "LineEdit",
|
|
283
|
+
"label": "Select Folder",
|
|
284
|
+
"value": "",
|
|
285
|
+
},
|
|
286
|
+
input_suffix={"label": "File Suffix (Example: .tif)", "value": ""},
|
|
268
287
|
)
|
|
269
288
|
def file_selector(
|
|
270
|
-
viewer: napari.Viewer, input_folder: str, input_suffix: str = "
|
|
289
|
+
viewer: napari.Viewer, input_folder: str, input_suffix: str = ".tif"
|
|
271
290
|
) -> List[str]:
|
|
272
291
|
"""
|
|
273
292
|
Find files in a specified input folder with a given suffix and prepare for batch processing.
|
|
@@ -303,6 +322,176 @@ def file_selector(
|
|
|
303
322
|
return matching_files
|
|
304
323
|
|
|
305
324
|
|
|
325
|
+
# Modify the file_selector widget to add a browse button after it's created
|
|
326
|
+
def _add_browse_button_to_selector(file_selector_widget):
|
|
327
|
+
"""
|
|
328
|
+
Add a browse button to the file selector widget
|
|
329
|
+
"""
|
|
330
|
+
# Get the container widget that holds the input_folder widget
|
|
331
|
+
container = file_selector_widget.native
|
|
332
|
+
|
|
333
|
+
# Create a browse button
|
|
334
|
+
browse_button = QPushButton("Browse...")
|
|
335
|
+
|
|
336
|
+
# Get access to the input_folder widget
|
|
337
|
+
input_folder_widget = file_selector_widget.input_folder.native
|
|
338
|
+
|
|
339
|
+
# Get the parent of the input_folder widget
|
|
340
|
+
parent_layout = input_folder_widget.parentWidget().layout()
|
|
341
|
+
|
|
342
|
+
# Create a container for input field and browse button
|
|
343
|
+
container_widget = QWidget()
|
|
344
|
+
h_layout = QHBoxLayout(container_widget)
|
|
345
|
+
h_layout.setContentsMargins(0, 0, 0, 0)
|
|
346
|
+
|
|
347
|
+
# Add the input field to our container
|
|
348
|
+
h_layout.addWidget(input_folder_widget)
|
|
349
|
+
|
|
350
|
+
# Add the browse button
|
|
351
|
+
h_layout.addWidget(browse_button)
|
|
352
|
+
|
|
353
|
+
# Replace the input field with our container
|
|
354
|
+
# parent = input_folder_widget.parentWidget()
|
|
355
|
+
layout_index = parent_layout.indexOf(input_folder_widget)
|
|
356
|
+
parent_layout.removeWidget(input_folder_widget)
|
|
357
|
+
parent_layout.insertWidget(layout_index, container_widget)
|
|
358
|
+
|
|
359
|
+
# Connect button to browse action
|
|
360
|
+
def browse_folder():
|
|
361
|
+
folder = QFileDialog.getExistingDirectory(
|
|
362
|
+
container,
|
|
363
|
+
"Select Folder",
|
|
364
|
+
file_selector_widget.input_folder.value or os.path.expanduser("~"),
|
|
365
|
+
)
|
|
366
|
+
if folder:
|
|
367
|
+
file_selector_widget.input_folder.value = folder
|
|
368
|
+
|
|
369
|
+
browse_button.clicked.connect(browse_folder)
|
|
370
|
+
|
|
371
|
+
return file_selector_widget
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# Create a modified file_selector with browse button
|
|
375
|
+
file_selector = _add_browse_button_to_selector(file_selector)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# Processing worker for multithreading
|
|
379
|
+
class ProcessingWorker(QThread):
|
|
380
|
+
"""
|
|
381
|
+
Worker thread for processing images in the background
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
# Signals to communicate with the main thread
|
|
385
|
+
progress_updated = Signal(int)
|
|
386
|
+
file_processed = Signal(dict)
|
|
387
|
+
processing_finished = Signal()
|
|
388
|
+
error_occurred = Signal(str, str) # filepath, error message
|
|
389
|
+
|
|
390
|
+
def __init__(
|
|
391
|
+
self,
|
|
392
|
+
file_list,
|
|
393
|
+
processing_func,
|
|
394
|
+
param_values,
|
|
395
|
+
output_folder,
|
|
396
|
+
input_suffix,
|
|
397
|
+
output_suffix,
|
|
398
|
+
):
|
|
399
|
+
super().__init__()
|
|
400
|
+
self.file_list = file_list
|
|
401
|
+
self.processing_func = processing_func
|
|
402
|
+
self.param_values = param_values
|
|
403
|
+
self.output_folder = output_folder
|
|
404
|
+
self.input_suffix = input_suffix
|
|
405
|
+
self.output_suffix = output_suffix
|
|
406
|
+
self.stop_requested = False
|
|
407
|
+
|
|
408
|
+
def run(self):
|
|
409
|
+
"""Process files in a separate thread"""
|
|
410
|
+
# Track processed files
|
|
411
|
+
processed_files_info = []
|
|
412
|
+
total_files = len(self.file_list)
|
|
413
|
+
|
|
414
|
+
# Create a thread pool for concurrent processing
|
|
415
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
416
|
+
# Submit tasks
|
|
417
|
+
future_to_file = {
|
|
418
|
+
executor.submit(self.process_file, filepath): filepath
|
|
419
|
+
for filepath in self.file_list
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# Process as they complete
|
|
423
|
+
for i, future in enumerate(
|
|
424
|
+
concurrent.futures.as_completed(future_to_file)
|
|
425
|
+
):
|
|
426
|
+
# Check if cancellation was requested
|
|
427
|
+
if self.stop_requested:
|
|
428
|
+
break
|
|
429
|
+
|
|
430
|
+
filepath = future_to_file[future]
|
|
431
|
+
try:
|
|
432
|
+
result = future.result()
|
|
433
|
+
if result:
|
|
434
|
+
processed_files_info.append(result)
|
|
435
|
+
self.file_processed.emit(result)
|
|
436
|
+
except (
|
|
437
|
+
ValueError,
|
|
438
|
+
TypeError,
|
|
439
|
+
OSError,
|
|
440
|
+
tifffile.TiffFileError,
|
|
441
|
+
) as e:
|
|
442
|
+
self.error_occurred.emit(filepath, str(e))
|
|
443
|
+
|
|
444
|
+
# Update progress
|
|
445
|
+
self.progress_updated.emit(int((i + 1) / total_files * 100))
|
|
446
|
+
|
|
447
|
+
# Signal that processing is complete
|
|
448
|
+
self.processing_finished.emit()
|
|
449
|
+
|
|
450
|
+
def process_file(self, filepath):
|
|
451
|
+
"""Process a single file"""
|
|
452
|
+
try:
|
|
453
|
+
# Load the image
|
|
454
|
+
image = imread(filepath)
|
|
455
|
+
image_dtype = image.dtype
|
|
456
|
+
|
|
457
|
+
# Apply processing with parameters
|
|
458
|
+
processed_image = self.processing_func(image, **self.param_values)
|
|
459
|
+
|
|
460
|
+
# Generate new filename
|
|
461
|
+
filename = os.path.basename(filepath)
|
|
462
|
+
name, ext = os.path.splitext(filename)
|
|
463
|
+
new_filename = (
|
|
464
|
+
name.replace(self.input_suffix, "") + self.output_suffix + ext
|
|
465
|
+
)
|
|
466
|
+
new_filepath = os.path.join(self.output_folder, new_filename)
|
|
467
|
+
|
|
468
|
+
# Save the processed image
|
|
469
|
+
if "labels" in new_filename or "semantic" in new_filename:
|
|
470
|
+
# processed_image = ndi.label(processed_image)[0]
|
|
471
|
+
tifffile.imwrite(
|
|
472
|
+
new_filepath,
|
|
473
|
+
processed_image.astype(np.uint32),
|
|
474
|
+
compression="zlib",
|
|
475
|
+
)
|
|
476
|
+
else:
|
|
477
|
+
tifffile.imwrite(
|
|
478
|
+
new_filepath,
|
|
479
|
+
processed_image.astype(image_dtype),
|
|
480
|
+
compression="zlib",
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# Return processing info
|
|
484
|
+
return {"original_file": filepath, "processed_file": new_filepath}
|
|
485
|
+
|
|
486
|
+
except Exception:
|
|
487
|
+
# Re-raise to be caught by the executor
|
|
488
|
+
raise
|
|
489
|
+
|
|
490
|
+
def stop(self):
|
|
491
|
+
"""Request worker to stop processing"""
|
|
492
|
+
self.stop_requested = True
|
|
493
|
+
|
|
494
|
+
|
|
306
495
|
class FileResultsWidget(QWidget):
|
|
307
496
|
"""
|
|
308
497
|
Custom widget to display matching files and enable batch processing
|
|
@@ -322,19 +511,20 @@ class FileResultsWidget(QWidget):
|
|
|
322
511
|
self.file_list = file_list
|
|
323
512
|
self.input_folder = input_folder
|
|
324
513
|
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)
|
|
514
|
+
self.worker = None # Will hold the processing worker
|
|
333
515
|
|
|
334
516
|
# Create main layout
|
|
335
517
|
layout = QVBoxLayout()
|
|
336
518
|
self.setLayout(layout)
|
|
337
519
|
|
|
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
|
+
|
|
338
528
|
# Create table of files
|
|
339
529
|
self.table = ProcessedFilesTableWidget(viewer)
|
|
340
530
|
self.table.add_initial_files(file_list)
|
|
@@ -342,6 +532,14 @@ class FileResultsWidget(QWidget):
|
|
|
342
532
|
# Add table to layout
|
|
343
533
|
layout.addWidget(self.table)
|
|
344
534
|
|
|
535
|
+
# Load all processing functions
|
|
536
|
+
print("Calling discover_and_load_processing_functions")
|
|
537
|
+
discover_and_load_processing_functions()
|
|
538
|
+
# print what is found by discover_and_load_processing_functions
|
|
539
|
+
print("Available processing functions:")
|
|
540
|
+
for func_name in BatchProcessingRegistry.list_functions():
|
|
541
|
+
print(func_name)
|
|
542
|
+
|
|
345
543
|
# Create processing function selector
|
|
346
544
|
processing_layout = QVBoxLayout()
|
|
347
545
|
processing_label = QLabel("Select Processing Function:")
|
|
@@ -377,18 +575,88 @@ class FileResultsWidget(QWidget):
|
|
|
377
575
|
)
|
|
378
576
|
output_layout.addWidget(self.output_folder)
|
|
379
577
|
|
|
578
|
+
# Thread count selector
|
|
579
|
+
thread_layout = QHBoxLayout()
|
|
580
|
+
thread_label = QLabel("Number of threads:")
|
|
581
|
+
thread_layout.addWidget(thread_label)
|
|
582
|
+
|
|
583
|
+
self.thread_count = QSpinBox()
|
|
584
|
+
self.thread_count.setMinimum(1)
|
|
585
|
+
self.thread_count.setMaximum(
|
|
586
|
+
os.cpu_count() or 4
|
|
587
|
+
) # Default to CPU count or 4
|
|
588
|
+
self.thread_count.setValue(
|
|
589
|
+
max(1, (os.cpu_count() or 4) - 1)
|
|
590
|
+
) # Default to CPU count - 1
|
|
591
|
+
thread_layout.addWidget(self.thread_count)
|
|
592
|
+
|
|
593
|
+
output_layout.addLayout(thread_layout)
|
|
594
|
+
|
|
595
|
+
# Progress bar
|
|
596
|
+
self.progress_bar = QProgressBar()
|
|
597
|
+
self.progress_bar.setRange(0, 100)
|
|
598
|
+
self.progress_bar.setValue(0)
|
|
599
|
+
self.progress_bar.setVisible(False) # Hide initially
|
|
600
|
+
|
|
380
601
|
layout.addLayout(processing_layout)
|
|
381
602
|
layout.addLayout(output_layout)
|
|
603
|
+
layout.addWidget(self.progress_bar)
|
|
604
|
+
|
|
605
|
+
# Add batch processing and cancel buttons
|
|
606
|
+
button_layout = QHBoxLayout()
|
|
382
607
|
|
|
383
|
-
# Add batch processing button
|
|
384
608
|
self.batch_button = QPushButton("Start Batch Processing")
|
|
385
609
|
self.batch_button.clicked.connect(self.start_batch_processing)
|
|
386
|
-
|
|
610
|
+
button_layout.addWidget(self.batch_button)
|
|
611
|
+
|
|
612
|
+
self.cancel_button = QPushButton("Cancel Processing")
|
|
613
|
+
self.cancel_button.clicked.connect(self.cancel_processing)
|
|
614
|
+
self.cancel_button.setEnabled(False) # Disabled initially
|
|
615
|
+
button_layout.addWidget(self.cancel_button)
|
|
616
|
+
|
|
617
|
+
layout.addLayout(button_layout)
|
|
387
618
|
|
|
388
619
|
# Initialize parameters for the first function
|
|
389
620
|
if self.processing_selector.count() > 0:
|
|
390
621
|
self.update_function_info(self.processing_selector.currentText())
|
|
391
622
|
|
|
623
|
+
# Container for tracking processed files during batch operation
|
|
624
|
+
self.processed_files_info = []
|
|
625
|
+
|
|
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
|
+
|
|
392
660
|
def update_function_info(self, function_name: str):
|
|
393
661
|
"""
|
|
394
662
|
Update the function description and parameters when a new function is selected
|
|
@@ -434,7 +702,7 @@ class FileResultsWidget(QWidget):
|
|
|
434
702
|
|
|
435
703
|
def start_batch_processing(self):
|
|
436
704
|
"""
|
|
437
|
-
Initiate batch processing of selected files
|
|
705
|
+
Initiate multithreaded batch processing of selected files
|
|
438
706
|
"""
|
|
439
707
|
# Get selected processing function
|
|
440
708
|
selected_function_name = self.processing_selector.currentText()
|
|
@@ -467,48 +735,77 @@ class FileResultsWidget(QWidget):
|
|
|
467
735
|
# Ensure output folder exists
|
|
468
736
|
os.makedirs(output_folder, exist_ok=True)
|
|
469
737
|
|
|
470
|
-
#
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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}")
|
|
738
|
+
# Reset progress tracking
|
|
739
|
+
self.progress_bar.setValue(0)
|
|
740
|
+
self.progress_bar.setVisible(True)
|
|
741
|
+
self.processed_files_info = []
|
|
742
|
+
|
|
743
|
+
# Update UI
|
|
744
|
+
self.batch_button.setEnabled(False)
|
|
745
|
+
self.cancel_button.setEnabled(True)
|
|
746
|
+
|
|
747
|
+
# Create and start the worker thread
|
|
748
|
+
self.worker = ProcessingWorker(
|
|
749
|
+
self.file_list,
|
|
750
|
+
processing_func,
|
|
751
|
+
param_values,
|
|
752
|
+
output_folder,
|
|
753
|
+
self.input_suffix,
|
|
754
|
+
output_suffix,
|
|
755
|
+
)
|
|
506
756
|
|
|
507
|
-
#
|
|
508
|
-
self.
|
|
757
|
+
# Connect signals
|
|
758
|
+
self.worker.progress_updated.connect(self.update_progress)
|
|
759
|
+
self.worker.file_processed.connect(self.file_processed)
|
|
760
|
+
self.worker.processing_finished.connect(self.processing_finished)
|
|
761
|
+
self.worker.error_occurred.connect(self.processing_error)
|
|
762
|
+
|
|
763
|
+
# Start processing
|
|
764
|
+
self.worker.start()
|
|
765
|
+
|
|
766
|
+
# Update status
|
|
767
|
+
self.viewer.status = f"Processing {len(self.file_list)} files with {selected_function_name} using {self.thread_count.value()} threads"
|
|
768
|
+
|
|
769
|
+
def update_progress(self, value):
|
|
770
|
+
"""Update the progress bar"""
|
|
771
|
+
self.progress_bar.setValue(value)
|
|
772
|
+
|
|
773
|
+
def file_processed(self, result):
|
|
774
|
+
"""Handle a processed file result"""
|
|
775
|
+
self.processed_files_info.append(result)
|
|
776
|
+
# Update table with this single processed file
|
|
777
|
+
self.table.update_processed_files([result])
|
|
778
|
+
|
|
779
|
+
def processing_finished(self):
|
|
780
|
+
"""Handle processing completion"""
|
|
781
|
+
# Update UI
|
|
782
|
+
self.progress_bar.setValue(100)
|
|
783
|
+
self.batch_button.setEnabled(True)
|
|
784
|
+
self.cancel_button.setEnabled(False)
|
|
785
|
+
|
|
786
|
+
# Clean up worker
|
|
787
|
+
self.worker = None
|
|
788
|
+
|
|
789
|
+
# Update status
|
|
790
|
+
self.viewer.status = (
|
|
791
|
+
f"Completed processing {len(self.processed_files_info)} files"
|
|
792
|
+
)
|
|
509
793
|
|
|
510
|
-
|
|
511
|
-
|
|
794
|
+
def processing_error(self, filepath, error_msg):
|
|
795
|
+
"""Handle processing errors"""
|
|
796
|
+
print(f"Error processing {filepath}: {error_msg}")
|
|
797
|
+
self.viewer.status = f"Error processing {filepath}: {error_msg}"
|
|
798
|
+
|
|
799
|
+
def cancel_processing(self):
|
|
800
|
+
"""Cancel the current processing operation"""
|
|
801
|
+
if self.worker and self.worker.isRunning():
|
|
802
|
+
self.worker.stop()
|
|
803
|
+
self.worker.wait() # Wait for the thread to finish
|
|
804
|
+
|
|
805
|
+
# Update UI
|
|
806
|
+
self.batch_button.setEnabled(True)
|
|
807
|
+
self.cancel_button.setEnabled(False)
|
|
808
|
+
self.viewer.status = "Processing cancelled"
|
|
512
809
|
|
|
513
810
|
|
|
514
811
|
def napari_experimental_provide_dock_widget():
|
|
@@ -4,7 +4,8 @@ import sys
|
|
|
4
4
|
from magicgui import magicgui
|
|
5
5
|
from napari.layers import Labels
|
|
6
6
|
from napari.viewer import Viewer
|
|
7
|
-
from
|
|
7
|
+
from qtpy.QtWidgets import QFileDialog, QPushButton
|
|
8
|
+
from skimage.io import imread # , imsave
|
|
8
9
|
|
|
9
10
|
sys.path.append("src/napari_tmidas")
|
|
10
11
|
|
|
@@ -15,31 +16,56 @@ class LabelInspector:
|
|
|
15
16
|
self.image_label_pairs = []
|
|
16
17
|
self.current_index = 0
|
|
17
18
|
|
|
18
|
-
def load_image_label_pairs(
|
|
19
|
-
self, folder_path: str, image_suffix: str, label_suffix: str
|
|
20
|
-
):
|
|
19
|
+
def load_image_label_pairs(self, folder_path: str, label_suffix: str):
|
|
21
20
|
"""
|
|
22
21
|
Load image-label pairs from a folder.
|
|
22
|
+
Finds label files with the given suffix and matches them with their corresponding image files.
|
|
23
23
|
"""
|
|
24
24
|
files = os.listdir(folder_path)
|
|
25
|
-
image_files = [file for file in files if file.endswith(image_suffix)]
|
|
26
25
|
label_files = [file for file in files if file.endswith(label_suffix)]
|
|
27
26
|
|
|
27
|
+
# Extract the file extension (e.g., .tif)
|
|
28
|
+
file_extension = (
|
|
29
|
+
os.path.splitext(label_suffix)[-1] if "." in label_suffix else ""
|
|
30
|
+
)
|
|
31
|
+
|
|
28
32
|
# Modified matching logic
|
|
29
33
|
self.image_label_pairs = []
|
|
30
|
-
for
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
for lbl in label_files:
|
|
35
|
+
# Remove the label suffix to get the base name
|
|
36
|
+
label_prefix = lbl[: -len(label_suffix)]
|
|
37
|
+
|
|
38
|
+
# Potential corresponding image file
|
|
39
|
+
img = f"{label_prefix}{file_extension}"
|
|
40
|
+
img_path = os.path.join(folder_path, img)
|
|
41
|
+
|
|
42
|
+
# Check if the image file exists
|
|
43
|
+
if os.path.exists(img_path):
|
|
44
|
+
self.image_label_pairs.append(
|
|
45
|
+
(
|
|
46
|
+
img_path,
|
|
47
|
+
os.path.join(folder_path, lbl),
|
|
41
48
|
)
|
|
42
|
-
|
|
49
|
+
)
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
# If not found, try finding any file that starts with the base name
|
|
53
|
+
potential_images = [
|
|
54
|
+
file
|
|
55
|
+
for file in files
|
|
56
|
+
if file.startswith(label_prefix)
|
|
57
|
+
and file.endswith(file_extension)
|
|
58
|
+
and file != lbl
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
if potential_images:
|
|
62
|
+
# Use the first matching image
|
|
63
|
+
self.image_label_pairs.append(
|
|
64
|
+
(
|
|
65
|
+
os.path.join(folder_path, potential_images[0]),
|
|
66
|
+
os.path.join(folder_path, lbl),
|
|
67
|
+
)
|
|
68
|
+
)
|
|
43
69
|
|
|
44
70
|
if not self.image_label_pairs:
|
|
45
71
|
self.viewer.status = "No matching image-label pairs found."
|
|
@@ -99,7 +125,8 @@ class LabelInspector:
|
|
|
99
125
|
return
|
|
100
126
|
|
|
101
127
|
# Save the labels layer data to the original file path
|
|
102
|
-
imsave(label_path, labels_layer.data.astype("
|
|
128
|
+
# imsave(label_path, labels_layer.data.astype("uint32"))
|
|
129
|
+
labels_layer.save(label_path)
|
|
103
130
|
self.viewer.status = f"Saved labels to {label_path}."
|
|
104
131
|
|
|
105
132
|
def next_pair(self):
|
|
@@ -115,27 +142,30 @@ class LabelInspector:
|
|
|
115
142
|
|
|
116
143
|
# Check if we're already at the last pair
|
|
117
144
|
if self.current_index >= len(self.image_label_pairs) - 1:
|
|
118
|
-
self.viewer.status =
|
|
145
|
+
self.viewer.status = (
|
|
146
|
+
"No more pairs to inspect. Inspection complete."
|
|
147
|
+
)
|
|
119
148
|
# should also clear the viewer
|
|
120
149
|
self.viewer.layers.clear()
|
|
121
|
-
return
|
|
150
|
+
return False # Return False to indicate we're at the end
|
|
122
151
|
|
|
123
152
|
# Move to the next pair
|
|
124
153
|
self.current_index += 1
|
|
125
154
|
|
|
126
155
|
# Load the next pair
|
|
127
156
|
self._load_current_pair()
|
|
157
|
+
return (
|
|
158
|
+
True # Return True to indicate successful navigation to next pair
|
|
159
|
+
)
|
|
128
160
|
|
|
129
161
|
|
|
130
162
|
@magicgui(
|
|
131
163
|
call_button="Start Label Inspection",
|
|
132
|
-
folder_path={"label": "Folder Path"},
|
|
133
|
-
|
|
134
|
-
label_suffix={"label": "Label Suffix (e.g., _labels.tif)"},
|
|
164
|
+
folder_path={"label": "Folder Path", "widget_type": "LineEdit"},
|
|
165
|
+
label_suffix={"label": "Label Suffix (e.g., _otsu_labels.tif)"},
|
|
135
166
|
)
|
|
136
167
|
def label_inspector(
|
|
137
168
|
folder_path: str,
|
|
138
|
-
image_suffix: str,
|
|
139
169
|
label_suffix: str,
|
|
140
170
|
viewer: Viewer,
|
|
141
171
|
):
|
|
@@ -143,11 +173,18 @@ def label_inspector(
|
|
|
143
173
|
MagicGUI widget for starting label inspection.
|
|
144
174
|
"""
|
|
145
175
|
inspector = LabelInspector(viewer)
|
|
146
|
-
inspector.load_image_label_pairs(folder_path,
|
|
176
|
+
inspector.load_image_label_pairs(folder_path, label_suffix)
|
|
147
177
|
|
|
148
178
|
# Add buttons for saving and continuing to the next pair
|
|
149
179
|
@magicgui(call_button="Save Changes and Continue")
|
|
150
180
|
def save_and_continue():
|
|
181
|
+
# Check if we're at the last pair before proceeding
|
|
182
|
+
if inspector.current_index >= len(inspector.image_label_pairs) - 1:
|
|
183
|
+
save_and_continue.call_button.enabled = False
|
|
184
|
+
inspector.viewer.status = (
|
|
185
|
+
"All pairs processed. Inspection complete."
|
|
186
|
+
)
|
|
187
|
+
return
|
|
151
188
|
inspector.next_pair()
|
|
152
189
|
|
|
153
190
|
viewer.window.add_dock_widget(save_and_continue)
|
|
@@ -157,4 +194,28 @@ def label_inspector_widget():
|
|
|
157
194
|
"""
|
|
158
195
|
Provide the label inspector widget to Napari
|
|
159
196
|
"""
|
|
160
|
-
|
|
197
|
+
# Create the magicgui widget
|
|
198
|
+
widget = label_inspector
|
|
199
|
+
|
|
200
|
+
# Create and add browse button
|
|
201
|
+
browse_button = QPushButton("Browse...")
|
|
202
|
+
|
|
203
|
+
def on_browse_clicked():
|
|
204
|
+
folder = QFileDialog.getExistingDirectory(
|
|
205
|
+
None,
|
|
206
|
+
"Select Folder",
|
|
207
|
+
os.path.expanduser("~"),
|
|
208
|
+
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks,
|
|
209
|
+
)
|
|
210
|
+
if folder:
|
|
211
|
+
# Update the folder_path field
|
|
212
|
+
widget.folder_path.value = folder
|
|
213
|
+
|
|
214
|
+
browse_button.clicked.connect(on_browse_clicked)
|
|
215
|
+
|
|
216
|
+
# Insert the browse button next to the folder_path field
|
|
217
|
+
# Find the folder_path widget and its layout
|
|
218
|
+
folder_layout = widget.folder_path.native.parent().layout()
|
|
219
|
+
folder_layout.addWidget(browse_button)
|
|
220
|
+
|
|
221
|
+
return widget
|