spacr 0.4.1__py3-none-any.whl → 0.4.2__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
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,6 +22,8 @@ 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
 
@@ -30,9 +31,19 @@ def preprocess_generate_masks(settings):
30
31
  source_folders = settings['src']
31
32
  for source_folder in source_folders:
32
33
  print(f'Processing folder: {source_folder}')
34
+
35
+ if settings['metadata_type'] == 'auto':
36
+ convert_to_yokogawa(folder=source_folder)
37
+
38
+ source_folder = format_path_for_system(source_folder)
33
39
  settings['src'] = source_folder
34
40
  src = source_folder
35
41
  settings = set_default_settings_preprocess_generate_masks(settings)
42
+
43
+ if settings['cell_channel'] is None and settings['nucleus_channel'] is None and settings['pathogen_channel'] is None:
44
+ print(f'Error: At least one of cell_channel, nucleus_channel or pathogen_channel must be defined')
45
+ return
46
+
36
47
  save_settings(settings, name='gen_mask_settings')
37
48
 
38
49
  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,136 @@ 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
+ npy_continue = _folder_has_images(path, image_extensions={".npy"})
917
+ conditions = [npy_continue]
918
+
919
+ if settings_type == 'recruitment':
920
+ db_continue = _folder_has_images(path, image_extensions={".db"})
921
+ conditions = [db_continue]
922
+
923
+ if settings_type == 'umap':
924
+ db_continue = _folder_has_images(path, image_extensions={".db"})
925
+ conditions = [db_continue]
926
+
927
+ if settings_type == 'analyze_plaques':
928
+ conditions = [True]
929
+
930
+ if settings_type == 'map_barcodes':
931
+ conditions = [True]
932
+
933
+ if settings_type == 'regression':
934
+ db_continue = _folder_has_images(path, image_extensions={".db"})
935
+ conditions = [db_continue]
936
+
937
+ if settings_type == 'classify':
938
+ db_continue = _folder_has_images(path, image_extensions={".db"})
939
+ conditions = [db_continue]
940
+
941
+ if settings_type == 'analyze_plaques':
942
+ db_continue = _folder_has_images(path, image_extensions={".db"})
943
+ conditions = [db_continue]
944
+
945
+ if not any(conditions):
946
+ q.put(f"Error: The following path(s) is missing images or folders: {path}")
947
+ request_stop = True
948
+
949
+ return request_stop
865
950
 
866
951
  def start_process(q=None, fig_queue=None, settings_type='mask'):
867
952
  global thread_control, vars_dict, parent_frame
868
953
  from .settings import check_settings, expected_types
869
954
  from .gui_utils import run_function_gui, set_cpu_affinity, initialize_cuda, display_gif_in_plot_frame, print_widget_structure
870
-
955
+
871
956
  if q is None:
872
957
  q = Queue()
873
958
  if fig_queue is None:
874
959
  fig_queue = Queue()
875
960
  try:
876
- settings = check_settings(vars_dict, expected_types, q)
961
+ settings, errors = check_settings(vars_dict, expected_types, q)
962
+
963
+ if len(errors) > 0:
964
+ return
965
+
966
+ if check_src_folders_files(settings, settings_type, q):
967
+ return
968
+
877
969
  except ValueError as e:
878
970
  q.put(f"Error: {e}")
879
971
  return
880
-
972
+
881
973
  if isinstance(thread_control, dict) and thread_control.get("run_thread") is not None:
882
974
  initiate_abort()
883
975
 
@@ -902,13 +994,176 @@ def start_process(q=None, fig_queue=None, settings_type='mask'):
902
994
 
903
995
  # Store the process in thread_control for future reference
904
996
  thread_control["run_thread"] = process
997
+
905
998
  else:
906
999
  q.put(f"Error: Unknown settings type '{settings_type}'")
907
1000
  return
908
-
1001
+
909
1002
  def process_console_queue():
910
1003
  global q, console_output, parent_frame, progress_bar, process_console_queue
911
1004
 
1005
+ # Initialize function attribute if it doesn't exist
1006
+ if not hasattr(process_console_queue, "completed_tasks"):
1007
+ process_console_queue.completed_tasks = []
1008
+ if not hasattr(process_console_queue, "current_maximum"):
1009
+ process_console_queue.current_maximum = None
1010
+
1011
+ ansi_escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
1012
+
1013
+ spacing = 5
1014
+
1015
+ # **Configure styles for different message types**
1016
+ console_output.tag_configure("error", foreground="red", spacing3 = spacing)
1017
+ console_output.tag_configure("warning", foreground="orange", spacing3 = spacing)
1018
+ console_output.tag_configure("normal", foreground="white", spacing3 = spacing)
1019
+
1020
+ while not q.empty():
1021
+ message = q.get_nowait()
1022
+ clean_message = ansi_escape_pattern.sub('', message)
1023
+
1024
+ # **Detect Error Messages (Red)**
1025
+ if clean_message.startswith("Error:"):
1026
+ console_output.insert(tk.END, clean_message + "\n", "error")
1027
+ console_output.see(tk.END)
1028
+ #print("Run aborted due to error:", clean_message) # Debug message
1029
+ #return # **Exit immediately to stop further execution**
1030
+
1031
+ # **Detect Warning Messages (Orange)**
1032
+ elif clean_message.startswith("Warning:"):
1033
+ console_output.insert(tk.END, clean_message + "\n", "warning")
1034
+
1035
+ # **Process Progress Messages Normally**
1036
+ elif clean_message.startswith("Progress:"):
1037
+ try:
1038
+ # Extract the progress information
1039
+ match = re.search(r'Progress: (\d+)/(\d+), operation_type: ([\w\s]*),(.*)', clean_message)
1040
+
1041
+ if match:
1042
+ current_progress = int(match.group(1))
1043
+ total_progress = int(match.group(2))
1044
+ operation_type = match.group(3).strip()
1045
+ additional_info = match.group(4).strip() # Capture everything after operation_type
1046
+
1047
+ # Check if the maximum value has changed
1048
+ if process_console_queue.current_maximum != total_progress:
1049
+ process_console_queue.current_maximum = total_progress
1050
+ process_console_queue.completed_tasks = []
1051
+
1052
+ # Add the task to the completed set
1053
+ process_console_queue.completed_tasks.append(current_progress)
1054
+
1055
+ # Calculate the unique progress count
1056
+ unique_progress_count = len(np.unique(process_console_queue.completed_tasks))
1057
+
1058
+ # Update the progress bar
1059
+ if progress_bar:
1060
+ progress_bar['maximum'] = total_progress
1061
+ progress_bar['value'] = unique_progress_count
1062
+
1063
+ # Store operation type and additional info
1064
+ if operation_type:
1065
+ progress_bar.operation_type = operation_type
1066
+ progress_bar.additional_info = additional_info
1067
+
1068
+ # Update the progress label
1069
+ if progress_bar.progress_label:
1070
+ progress_bar.update_label()
1071
+
1072
+ # Clear completed tasks when progress is complete
1073
+ if unique_progress_count >= total_progress:
1074
+ process_console_queue.completed_tasks.clear()
1075
+
1076
+ except Exception as e:
1077
+ print(f"Error parsing progress message: {e}")
1078
+
1079
+ # **Insert Normal Messages with Extra Line Spacing**
1080
+ else:
1081
+ console_output.insert(tk.END, clean_message + "\n", "normal")
1082
+
1083
+ console_output.see(tk.END)
1084
+
1085
+ # **Continue processing if no error was detected**
1086
+ after_id = console_output.after(uppdate_frequency, process_console_queue)
1087
+ parent_frame.after_tasks.append(after_id)
1088
+
1089
+ def process_console_queue_v2():
1090
+ global q, console_output, parent_frame, progress_bar, process_console_queue
1091
+
1092
+ # Initialize function attribute if it doesn't exist
1093
+ if not hasattr(process_console_queue, "completed_tasks"):
1094
+ process_console_queue.completed_tasks = []
1095
+ if not hasattr(process_console_queue, "current_maximum"):
1096
+ process_console_queue.current_maximum = None
1097
+
1098
+ ansi_escape_pattern = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
1099
+
1100
+ while not q.empty():
1101
+ message = q.get_nowait()
1102
+ clean_message = ansi_escape_pattern.sub('', message)
1103
+
1104
+ # **Abort Execution if an Error Message is Detected**
1105
+ if clean_message.startswith("Error:"):
1106
+ console_output.insert(tk.END, clean_message + "\n", "error")
1107
+ console_output.see(tk.END)
1108
+ print("Run aborted due to error:", clean_message) # Debug message
1109
+ return # **Exit immediately to stop further execution**
1110
+
1111
+ # Check if the message contains progress information
1112
+ if clean_message.startswith("Progress:"):
1113
+ try:
1114
+ # Extract the progress information
1115
+ match = re.search(r'Progress: (\d+)/(\d+), operation_type: ([\w\s]*),(.*)', clean_message)
1116
+
1117
+ if match:
1118
+ current_progress = int(match.group(1))
1119
+ total_progress = int(match.group(2))
1120
+ operation_type = match.group(3).strip()
1121
+ additional_info = match.group(4).strip() # Capture everything after operation_type
1122
+
1123
+ # Check if the maximum value has changed
1124
+ if process_console_queue.current_maximum != total_progress:
1125
+ process_console_queue.current_maximum = total_progress
1126
+ process_console_queue.completed_tasks = []
1127
+
1128
+ # Add the task to the completed set
1129
+ process_console_queue.completed_tasks.append(current_progress)
1130
+
1131
+ # Calculate the unique progress count
1132
+ unique_progress_count = len(np.unique(process_console_queue.completed_tasks))
1133
+
1134
+ # Update the progress bar
1135
+ if progress_bar:
1136
+ progress_bar['maximum'] = total_progress
1137
+ progress_bar['value'] = unique_progress_count
1138
+
1139
+ # Store operation type and additional info
1140
+ if operation_type:
1141
+ progress_bar.operation_type = operation_type
1142
+ progress_bar.additional_info = additional_info
1143
+
1144
+ # Update the progress label
1145
+ if progress_bar.progress_label:
1146
+ progress_bar.update_label()
1147
+
1148
+ # Clear completed tasks when progress is complete
1149
+ if unique_progress_count >= total_progress:
1150
+ process_console_queue.completed_tasks.clear()
1151
+
1152
+ except Exception as e:
1153
+ print(f"Error parsing progress message: {e}")
1154
+
1155
+ else:
1156
+ # Insert non-progress messages into the console
1157
+ console_output.insert(tk.END, clean_message + "\n")
1158
+ console_output.see(tk.END)
1159
+
1160
+ # **Continue processing if no error was detected**
1161
+ after_id = console_output.after(uppdate_frequency, process_console_queue)
1162
+ parent_frame.after_tasks.append(after_id)
1163
+
1164
+ def process_console_queue_v1():
1165
+ global q, console_output, parent_frame, progress_bar, process_console_queue
1166
+
912
1167
  # Initialize function attribute if it doesn't exist
913
1168
  if not hasattr(process_console_queue, "completed_tasks"):
914
1169
  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()