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 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 zoom_2(event):
209
- zoom_speed = 0.1 # Change this to control how fast you zoom
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 = 1 - zoom_speed
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 = 1 + zoom_speed
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
- x_center = (xlim[1] + xlim[0]) / 2
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
- # Set the new limits
260
- ax.set_xlim([x_center - x_range / 2, x_center + x_range / 2])
261
- ax.set_ylim([y_center - y_range / 2, y_center + y_range / 2])
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()