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.
- napari_tmidas/_crop_anything.py +1895 -608
- napari_tmidas/_file_selector.py +87 -6
- napari_tmidas/_label_inspection.py +94 -47
- napari_tmidas/_version.py +2 -2
- napari_tmidas/processing_functions/basic.py +554 -23
- napari_tmidas/processing_functions/careamics_denoising.py +324 -0
- napari_tmidas/processing_functions/careamics_env_manager.py +339 -0
- napari_tmidas/processing_functions/cellpose_env_manager.py +55 -20
- napari_tmidas/processing_functions/cellpose_segmentation.py +105 -218
- napari_tmidas/processing_functions/sam2_env_manager.py +111 -0
- napari_tmidas/processing_functions/sam2_mp4.py +283 -0
- napari_tmidas/processing_functions/skimage_filters.py +323 -0
- napari_tmidas/processing_functions/timepoint_merger.py +490 -0
- napari_tmidas/processing_functions/trackastra_tracking.py +303 -0
- {napari_tmidas-0.1.9.dist-info → napari_tmidas-0.2.1.dist-info}/METADATA +15 -8
- {napari_tmidas-0.1.9.dist-info → napari_tmidas-0.2.1.dist-info}/RECORD +20 -14
- {napari_tmidas-0.1.9.dist-info → napari_tmidas-0.2.1.dist-info}/WHEEL +1 -1
- {napari_tmidas-0.1.9.dist-info → napari_tmidas-0.2.1.dist-info}/entry_points.txt +0 -0
- {napari_tmidas-0.1.9.dist-info → napari_tmidas-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {napari_tmidas-0.1.9.dist-info → napari_tmidas-0.2.1.dist-info}/top_level.txt +0 -0
napari_tmidas/_file_selector.py
CHANGED
|
@@ -585,7 +585,8 @@ class ProcessingWorker(QThread):
|
|
|
585
585
|
filepath = future_to_file[future]
|
|
586
586
|
try:
|
|
587
587
|
result = future.result()
|
|
588
|
-
if
|
|
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
|
-
#
|
|
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 =
|
|
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 {
|
|
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
|
|
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
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
#
|
|
57
|
+
# Process all potential label files
|
|
43
58
|
self.image_label_pairs = []
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.,
|
|
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