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.
@@ -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 = tifffile.imread(filepath)
144
- self.current_original_image = self.viewer.add_image(
145
- image, name=f"Original: {os.path.basename(filepath)}"
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 = tifffile.imread(filepath)
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={"label": "Select Folder"},
267
- input_suffix={"label": "File Suffix (Example: _labels.tif)", "value": ""},
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 = "_labels.tif"
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
- layout.addWidget(self.batch_button)
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
- # 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}")
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
- # Update table with processed files
508
- self.table.update_processed_files(processed_files_info)
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
- # Update viewer status
511
- self.viewer.status = f"Processed {len(processed_files_info)} files with {selected_function_name}"
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 skimage.io import imread, imsave
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 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
- )
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
- break # Match found, move to next image
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("uint16"))
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 = "No more pairs to inspect."
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
- image_suffix={"label": "Image Suffix (e.g., .tif)"},
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, image_suffix, label_suffix)
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
- return label_inspector
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