napari-tmidas 0.1.9__py3-none-any.whl → 0.2.1__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.
@@ -585,7 +585,8 @@ class ProcessingWorker(QThread):
585
585
  filepath = future_to_file[future]
586
586
  try:
587
587
  result = future.result()
588
- if result:
588
+ # Only process result if it's not None (folder functions may return None)
589
+ if result is not None:
589
590
  processed_files_info.append(result)
590
591
  self.file_processed.emit(result)
591
592
  except (
@@ -611,6 +612,16 @@ class ProcessingWorker(QThread):
611
612
 
612
613
  print(f"Original image shape: {image.shape}, dtype: {image_dtype}")
613
614
 
615
+ # Check if this is a folder-processing function that shouldn't save individual files
616
+ function_name = getattr(
617
+ self.processing_func, "__name__", "unknown"
618
+ )
619
+ is_folder_function = (
620
+ "timepoint" in function_name.lower()
621
+ or "merge" in function_name.lower()
622
+ or "folder" in function_name.lower()
623
+ )
624
+
614
625
  # Apply processing with parameters
615
626
  processed_image = self.processing_func(image, **self.param_values)
616
627
 
@@ -618,6 +629,19 @@ class ProcessingWorker(QThread):
618
629
  f"Processed image shape before removing singletons: {processed_image.shape}, dtype: {processed_image.dtype}"
619
630
  )
620
631
 
632
+ # For folder functions, check if the output is the same as input (indicating no individual file should be saved)
633
+ if is_folder_function:
634
+ # If the function returns the original image unchanged, it means it handled saving internally
635
+ if np.array_equal(processed_image, image):
636
+ print(
637
+ "Folder function returned unchanged image - skipping individual file save"
638
+ )
639
+ return None # Return None to indicate no file should be created
640
+ else:
641
+ print(
642
+ "Folder function returned different data - will save individual file"
643
+ )
644
+
621
645
  # Remove ALL singleton dimensions from the processed image
622
646
  # This will keep only dimensions with size > 1
623
647
  processed_image = np.squeeze(processed_image)
@@ -939,9 +963,49 @@ class FileResultsWidget(QWidget):
939
963
 
940
964
  # Update description
941
965
  description = function_info.get("description", "")
942
- self.function_description.setText(description)
943
966
 
944
- # Update parameters
967
+ # Check if this is a folder-processing function that needs single threading
968
+ is_folder_function = (
969
+ "folder" in function_name.lower()
970
+ or "timepoint" in function_name.lower()
971
+ or "merge" in function_name.lower()
972
+ or "folder" in description.lower()
973
+ or "cellpose" in description.lower()
974
+ or "careamics" in description.lower()
975
+ or "trackastra" in description.lower()
976
+ )
977
+
978
+ # Disable threading controls for folder functions
979
+ if is_folder_function:
980
+ self.thread_count.setValue(1)
981
+ self.thread_count.setEnabled(False)
982
+ self.thread_count.setToolTip(
983
+ "This function processes entire folders and must run with 1 thread only."
984
+ )
985
+
986
+ # Add warning to description if not already present
987
+ if (
988
+ "IMPORTANT:" not in description
989
+ and "WARNING:" not in description
990
+ ):
991
+ description += "\nThis function has to run single-threaded."
992
+
993
+ self.function_description.setText(description)
994
+
995
+ # Change the description color to make it more prominent
996
+ self.function_description.setStyleSheet(
997
+ "QLabel { color: #ff6b00; font-weight: bold; }"
998
+ )
999
+ else:
1000
+ # Re-enable threading controls for normal functions
1001
+ self.thread_count.setEnabled(True)
1002
+ self.thread_count.setToolTip(
1003
+ "Number of threads to use for parallel processing"
1004
+ )
1005
+ self.function_description.setStyleSheet("") # Reset styling
1006
+ self.function_description.setText(description)
1007
+
1008
+ # Get parameters
945
1009
  parameters = function_info.get("parameters", {})
946
1010
 
947
1011
  # Remove old parameters widget if it exists
@@ -1014,6 +1078,23 @@ class FileResultsWidget(QWidget):
1014
1078
  self.batch_button.setEnabled(False)
1015
1079
  self.cancel_button.setEnabled(True)
1016
1080
 
1081
+ # Set thread count based on function properties
1082
+ worker_thread_count = self.thread_count.value()
1083
+
1084
+ # Check if function should run single-threaded
1085
+ if (
1086
+ hasattr(processing_func, "thread_safe")
1087
+ and not processing_func.thread_safe
1088
+ ):
1089
+ worker_thread_count = 1
1090
+ self.viewer.status = (
1091
+ "Processing with a single thread (function is not thread-safe)"
1092
+ )
1093
+ else:
1094
+ self.viewer.status = (
1095
+ f"Processing with {worker_thread_count} threads"
1096
+ )
1097
+
1017
1098
  # Create and start the worker thread
1018
1099
  self.worker = ProcessingWorker(
1019
1100
  self.file_list,
@@ -1024,8 +1105,8 @@ class FileResultsWidget(QWidget):
1024
1105
  output_suffix,
1025
1106
  )
1026
1107
 
1027
- # Set the thread count from the UI
1028
- self.worker.thread_count = self.thread_count.value()
1108
+ # Set the thread count from the UI or function attribute
1109
+ self.worker.thread_count = worker_thread_count
1029
1110
 
1030
1111
  # Connect signals
1031
1112
  self.worker.progress_updated.connect(self.update_progress)
@@ -1037,7 +1118,7 @@ class FileResultsWidget(QWidget):
1037
1118
  self.worker.start()
1038
1119
 
1039
1120
  # Update status
1040
- self.viewer.status = f"Processing {len(self.file_list)} files with {selected_function_name} using {self.thread_count.value()} threads"
1121
+ self.viewer.status = f"Processing {len(self.file_list)} files with {selected_function_name} using {worker_thread_count} threads"
1041
1122
 
1042
1123
  def update_progress(self, value):
1043
1124
  """Update the progress bar"""
@@ -11,10 +11,11 @@ Users can make and save changes to the labels, and proceed to the next pair.
11
11
  import os
12
12
  import sys
13
13
 
14
+ import numpy as np
14
15
  from magicgui import magicgui
15
16
  from napari.layers import Labels
16
17
  from napari.viewer import Viewer
17
- from qtpy.QtWidgets import QFileDialog, QPushButton
18
+ from qtpy.QtWidgets import QFileDialog, QMessageBox, QPushButton
18
19
  from skimage.io import imread # , imsave
19
20
 
20
21
  sys.path.append("src/napari_tmidas")
@@ -29,63 +30,105 @@ class LabelInspector:
29
30
  def load_image_label_pairs(self, folder_path: str, label_suffix: str):
30
31
  """
31
32
  Load image-label pairs from a folder.
32
- Finds label files with the given suffix and matches them with their corresponding image files.
33
+ Finds all files with the given suffix and matches them with their corresponding image files.
34
+ Validates that label files are in the correct format.
33
35
  """
36
+ if not os.path.exists(folder_path) or not os.path.isdir(folder_path):
37
+ self.viewer.status = f"Folder path does not exist: {folder_path}"
38
+ return
39
+
34
40
  files = os.listdir(folder_path)
35
- label_files = [file for file in files if file.endswith(label_suffix)]
36
41
 
37
- # Extract the file extension (e.g., .tif)
38
- file_extension = (
39
- os.path.splitext(label_suffix)[-1] if "." in label_suffix else ""
40
- )
42
+ # Find all files that contain the label suffix
43
+ # Using "in" instead of "endswith" for more flexibility
44
+ potential_label_files = [
45
+ file for file in files if label_suffix in file
46
+ ]
47
+
48
+ if not potential_label_files:
49
+ self.viewer.status = f"No files found with suffix '{label_suffix}'"
50
+ QMessageBox.warning(
51
+ None,
52
+ "No Label Files Found",
53
+ f"No files containing '{label_suffix}' were found in {folder_path}.",
54
+ )
55
+ return
41
56
 
42
- # Modified matching logic
57
+ # Process all potential label files
43
58
  self.image_label_pairs = []
44
- for lbl in label_files:
45
- # Remove the label suffix to get the base name
46
- label_prefix = lbl[: -len(label_suffix)]
47
-
48
- # Potential corresponding image file
49
- img = f"{label_prefix}{file_extension}"
50
- img_path = os.path.join(folder_path, img)
51
-
52
- # Check if the image file exists
53
- if os.path.exists(img_path):
54
- self.image_label_pairs.append(
55
- (
56
- img_path,
57
- os.path.join(folder_path, lbl),
58
- )
59
- )
60
- continue
61
-
62
- # If not found, try finding any file that starts with the base name
59
+ skipped_files = []
60
+ format_issues = []
61
+
62
+ for label_file in potential_label_files:
63
+ label_path = os.path.join(folder_path, label_file)
64
+
65
+ # Get file extension
66
+ _, file_extension = os.path.splitext(label_file)
67
+
68
+ # Try to find a matching image file (everything before the label suffix)
69
+ base_name = label_file.split(label_suffix)[0]
70
+
71
+ # Look for potential images matching the base name
63
72
  potential_images = [
64
73
  file
65
74
  for file in files
66
- if file.startswith(label_prefix)
75
+ if file.startswith(base_name)
76
+ and file != label_file
67
77
  and file.endswith(file_extension)
68
- and file != lbl
69
78
  ]
70
79
 
80
+ # If we found at least one potential image
71
81
  if potential_images:
72
- # Use the first matching image
73
- self.image_label_pairs.append(
74
- (
75
- os.path.join(folder_path, potential_images[0]),
76
- os.path.join(folder_path, lbl),
77
- )
78
- )
79
-
80
- if not self.image_label_pairs:
81
- self.viewer.status = "No matching image-label pairs found."
82
- return
83
-
84
- self.viewer.status = (
85
- f"Found {len(self.image_label_pairs)} image-label pairs."
86
- )
87
- self.current_index = 0
88
- self._load_current_pair()
82
+ image_path = os.path.join(folder_path, potential_images[0])
83
+
84
+ # Validate label file format
85
+ try:
86
+ label_data = imread(label_path)
87
+
88
+ # Check if it looks like a label image (integer type)
89
+ if not np.issubdtype(label_data.dtype, np.integer):
90
+ format_issues.append(
91
+ (label_file, "not an integer type")
92
+ )
93
+ continue
94
+
95
+ # Add valid pair
96
+ self.image_label_pairs.append((image_path, label_path))
97
+
98
+ except (
99
+ FileNotFoundError,
100
+ OSError,
101
+ ValueError,
102
+ Exception,
103
+ ) as e:
104
+ skipped_files.append((label_file, str(e)))
105
+ else:
106
+ skipped_files.append((label_file, "no matching image found"))
107
+
108
+ # Report results
109
+ if self.image_label_pairs:
110
+ self.viewer.status = (
111
+ f"Found {len(self.image_label_pairs)} valid image-label pairs."
112
+ )
113
+ self.current_index = 0
114
+ self._load_current_pair()
115
+ else:
116
+ self.viewer.status = "No valid image-label pairs found."
117
+
118
+ # Show detailed report if there were issues
119
+ if skipped_files or format_issues:
120
+ msg = ""
121
+ if skipped_files:
122
+ msg += "Skipped files:\n"
123
+ for file, reason in skipped_files:
124
+ msg += f"- {file}: {reason}\n"
125
+
126
+ if format_issues:
127
+ msg += "\nFormat issues:\n"
128
+ for file, issue in format_issues:
129
+ msg += f"- {file}: {issue}\n"
130
+
131
+ QMessageBox.information(None, "Loading Report", msg)
89
132
 
90
133
  def _load_current_pair(self):
91
134
  """
@@ -110,6 +153,10 @@ class LabelInspector:
110
153
  label_image, name=f"Labels ({os.path.basename(label_path)})"
111
154
  )
112
155
 
156
+ # Show progress
157
+ total = len(self.image_label_pairs)
158
+ self.viewer.status = f"Viewing pair {self.current_index + 1} of {total}: {os.path.basename(image_path)}"
159
+
113
160
  def save_current_labels(self):
114
161
  """
115
162
  Save the current labels back to the original file.
@@ -172,7 +219,7 @@ class LabelInspector:
172
219
  @magicgui(
173
220
  call_button="Start Label Inspection",
174
221
  folder_path={"label": "Folder Path", "widget_type": "LineEdit"},
175
- label_suffix={"label": "Label Suffix (e.g., _otsu_labels.tif)"},
222
+ label_suffix={"label": "Label Suffix (e.g., _labels.tif)"},
176
223
  )
177
224
  def label_inspector(
178
225
  folder_path: str,
napari_tmidas/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.1.9'
21
- __version_tuple__ = version_tuple = (0, 1, 9)
20
+ __version__ = version = '0.2.1'
21
+ __version_tuple__ = version_tuple = (0, 2, 1)