spacr 0.4.1__py3-none-any.whl → 0.4.3__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.
- spacr/core.py +31 -3
- spacr/gui_core.py +319 -46
- spacr/gui_elements.py +131 -0
- spacr/gui_utils.py +24 -20
- spacr/io.py +221 -8
- spacr/measure.py +11 -12
- spacr/settings.py +156 -48
- spacr/submodules.py +1 -1
- spacr/utils.py +99 -17
- {spacr-0.4.1.dist-info → spacr-0.4.3.dist-info}/METADATA +2 -1
- {spacr-0.4.1.dist-info → spacr-0.4.3.dist-info}/RECORD +15 -15
- {spacr-0.4.1.dist-info → spacr-0.4.3.dist-info}/LICENSE +0 -0
- {spacr-0.4.1.dist-info → spacr-0.4.3.dist-info}/WHEEL +0 -0
- {spacr-0.4.1.dist-info → spacr-0.4.3.dist-info}/entry_points.txt +0 -0
- {spacr-0.4.1.dist-info → spacr-0.4.3.dist-info}/top_level.txt +0 -0
spacr/core.py
CHANGED
@@ -9,12 +9,11 @@ warnings.filterwarnings("ignore", message="3D stack used, but stitch_threshold=0
|
|
9
9
|
|
10
10
|
def preprocess_generate_masks(settings):
|
11
11
|
|
12
|
-
from .io import preprocess_img_data, _load_and_concatenate_arrays
|
12
|
+
from .io import preprocess_img_data, _load_and_concatenate_arrays, convert_to_yokogawa, convert_separate_files_to_yokogawa
|
13
13
|
from .plot import plot_image_mask_overlay, plot_arrays
|
14
|
-
from .utils import _pivot_counts_table, check_mask_folder, adjust_cell_masks, print_progress, save_settings, delete_intermedeate_files
|
14
|
+
from .utils import _pivot_counts_table, check_mask_folder, adjust_cell_masks, print_progress, save_settings, delete_intermedeate_files, format_path_for_system, normalize_src_path
|
15
15
|
from .settings import set_default_settings_preprocess_generate_masks
|
16
16
|
|
17
|
-
|
18
17
|
if 'src' in settings:
|
19
18
|
if not isinstance(settings['src'], (str, list)):
|
20
19
|
ValueError(f'src must be a string or a list of strings')
|
@@ -23,16 +22,45 @@ def preprocess_generate_masks(settings):
|
|
23
22
|
ValueError(f'src is a required parameter')
|
24
23
|
return
|
25
24
|
|
25
|
+
settings['src'] = normalize_src_path(settings['src'])
|
26
|
+
|
26
27
|
if isinstance(settings['src'], str):
|
27
28
|
settings['src'] = [settings['src']]
|
28
29
|
|
29
30
|
if isinstance(settings['src'], list):
|
30
31
|
source_folders = settings['src']
|
31
32
|
for source_folder in source_folders:
|
33
|
+
|
32
34
|
print(f'Processing folder: {source_folder}')
|
35
|
+
|
36
|
+
if settings['metadata_type'] == 'auto':
|
37
|
+
if settings['custom_regex'] == None:
|
38
|
+
try:
|
39
|
+
convert_separate_files_to_yokogawa(folder=source_folder, regex=settings['custom_regex'])
|
40
|
+
except:
|
41
|
+
try:
|
42
|
+
convert_to_yokogawa(folder=source_folder)
|
43
|
+
except Exception as e:
|
44
|
+
print(f"Error: Tried to convert image files and image file name metadata with regex {settings['custom_regex']} then without regex but failed both.")
|
45
|
+
print(f'Error: {e}')
|
46
|
+
return
|
47
|
+
else:
|
48
|
+
try:
|
49
|
+
convert_to_yokogawa(folder=source_folder)
|
50
|
+
except Exception as e:
|
51
|
+
print(f"Error: Tried to convert image files and image file name metadata without regex but failed.")
|
52
|
+
print(f'Error: {e}')
|
53
|
+
return
|
54
|
+
|
55
|
+
source_folder = format_path_for_system(source_folder)
|
33
56
|
settings['src'] = source_folder
|
34
57
|
src = source_folder
|
35
58
|
settings = set_default_settings_preprocess_generate_masks(settings)
|
59
|
+
|
60
|
+
if settings['cell_channel'] is None and settings['nucleus_channel'] is None and settings['pathogen_channel'] is None:
|
61
|
+
print(f'Error: At least one of cell_channel, nucleus_channel or pathogen_channel must be defined')
|
62
|
+
return
|
63
|
+
|
36
64
|
save_settings(settings, name='gen_mask_settings')
|
37
65
|
|
38
66
|
if not settings['pathogen_channel'] is None:
|
spacr/gui_core.py
CHANGED
@@ -170,49 +170,22 @@ def display_figure(fig):
|
|
170
170
|
#flash_feedback("right")
|
171
171
|
show_next_figure()
|
172
172
|
|
173
|
-
def zoom_v1(event):
|
174
|
-
nonlocal scale_factor
|
175
|
-
|
176
|
-
zoom_speed = 0.1 # Adjust the zoom speed for smoother experience
|
177
|
-
|
178
|
-
# Determine the zoom direction based on the scroll event
|
179
|
-
if event.num == 4 or event.delta > 0: # Scroll up (zoom in)
|
180
|
-
scale_factor /= (1 + zoom_speed) # Divide to zoom in
|
181
|
-
elif event.num == 5 or event.delta < 0: # Scroll down (zoom out)
|
182
|
-
scale_factor *= (1 + zoom_speed) # Multiply to zoom out
|
183
|
-
|
184
|
-
# Adjust the axes limits based on the new scale factor
|
185
|
-
for ax in canvas.figure.get_axes():
|
186
|
-
xlim = ax.get_xlim()
|
187
|
-
ylim = ax.get_ylim()
|
188
|
-
|
189
|
-
x_center = (xlim[1] + xlim[0]) / 2
|
190
|
-
y_center = (ylim[1] + ylim[0]) / 2
|
191
|
-
|
192
|
-
x_range = (xlim[1] - xlim[0]) * scale_factor
|
193
|
-
y_range = (ylim[1] - ylim[0]) * scale_factor
|
194
|
-
|
195
|
-
# Set the new limits
|
196
|
-
ax.set_xlim([x_center - x_range / 2, x_center + x_range / 2])
|
197
|
-
ax.set_ylim([y_center - y_range / 2, y_center + y_range / 2])
|
198
|
-
|
199
|
-
# Redraw the figure efficiently
|
200
|
-
canvas.draw_idle()
|
201
|
-
|
202
173
|
def zoom_test(event):
|
203
174
|
if event.num == 4: # Scroll up
|
204
175
|
print("zoom in")
|
205
176
|
elif event.num == 5: # Scroll down
|
206
177
|
print("zoom out")
|
207
|
-
|
208
|
-
def
|
209
|
-
|
178
|
+
|
179
|
+
def zoom_v1(event):
|
180
|
+
# Fixed zoom factors (adjust these if you want faster or slower zoom)
|
181
|
+
zoom_in_factor = 0.9 # When zooming in, ranges shrink by 10%
|
182
|
+
zoom_out_factor = 1.1 # When zooming out, ranges increase by 10%
|
210
183
|
|
211
184
|
# Determine the zoom direction based on the scroll event
|
212
185
|
if event.num == 4 or (hasattr(event, 'delta') and event.delta > 0): # Scroll up = zoom in
|
213
|
-
factor =
|
186
|
+
factor = zoom_in_factor
|
214
187
|
elif event.num == 5 or (hasattr(event, 'delta') and event.delta < 0): # Scroll down = zoom out
|
215
|
-
factor =
|
188
|
+
factor = zoom_out_factor
|
216
189
|
else:
|
217
190
|
return # No recognized scroll direction
|
218
191
|
|
@@ -247,23 +220,28 @@ def display_figure(fig):
|
|
247
220
|
return # No recognized scroll direction
|
248
221
|
|
249
222
|
for ax in canvas.figure.get_axes():
|
223
|
+
# Get the current mouse position in pixel coordinates
|
224
|
+
mouse_x, mouse_y = event.x, event.y
|
225
|
+
|
226
|
+
# Convert pixel coordinates to data coordinates
|
227
|
+
inv = ax.transData.inverted()
|
228
|
+
data_x, data_y = inv.transform((mouse_x, mouse_y))
|
229
|
+
|
230
|
+
# Get the current axis limits
|
250
231
|
xlim = ax.get_xlim()
|
251
232
|
ylim = ax.get_ylim()
|
252
233
|
|
253
|
-
|
254
|
-
y_center = (ylim[1] + ylim[0]) / 2
|
255
|
-
|
234
|
+
# Calculate the zooming range around the cursor position
|
256
235
|
x_range = (xlim[1] - xlim[0]) * factor
|
257
236
|
y_range = (ylim[1] - ylim[0]) * factor
|
258
237
|
|
259
|
-
#
|
260
|
-
ax.set_xlim([
|
261
|
-
ax.set_ylim([
|
238
|
+
# Adjust the limits while keeping the mouse position fixed
|
239
|
+
ax.set_xlim([data_x - (data_x - xlim[0]) * factor, data_x + (xlim[1] - data_x) * factor])
|
240
|
+
ax.set_ylim([data_y - (data_y - ylim[0]) * factor, data_y + (ylim[1] - data_y) * factor])
|
262
241
|
|
263
242
|
# Redraw the figure efficiently
|
264
243
|
canvas.draw_idle()
|
265
244
|
|
266
|
-
|
267
245
|
# Bind events for hover, click interactions, and zoom
|
268
246
|
canvas_widget.bind("<Motion>", on_hover)
|
269
247
|
canvas_widget.bind("<Leave>", on_leave)
|
@@ -854,7 +832,7 @@ def initiate_abort():
|
|
854
832
|
global thread_control, q, parent_frame
|
855
833
|
if thread_control.get("run_thread") is not None:
|
856
834
|
try:
|
857
|
-
q.put("Aborting processes...")
|
835
|
+
#q.put("Aborting processes...")
|
858
836
|
thread_control.get("run_thread").terminate()
|
859
837
|
thread_control["run_thread"] = None
|
860
838
|
q.put("Processes aborted.")
|
@@ -862,22 +840,154 @@ def initiate_abort():
|
|
862
840
|
q.put(f"Error aborting process: {e}")
|
863
841
|
|
864
842
|
thread_control = {"run_thread": None, "stop_requested": False}
|
843
|
+
|
844
|
+
def check_src_folders_files(settings, settings_type, q):
|
845
|
+
"""
|
846
|
+
Checks if 'src' is a key in the settings dictionary and if it exists as a valid path.
|
847
|
+
If 'src' is a list, iterates through the list and checks each path.
|
848
|
+
If any path is missing, prompts the user to edit or remove invalid paths.
|
849
|
+
"""
|
850
|
+
|
851
|
+
request_stop = False
|
852
|
+
|
853
|
+
def _folder_has_images(folder_path, image_extensions = {".png", ".jpg", ".jpeg", ".bmp", ".gif", ".tiff", ".tif", ".webp", ".npy", ".npz", "nd2", "czi", "lif"}):
|
854
|
+
"""Check if a folder contains any image files."""
|
855
|
+
return any(file.lower().endswith(tuple(image_extensions)) for file in os.listdir(folder_path))
|
856
|
+
|
857
|
+
def _has_folder(parent_folder, sub_folder="measure"):
|
858
|
+
"""Check if a specific sub-folder exists inside the given folder."""
|
859
|
+
return os.path.isdir(os.path.join(parent_folder, sub_folder))
|
860
|
+
|
861
|
+
from .utils import normalize_src_path
|
862
|
+
|
863
|
+
settings['src'] = normalize_src_path(settings['src'])
|
864
|
+
|
865
|
+
src_value = settings.get("src")
|
866
|
+
|
867
|
+
# **Skip if 'src' is missing**
|
868
|
+
if src_value is None:
|
869
|
+
return request_stop
|
870
|
+
|
871
|
+
# Convert single string src to a list for uniform handling
|
872
|
+
if isinstance(src_value, str):
|
873
|
+
src_list = [src_value]
|
874
|
+
elif isinstance(src_value, list):
|
875
|
+
src_list = src_value
|
876
|
+
else:
|
877
|
+
request_stop = True
|
878
|
+
return request_stop # Ensure early exit
|
879
|
+
|
880
|
+
# Identify missing paths
|
881
|
+
missing_paths = {i: path for i, path in enumerate(src_list) if not os.path.exists(path)}
|
882
|
+
|
883
|
+
if missing_paths:
|
884
|
+
q.put(f'Error: The following paths are missing: {missing_paths}')
|
885
|
+
request_stop = True
|
886
|
+
return request_stop # Ensure early exit
|
887
|
+
|
888
|
+
conditions = [True] # Initialize conditions list
|
889
|
+
|
890
|
+
for path in src_list: # Fixed: Use src_list instead of src_value
|
891
|
+
if settings_type == 'mask':
|
892
|
+
pictures_continue = _folder_has_images(path)
|
893
|
+
folder_chan_continue = _has_folder(path, "1")
|
894
|
+
folder_stack_continue = _has_folder(path, "stack")
|
895
|
+
folder_npz_continue = _has_folder(path, "norm_channel_stack")
|
896
|
+
|
897
|
+
if not pictures_continue:
|
898
|
+
if not any([folder_chan_continue, folder_stack_continue, folder_npz_continue]):
|
899
|
+
if not folder_chan_continue:
|
900
|
+
q.put(f"Error: Missing channel folder in folder: {path}")
|
901
|
+
|
902
|
+
if not folder_stack_continue:
|
903
|
+
q.put(f"Error: Missing stack folder in folder: {path}")
|
904
|
+
|
905
|
+
if not folder_npz_continue:
|
906
|
+
q.put(f"Error: Missing norm_channel_stack folder in folder: {path}")
|
907
|
+
else:
|
908
|
+
q.put(f"Error: No images in folder: {path}")
|
909
|
+
|
910
|
+
#q.put(f"path:{path}")
|
911
|
+
#q.put(f"pictures_continue:{pictures_continue}, folder_chan_continue:{folder_chan_continue}, folder_stack_continue:{folder_stack_continue}, folder_npz_continue:{folder_npz_continue}")
|
912
|
+
|
913
|
+
conditions = [pictures_continue, folder_chan_continue, folder_stack_continue, folder_npz_continue]
|
914
|
+
|
915
|
+
if settings_type == 'measure':
|
916
|
+
if not os.path.basename(path) == 'merged':
|
917
|
+
path = os.path.join(path, "merged")
|
918
|
+
npy_continue = _folder_has_images(path, image_extensions={".npy"})
|
919
|
+
conditions = [npy_continue]
|
920
|
+
|
921
|
+
#if settings_type == 'recruitment':
|
922
|
+
# if not os.path.basename(path) == 'measurements':
|
923
|
+
# path = os.path.join(path, "measurements")
|
924
|
+
# db_continue = _folder_has_images(path, image_extensions={".db"})
|
925
|
+
# conditions = [db_continue]
|
926
|
+
|
927
|
+
#if settings_type == 'umap':
|
928
|
+
# if not os.path.basename(path) == 'measurements':
|
929
|
+
# path = os.path.join(path, "measurements")
|
930
|
+
# db_continue = _folder_has_images(path, image_extensions={".db"})
|
931
|
+
# conditions = [db_continue]
|
932
|
+
|
933
|
+
#if settings_type == 'analyze_plaques':
|
934
|
+
# if not os.path.basename(path) == 'measurements':
|
935
|
+
# path = os.path.join(path, "measurements")
|
936
|
+
# db_continue = _folder_has_images(path, image_extensions={".db"})
|
937
|
+
# conditions = [db_continue]
|
938
|
+
|
939
|
+
#if settings_type == 'map_barcodes':
|
940
|
+
# if not os.path.basename(path) == 'measurements':
|
941
|
+
# path = os.path.join(path, "measurements")
|
942
|
+
# db_continue = _folder_has_images(path, image_extensions={".db"})
|
943
|
+
# conditions = [db_continue]
|
944
|
+
|
945
|
+
#if settings_type == 'regression':
|
946
|
+
# if not os.path.basename(path) == 'measurements':
|
947
|
+
# path = os.path.join(path, "measurements")
|
948
|
+
# db_continue = _folder_has_images(path, image_extensions={".db"})
|
949
|
+
# conditions = [db_continue]
|
950
|
+
|
951
|
+
#if settings_type == 'classify':
|
952
|
+
# if not os.path.basename(path) == 'measurements':
|
953
|
+
# path = os.path.join(path, "measurements")
|
954
|
+
# db_continue = _folder_has_images(path, image_extensions={".db"})
|
955
|
+
# conditions = [db_continue]
|
956
|
+
|
957
|
+
#if settings_type == 'analyze_plaques':
|
958
|
+
# if not os.path.basename(path) == 'measurements':
|
959
|
+
# path = os.path.join(path, "measurements")
|
960
|
+
# db_continue = _folder_has_images(path, image_extensions={".db"})
|
961
|
+
# conditions = [db_continue]
|
962
|
+
|
963
|
+
if not any(conditions):
|
964
|
+
q.put(f"Error: The following path(s) is missing images or folders: {path}")
|
965
|
+
request_stop = True
|
966
|
+
|
967
|
+
return request_stop
|
865
968
|
|
866
969
|
def start_process(q=None, fig_queue=None, settings_type='mask'):
|
867
970
|
global thread_control, vars_dict, parent_frame
|
868
971
|
from .settings import check_settings, expected_types
|
869
972
|
from .gui_utils import run_function_gui, set_cpu_affinity, initialize_cuda, display_gif_in_plot_frame, print_widget_structure
|
870
|
-
|
973
|
+
|
871
974
|
if q is None:
|
872
975
|
q = Queue()
|
873
976
|
if fig_queue is None:
|
874
977
|
fig_queue = Queue()
|
875
978
|
try:
|
876
|
-
settings = check_settings(vars_dict, expected_types, q)
|
979
|
+
settings, errors = check_settings(vars_dict, expected_types, q)
|
980
|
+
|
981
|
+
if len(errors) > 0:
|
982
|
+
return
|
983
|
+
|
984
|
+
if check_src_folders_files(settings, settings_type, q):
|
985
|
+
return
|
986
|
+
|
877
987
|
except ValueError as e:
|
878
988
|
q.put(f"Error: {e}")
|
879
989
|
return
|
880
|
-
|
990
|
+
|
881
991
|
if isinstance(thread_control, dict) and thread_control.get("run_thread") is not None:
|
882
992
|
initiate_abort()
|
883
993
|
|
@@ -902,13 +1012,176 @@ def start_process(q=None, fig_queue=None, settings_type='mask'):
|
|
902
1012
|
|
903
1013
|
# Store the process in thread_control for future reference
|
904
1014
|
thread_control["run_thread"] = process
|
1015
|
+
|
905
1016
|
else:
|
906
1017
|
q.put(f"Error: Unknown settings type '{settings_type}'")
|
907
1018
|
return
|
908
|
-
|
1019
|
+
|
909
1020
|
def process_console_queue():
|
910
1021
|
global q, console_output, parent_frame, progress_bar, process_console_queue
|
911
1022
|
|
1023
|
+
# Initialize function attribute if it doesn't exist
|
1024
|
+
if not hasattr(process_console_queue, "completed_tasks"):
|
1025
|
+
process_console_queue.completed_tasks = []
|
1026
|
+
if not hasattr(process_console_queue, "current_maximum"):
|
1027
|
+
process_console_queue.current_maximum = None
|
1028
|
+
|
1029
|
+
ansi_escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
|
1030
|
+
|
1031
|
+
spacing = 5
|
1032
|
+
|
1033
|
+
# **Configure styles for different message types**
|
1034
|
+
console_output.tag_configure("error", foreground="red", spacing3 = spacing)
|
1035
|
+
console_output.tag_configure("warning", foreground="orange", spacing3 = spacing)
|
1036
|
+
console_output.tag_configure("normal", foreground="white", spacing3 = spacing)
|
1037
|
+
|
1038
|
+
while not q.empty():
|
1039
|
+
message = q.get_nowait()
|
1040
|
+
clean_message = ansi_escape_pattern.sub('', message)
|
1041
|
+
|
1042
|
+
# **Detect Error Messages (Red)**
|
1043
|
+
if clean_message.startswith("Error:"):
|
1044
|
+
console_output.insert(tk.END, clean_message + "\n", "error")
|
1045
|
+
console_output.see(tk.END)
|
1046
|
+
#print("Run aborted due to error:", clean_message) # Debug message
|
1047
|
+
#return # **Exit immediately to stop further execution**
|
1048
|
+
|
1049
|
+
# **Detect Warning Messages (Orange)**
|
1050
|
+
elif clean_message.startswith("Warning:"):
|
1051
|
+
console_output.insert(tk.END, clean_message + "\n", "warning")
|
1052
|
+
|
1053
|
+
# **Process Progress Messages Normally**
|
1054
|
+
elif clean_message.startswith("Progress:"):
|
1055
|
+
try:
|
1056
|
+
# Extract the progress information
|
1057
|
+
match = re.search(r'Progress: (\d+)/(\d+), operation_type: ([\w\s]*),(.*)', clean_message)
|
1058
|
+
|
1059
|
+
if match:
|
1060
|
+
current_progress = int(match.group(1))
|
1061
|
+
total_progress = int(match.group(2))
|
1062
|
+
operation_type = match.group(3).strip()
|
1063
|
+
additional_info = match.group(4).strip() # Capture everything after operation_type
|
1064
|
+
|
1065
|
+
# Check if the maximum value has changed
|
1066
|
+
if process_console_queue.current_maximum != total_progress:
|
1067
|
+
process_console_queue.current_maximum = total_progress
|
1068
|
+
process_console_queue.completed_tasks = []
|
1069
|
+
|
1070
|
+
# Add the task to the completed set
|
1071
|
+
process_console_queue.completed_tasks.append(current_progress)
|
1072
|
+
|
1073
|
+
# Calculate the unique progress count
|
1074
|
+
unique_progress_count = len(np.unique(process_console_queue.completed_tasks))
|
1075
|
+
|
1076
|
+
# Update the progress bar
|
1077
|
+
if progress_bar:
|
1078
|
+
progress_bar['maximum'] = total_progress
|
1079
|
+
progress_bar['value'] = unique_progress_count
|
1080
|
+
|
1081
|
+
# Store operation type and additional info
|
1082
|
+
if operation_type:
|
1083
|
+
progress_bar.operation_type = operation_type
|
1084
|
+
progress_bar.additional_info = additional_info
|
1085
|
+
|
1086
|
+
# Update the progress label
|
1087
|
+
if progress_bar.progress_label:
|
1088
|
+
progress_bar.update_label()
|
1089
|
+
|
1090
|
+
# Clear completed tasks when progress is complete
|
1091
|
+
if unique_progress_count >= total_progress:
|
1092
|
+
process_console_queue.completed_tasks.clear()
|
1093
|
+
|
1094
|
+
except Exception as e:
|
1095
|
+
print(f"Error parsing progress message: {e}")
|
1096
|
+
|
1097
|
+
# **Insert Normal Messages with Extra Line Spacing**
|
1098
|
+
else:
|
1099
|
+
console_output.insert(tk.END, clean_message + "\n", "normal")
|
1100
|
+
|
1101
|
+
console_output.see(tk.END)
|
1102
|
+
|
1103
|
+
# **Continue processing if no error was detected**
|
1104
|
+
after_id = console_output.after(uppdate_frequency, process_console_queue)
|
1105
|
+
parent_frame.after_tasks.append(after_id)
|
1106
|
+
|
1107
|
+
def process_console_queue_v2():
|
1108
|
+
global q, console_output, parent_frame, progress_bar, process_console_queue
|
1109
|
+
|
1110
|
+
# Initialize function attribute if it doesn't exist
|
1111
|
+
if not hasattr(process_console_queue, "completed_tasks"):
|
1112
|
+
process_console_queue.completed_tasks = []
|
1113
|
+
if not hasattr(process_console_queue, "current_maximum"):
|
1114
|
+
process_console_queue.current_maximum = None
|
1115
|
+
|
1116
|
+
ansi_escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
|
1117
|
+
|
1118
|
+
while not q.empty():
|
1119
|
+
message = q.get_nowait()
|
1120
|
+
clean_message = ansi_escape_pattern.sub('', message)
|
1121
|
+
|
1122
|
+
# **Abort Execution if an Error Message is Detected**
|
1123
|
+
if clean_message.startswith("Error:"):
|
1124
|
+
console_output.insert(tk.END, clean_message + "\n", "error")
|
1125
|
+
console_output.see(tk.END)
|
1126
|
+
print("Run aborted due to error:", clean_message) # Debug message
|
1127
|
+
return # **Exit immediately to stop further execution**
|
1128
|
+
|
1129
|
+
# Check if the message contains progress information
|
1130
|
+
if clean_message.startswith("Progress:"):
|
1131
|
+
try:
|
1132
|
+
# Extract the progress information
|
1133
|
+
match = re.search(r'Progress: (\d+)/(\d+), operation_type: ([\w\s]*),(.*)', clean_message)
|
1134
|
+
|
1135
|
+
if match:
|
1136
|
+
current_progress = int(match.group(1))
|
1137
|
+
total_progress = int(match.group(2))
|
1138
|
+
operation_type = match.group(3).strip()
|
1139
|
+
additional_info = match.group(4).strip() # Capture everything after operation_type
|
1140
|
+
|
1141
|
+
# Check if the maximum value has changed
|
1142
|
+
if process_console_queue.current_maximum != total_progress:
|
1143
|
+
process_console_queue.current_maximum = total_progress
|
1144
|
+
process_console_queue.completed_tasks = []
|
1145
|
+
|
1146
|
+
# Add the task to the completed set
|
1147
|
+
process_console_queue.completed_tasks.append(current_progress)
|
1148
|
+
|
1149
|
+
# Calculate the unique progress count
|
1150
|
+
unique_progress_count = len(np.unique(process_console_queue.completed_tasks))
|
1151
|
+
|
1152
|
+
# Update the progress bar
|
1153
|
+
if progress_bar:
|
1154
|
+
progress_bar['maximum'] = total_progress
|
1155
|
+
progress_bar['value'] = unique_progress_count
|
1156
|
+
|
1157
|
+
# Store operation type and additional info
|
1158
|
+
if operation_type:
|
1159
|
+
progress_bar.operation_type = operation_type
|
1160
|
+
progress_bar.additional_info = additional_info
|
1161
|
+
|
1162
|
+
# Update the progress label
|
1163
|
+
if progress_bar.progress_label:
|
1164
|
+
progress_bar.update_label()
|
1165
|
+
|
1166
|
+
# Clear completed tasks when progress is complete
|
1167
|
+
if unique_progress_count >= total_progress:
|
1168
|
+
process_console_queue.completed_tasks.clear()
|
1169
|
+
|
1170
|
+
except Exception as e:
|
1171
|
+
print(f"Error parsing progress message: {e}")
|
1172
|
+
|
1173
|
+
else:
|
1174
|
+
# Insert non-progress messages into the console
|
1175
|
+
console_output.insert(tk.END, clean_message + "\n")
|
1176
|
+
console_output.see(tk.END)
|
1177
|
+
|
1178
|
+
# **Continue processing if no error was detected**
|
1179
|
+
after_id = console_output.after(uppdate_frequency, process_console_queue)
|
1180
|
+
parent_frame.after_tasks.append(after_id)
|
1181
|
+
|
1182
|
+
def process_console_queue_v1():
|
1183
|
+
global q, console_output, parent_frame, progress_bar, process_console_queue
|
1184
|
+
|
912
1185
|
# Initialize function attribute if it doesn't exist
|
913
1186
|
if not hasattr(process_console_queue, "completed_tasks"):
|
914
1187
|
process_console_queue.completed_tasks = []
|
spacr/gui_elements.py
CHANGED
@@ -2285,6 +2285,9 @@ class AnnotateApp:
|
|
2285
2285
|
|
2286
2286
|
self.train_button = Button(self.button_frame,text="orig.",command=self.swich_back_annotation_column,bg=self.bg_color,fg=self.fg_color,highlightbackground=self.fg_color,highlightcolor=self.fg_color,highlightthickness=1)
|
2287
2287
|
self.train_button.pack(side="right", padx=5)
|
2288
|
+
|
2289
|
+
self.settings_button = Button(self.button_frame, text="Settings", command=self.open_settings_window, bg=self.bg_color, fg=self.fg_color, highlightbackground=self.fg_color,highlightcolor=self.fg_color,highlightthickness=1)
|
2290
|
+
self.settings_button.pack(side="right", padx=5)
|
2288
2291
|
|
2289
2292
|
# Calculate grid rows and columns based on the root window size and image size
|
2290
2293
|
self.calculate_grid_dimensions()
|
@@ -2308,6 +2311,134 @@ class AnnotateApp:
|
|
2308
2311
|
for col in range(self.grid_cols):
|
2309
2312
|
self.grid_frame.grid_columnconfigure(col, weight=1)
|
2310
2313
|
|
2314
|
+
def open_settings_window(self):
|
2315
|
+
from .gui_utils import generate_annotate_fields, convert_to_number
|
2316
|
+
|
2317
|
+
# Create settings window
|
2318
|
+
settings_window = tk.Toplevel(self.root)
|
2319
|
+
settings_window.title("Modify Annotation Settings")
|
2320
|
+
|
2321
|
+
style_out = set_dark_style(ttk.Style())
|
2322
|
+
settings_window.configure(bg=style_out['bg_color'])
|
2323
|
+
|
2324
|
+
settings_frame = tk.Frame(settings_window, bg=style_out['bg_color'])
|
2325
|
+
settings_frame.pack(fill=tk.BOTH, expand=True)
|
2326
|
+
|
2327
|
+
# Generate fields with current settings pre-filled
|
2328
|
+
vars_dict = generate_annotate_fields(settings_frame)
|
2329
|
+
|
2330
|
+
# Pre-fill the current settings into vars_dict
|
2331
|
+
current_settings = {
|
2332
|
+
'image_type': self.image_type or '',
|
2333
|
+
'channels': ','.join(self.channels) if self.channels else '',
|
2334
|
+
'img_size': f"{self.image_size[0]},{self.image_size[1]}",
|
2335
|
+
'annotation_column': self.annotation_column or '',
|
2336
|
+
'normalize': str(self.normalize),
|
2337
|
+
'percentiles': ','.join(map(str, self.percentiles)),
|
2338
|
+
'measurement': ','.join(self.measurement) if self.measurement else '',
|
2339
|
+
'threshold': str(self.threshold) if self.threshold is not None else '',
|
2340
|
+
'normalize_channels': ','.join(self.normalize_channels) if self.normalize_channels else ''
|
2341
|
+
}
|
2342
|
+
|
2343
|
+
for key, data in vars_dict.items():
|
2344
|
+
if key in current_settings:
|
2345
|
+
data['entry'].delete(0, tk.END)
|
2346
|
+
data['entry'].insert(0, current_settings[key])
|
2347
|
+
|
2348
|
+
def apply_new_settings():
|
2349
|
+
settings = {key: data['entry'].get() for key, data in vars_dict.items()}
|
2350
|
+
|
2351
|
+
# Process settings exactly as your original initiation function does
|
2352
|
+
settings['channels'] = settings['channels'].split(',') if settings['channels'] else None
|
2353
|
+
settings['img_size'] = list(map(int, settings['img_size'].split(',')))
|
2354
|
+
settings['percentiles'] = list(map(convert_to_number, settings['percentiles'].split(','))) if settings['percentiles'] else [1, 99]
|
2355
|
+
settings['normalize'] = settings['normalize'].lower() == 'true'
|
2356
|
+
settings['normalize_channels'] = settings['normalize_channels'].split(',') if settings['normalize_channels'] else None
|
2357
|
+
|
2358
|
+
try:
|
2359
|
+
settings['measurement'] = settings['measurement'].split(',') if settings['measurement'] else None
|
2360
|
+
settings['threshold'] = None if settings['threshold'].lower() == 'none' else int(settings['threshold'])
|
2361
|
+
except:
|
2362
|
+
settings['measurement'] = None
|
2363
|
+
settings['threshold'] = None
|
2364
|
+
|
2365
|
+
# Convert empty strings to None
|
2366
|
+
for key, value in settings.items():
|
2367
|
+
if isinstance(value, list):
|
2368
|
+
settings[key] = [v if v != '' else None for v in value]
|
2369
|
+
elif value == '':
|
2370
|
+
settings[key] = None
|
2371
|
+
|
2372
|
+
# Apply these settings dynamically using update_settings method
|
2373
|
+
self.update_settings(**{
|
2374
|
+
'image_type': settings.get('image_type'),
|
2375
|
+
'channels': settings.get('channels'),
|
2376
|
+
'image_size': settings.get('img_size'),
|
2377
|
+
'annotation_column': settings.get('annotation_column'),
|
2378
|
+
'normalize': settings.get('normalize'),
|
2379
|
+
'percentiles': settings.get('percentiles'),
|
2380
|
+
'measurement': settings.get('measurement'),
|
2381
|
+
'threshold': settings.get('threshold'),
|
2382
|
+
'normalize_channels': settings.get('normalize_channels')
|
2383
|
+
})
|
2384
|
+
|
2385
|
+
settings_window.destroy()
|
2386
|
+
|
2387
|
+
apply_button = spacrButton(settings_window, text="Apply Settings", command=apply_new_settings,show_text=False)
|
2388
|
+
apply_button.pack(pady=10)
|
2389
|
+
|
2390
|
+
def update_settings(self, **kwargs):
|
2391
|
+
allowed_attributes = {
|
2392
|
+
'image_type', 'channels', 'image_size', 'annotation_column',
|
2393
|
+
'normalize', 'percentiles', 'measurement', 'threshold', 'normalize_channels'
|
2394
|
+
}
|
2395
|
+
|
2396
|
+
updated = False
|
2397
|
+
|
2398
|
+
for attr, value in kwargs.items():
|
2399
|
+
if attr in allowed_attributes and value is not None:
|
2400
|
+
setattr(self, attr, value)
|
2401
|
+
updated = True
|
2402
|
+
|
2403
|
+
if 'image_size' in kwargs:
|
2404
|
+
if isinstance(self.image_size, list):
|
2405
|
+
self.image_size = (int(self.image_size[0]), int(self.image_size[0]))
|
2406
|
+
elif isinstance(self.image_size, int):
|
2407
|
+
self.image_size = (self.image_size, self.image_size)
|
2408
|
+
else:
|
2409
|
+
raise ValueError("Invalid image size")
|
2410
|
+
|
2411
|
+
self.calculate_grid_dimensions()
|
2412
|
+
self.recreate_image_grid()
|
2413
|
+
|
2414
|
+
if updated:
|
2415
|
+
current_index = self.index # Retain current index
|
2416
|
+
self.prefilter_paths_annotations()
|
2417
|
+
|
2418
|
+
# Ensure the retained index is still valid (not out of bounds)
|
2419
|
+
max_index = len(self.filtered_paths_annotations) - 1
|
2420
|
+
self.index = min(current_index, max_index := max(0, max(0, max(len(self.filtered_paths_annotations) - self.grid_rows * self.grid_cols, 0))))
|
2421
|
+
self.load_images()
|
2422
|
+
|
2423
|
+
def recreate_image_grid(self):
|
2424
|
+
# Remove current labels
|
2425
|
+
for label in self.labels:
|
2426
|
+
label.destroy()
|
2427
|
+
self.labels.clear()
|
2428
|
+
|
2429
|
+
# Recreate the labels grid with updated dimensions
|
2430
|
+
for i in range(self.grid_rows * self.grid_cols):
|
2431
|
+
label = Label(self.grid_frame, bg=self.root.cget('bg'))
|
2432
|
+
label.grid(row=i // self.grid_cols, column=i % self.grid_cols, padx=2, pady=2, sticky="nsew")
|
2433
|
+
self.labels.append(label)
|
2434
|
+
|
2435
|
+
# Reconfigure grid weights
|
2436
|
+
for row in range(self.grid_rows):
|
2437
|
+
self.grid_frame.grid_rowconfigure(row, weight=1)
|
2438
|
+
for col in range(self.grid_cols):
|
2439
|
+
self.grid_frame.grid_columnconfigure(col, weight=1)
|
2440
|
+
|
2441
|
+
|
2311
2442
|
def swich_back_annotation_column(self):
|
2312
2443
|
self.annotation_column = self.orig_annotation_columns
|
2313
2444
|
self.prefilter_paths_annotations()
|