spacr 0.4.0__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/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()
spacr/gui_utils.py CHANGED
@@ -106,7 +106,6 @@ def parse_list(value):
106
106
  except (ValueError, SyntaxError) as e:
107
107
  raise ValueError(f"Invalid format for list: {value}. Error: {e}")
108
108
 
109
- # Usage example in your create_input_field function
110
109
  def create_input_field(frame, label_text, row, var_type='entry', options=None, default_value=None):
111
110
  """
112
111
  Create an input field in the specified frame.
@@ -365,27 +364,30 @@ def convert_settings_dict_for_gui(settings):
365
364
  from torchvision import models as torch_models
366
365
  torchvision_models = [name for name, obj in torch_models.__dict__.items() if callable(obj)]
367
366
  chans = ['0', '1', '2', '3', '4', '5', '6', '7', '8', None]
367
+ chan_list = ['[0,1,2,3,4,5,6,7,8]','[0,1,2,3,4,5,6,7]','[0,1,2,3,4,5,6]','[0,1,2,3,4,5]','[0,1,2,3,4]','[0,1,2,3]', '[0,1,2]', '[0,1]', '[0]', '[0,0]']
368
368
  chans_v2 = [0, 1, 2, 3, None]
369
+ chans_v3 = list(range(0, 21, 1)) + [None]
370
+ chans_v4 = [0, 1, 2, 3, None]
369
371
  variables = {}
370
372
  special_cases = {
371
- 'metadata_type': ('combo', ['cellvoyager', 'cq1', 'nikon', 'zeis', 'custom'], 'cellvoyager'),
372
- 'channels': ('combo', ['[0,1,2,3]', '[0,1,2]', '[0,1]', '[0]', '[0,0]'], '[0,1,2,3]'),
373
+ 'metadata_type': ('combo', ['cellvoyager', 'cq1', 'auto', 'custom'], 'cellvoyager'),
374
+ 'channels': ('combo', chan_list, '[0,1,2,3]'),
373
375
  'train_channels': ('combo', ["['r','g','b']", "['r','g']", "['r','b']", "['g','b']", "['r']", "['g']", "['b']"], "['r','g','b']"),
374
- 'channel_dims': ('combo', ['[0,1,2,3]', '[0,1,2]', '[0,1]', '[0]'], '[0,1,2,3]'),
376
+ 'channel_dims': ('combo', chan_list, '[0,1,2,3]'),
375
377
  'dataset_mode': ('combo', ['annotation', 'metadata', 'recruitment'], 'metadata'),
376
378
  'cov_type': ('combo', ['HC0', 'HC1', 'HC2', 'HC3', None], None),
377
- 'cell_mask_dim': ('combo', chans, None),
378
- 'cell_chann_dim': ('combo', chans, None),
379
- 'nucleus_mask_dim': ('combo', chans, None),
380
- 'nucleus_chann_dim': ('combo', chans, None),
381
- 'pathogen_mask_dim': ('combo', chans, None),
382
- 'pathogen_chann_dim': ('combo', chans, None),
383
- 'crop_mode': ('combo', [['cell'], ['nucleus'], ['pathogen'], ['cell', 'nucleus'], ['cell', 'pathogen'], ['nucleus', 'pathogen'], ['cell', 'nucleus', 'pathogen']], ['cell']),
384
- 'magnification': ('combo', [20, 40, 60], 20),
385
- 'nucleus_channel': ('combo', chans_v2, None),
386
- 'cell_channel': ('combo', chans_v2, None),
387
- 'channel_of_interest': ('combo', chans_v2, None),
388
- 'pathogen_channel': ('combo', chans_v2, None),
379
+ #'cell_mask_dim': ('combo', chans_v3, None),
380
+ #'cell_chann_dim': ('combo', chans_v3, None),
381
+ #'nucleus_mask_dim': ('combo', chans_v3, None),
382
+ #'nucleus_chann_dim': ('combo', chans_v3, None),
383
+ #'pathogen_mask_dim': ('combo', chans_v3, None),
384
+ #'pathogen_chann_dim': ('combo', chans_v3, None),
385
+ 'crop_mode': ('combo', ["['cell']", "['nucleus']", "['pathogen']", "['cell', 'nucleus']", "['cell', 'pathogen']", "['nucleus', 'pathogen']", "['cell', 'nucleus', 'pathogen']"], "['cell']"),
386
+ #'magnification': ('combo', [20, 40, 60], 20),
387
+ #'nucleus_channel': ('combo', chans_v3, None),
388
+ #'cell_channel': ('combo', chans_v3, None),
389
+ #'channel_of_interest': ('combo', chans_v3, None),
390
+ #'pathogen_channel': ('combo', chans_v3, None),
389
391
  'timelapse_mode': ('combo', ['trackpy', 'btrack'], 'trackpy'),
390
392
  'train_mode': ('combo', ['erm', 'irm'], 'erm'),
391
393
  'clustering': ('combo', ['dbscan', 'kmean'], 'dbscan'),
@@ -462,10 +464,11 @@ def function_gui_wrapper(function=None, settings={}, q=None, fig_queue=None, imp
462
464
  finally:
463
465
  # Restore the original plt.show function
464
466
  plt.show = original_show
467
+
465
468
 
469
+
466
470
  def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
467
471
 
468
- from .gui_utils import process_stdout_stderr
469
472
  from .core import generate_image_umap, preprocess_generate_masks
470
473
  from .cellpose import identify_masks_finetune, check_cellpose_models, compare_cellpose_masks
471
474
  from .submodules import analyze_recruitment
@@ -476,9 +479,10 @@ def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
476
479
  from .sim import run_multiple_simulations
477
480
  from .deep_spacr import deep_spacr, apply_model_to_tar
478
481
  from .sequencing import generate_barecode_mapping
482
+
479
483
  process_stdout_stderr(q)
480
-
481
- print(f'run_function_gui settings_type: {settings_type}')
484
+
485
+ print(f'run_function_gui settings_type: {settings_type}')
482
486
 
483
487
  if settings_type == 'mask':
484
488
  function = preprocess_generate_masks
@@ -523,7 +527,7 @@ def run_function_gui(settings_type, settings, q, fig_queue, stop_requested):
523
527
  function = process_non_tif_non_2D_images
524
528
  imports = 1
525
529
  else:
526
- raise ValueError(f"Invalid settings type: {settings_type}")
530
+ raise ValueError(f"Error: Invalid settings type: {settings_type}")
527
531
  try:
528
532
  function_gui_wrapper(function, settings, q, fig_queue, imports)
529
533
  except Exception as e:
spacr/io.py CHANGED
@@ -891,11 +891,16 @@ def _merge_channels(src, plot=False):
891
891
  from .utils import print_progress
892
892
 
893
893
  stack_dir = os.path.join(src, 'stack')
894
- allowed_names = ['01', '02', '03', '04', '00', '1', '2', '3', '4', '0']
894
+ #allowed_names = ['01', '02', '03', '04', '00', '1', '2', '3', '4', '0']
895
+
896
+ string_list = [str(i) for i in range(101)]+[f"{i:02d}" for i in range(10)]
897
+ allowed_names = sorted(string_list, key=lambda x: int(x))
895
898
 
896
899
  # List directories that match the allowed names
897
900
  chan_dirs = [d for d in os.listdir(src) if os.path.isdir(os.path.join(src, d)) and d in allowed_names]
898
901
  chan_dirs.sort()
902
+
903
+ num_matching_folders = len(chan_dirs)
899
904
 
900
905
  print(f'List of folders in src: {chan_dirs}. Single channel folders.')
901
906
 
@@ -925,7 +930,7 @@ def _merge_channels(src, plot=False):
925
930
  if plot:
926
931
  plot_arrays(os.path.join(src, 'stack'))
927
932
 
928
- return
933
+ return num_matching_folders
929
934
 
930
935
  def _mip_all(src, include_first_chan=True):
931
936
 
@@ -1584,10 +1589,6 @@ def preprocess_img_data(settings):
1584
1589
  else:
1585
1590
  print(f'Could not find any {valid_ext} files in {src} only found {extension_counts[0]}')
1586
1591
 
1587
-
1588
-
1589
-
1590
-
1591
1592
  if os.path.exists(os.path.join(src,'stack')):
1592
1593
  print('Found existing stack folder.')
1593
1594
  if os.path.exists(os.path.join(src,'channel_stack')):
@@ -1644,7 +1645,13 @@ def preprocess_img_data(settings):
1644
1645
  print(f"all images: {all_imgs}, full batch: {full_batches}, last batch: {last_batch_size}")
1645
1646
  raise ValueError("Last batch of size 1 detected. Adjust the batch size.")
1646
1647
 
1647
- _merge_channels(src, plot=False)
1648
+ nr_channel_folders = _merge_channels(src, plot=False)
1649
+
1650
+ if len(settings['channels']) != nr_channel_folders:
1651
+ print(f"Number of channels does not match number of channel folders. channels: {settings['channels']} channel folders: {nr_channel_folders}")
1652
+ new_channels = list(range(nr_channel_folders))
1653
+ print(f"Changing channels from {settings['channels']} to {new_channels}")
1654
+ settings['channels'] = new_channels
1648
1655
 
1649
1656
  if timelapse:
1650
1657
  _create_movies_from_npy_per_channel(stack_path, fps=2)
@@ -3095,4 +3102,301 @@ def generate_dataset_from_lists(dst, class_data, classes, test_split=0.1):
3095
3102
  test_class_dir = os.path.join(dst, f'test/{cls}')
3096
3103
  print(f'Train class {cls}: {len(os.listdir(train_class_dir))}, Test class {cls}: {len(os.listdir(test_class_dir))}')
3097
3104
 
3098
- return os.path.join(dst, 'train'), os.path.join(dst, 'test')
3105
+ return os.path.join(dst, 'train'), os.path.join(dst, 'test')
3106
+
3107
+ def convert_to_yokogawa_v1(folder):
3108
+ """
3109
+ Detects file type in the folder and converts them
3110
+ to Yokogawa-style naming with Maximum Intensity Projection (MIP).
3111
+ """
3112
+
3113
+ def _get_next_well(used_wells):
3114
+ """
3115
+ Determines the next available well position in a 384-well format.
3116
+ Iterates wells, and after P24, switches to plate2.
3117
+ """
3118
+ plate = 1
3119
+ for well in WELLS:
3120
+ well_name = f"plate{plate}_{well}"
3121
+ if well_name not in used_wells:
3122
+ return well_name
3123
+ if well == "P24":
3124
+ plate += 1
3125
+ return f"plate{plate}_A01"
3126
+
3127
+ # Define 384-well plate format
3128
+ ROWS = "ABCDEFGHIJKLMNOP"
3129
+ COLS = [f"{i:02d}" for i in range(1, 25)]
3130
+ WELLS = [f"{r}{c}" for r in ROWS for c in COLS]
3131
+
3132
+ filenames = []
3133
+ rename_log = []
3134
+ csv_path = os.path.join(folder, "rename_log.csv")
3135
+ used_wells = set(os.listdir(folder))
3136
+
3137
+ for file in os.listdir(folder):
3138
+ path = os.path.join(folder, file)
3139
+ ext = file.lower().split('.')[-1]
3140
+
3141
+ ### **Process Nikon ND2 Files**
3142
+ if ext == 'nd2':
3143
+ nd2 = ND2Reader(path)
3144
+ metadata = nd2.metadata
3145
+
3146
+ timepoints = metadata.get("frames", [0])
3147
+ fields = metadata.get("fields_of_view", [0])
3148
+ z_levels = list(metadata.get("z_levels", range(1)))
3149
+ channels = metadata.get("channels", [])
3150
+
3151
+ for t_idx in timepoints:
3152
+ for f_idx in fields:
3153
+ for c_idx, channel in enumerate(channels):
3154
+ well = _get_next_well(used_wells)
3155
+
3156
+ z_stack = [nd2.get_frame_2D(t=t_idx, v=f_idx, z=z_idx, c=c_idx) for z_idx in z_levels]
3157
+ mip_image = np.max(np.stack(z_stack), axis=0)
3158
+ dtype = z_stack[0].dtype
3159
+
3160
+ filename = f"{well}_T{t_idx+1:04d}F{f_idx+1:03d}L01C{c_idx+1:02d}.tif"
3161
+ filepath = os.path.join(folder, filename)
3162
+ tifffile.imwrite(filepath, mip_image.astype(dtype))
3163
+ used_wells.add(well)
3164
+ rename_log.append({"Original File": file, "Renamed TIFF": filename})
3165
+
3166
+ ### **Process Zeiss CZI Files**
3167
+ elif ext == 'czi':
3168
+ with czifile.CziFile(path) as czi:
3169
+ shape = czi.shape
3170
+
3171
+ timepoints = range(shape[0])
3172
+ z_levels = range(shape[1])
3173
+ channels = range(shape[2])
3174
+
3175
+ for t_idx in timepoints:
3176
+ for c_idx in channels:
3177
+ well = _get_next_well(used_wells)
3178
+
3179
+ z_stack = [czi.asarray()[t_idx, z_idx, c_idx] for z_idx in z_levels]
3180
+ mip_image = np.max(np.stack(z_stack), axis=0)
3181
+ dtype = z_stack[0].dtype
3182
+
3183
+ filename = f"{well}_T{t_idx+1:04d}F001L01C{c_idx+1:02d}.tif"
3184
+ filepath = os.path.join(folder, filename)
3185
+ tifffile.imwrite(filepath, mip_image.astype(dtype))
3186
+ used_wells.add(well)
3187
+ rename_log.append({"Original File": file, "Renamed TIFF": filename})
3188
+
3189
+ ### **Process Leica LIF Files**
3190
+ elif ext == 'lif':
3191
+ lif_file = readlif.Reader(path)
3192
+
3193
+ for image in lif_file.getIterImage():
3194
+ timepoints = range(image.dims.t)
3195
+ z_levels = range(image.dims.z)
3196
+ channels = range(image.dims.c)
3197
+
3198
+ for t_idx in timepoints:
3199
+ for c_idx in channels:
3200
+ well = _get_next_well(used_wells)
3201
+
3202
+ z_stack = [image.getFrame(z=z_idx, t=t_idx, c=c_idx) for z_idx in z_levels]
3203
+ mip_image = np.max(np.stack(z_stack), axis=0)
3204
+ dtype = z_stack[0].dtype
3205
+
3206
+ filename = f"{well}_T{t_idx+1:04d}F001L01C{c_idx+1:02d}.tif"
3207
+ filepath = os.path.join(folder, filename)
3208
+ tifffile.imwrite(filepath, mip_image.astype(dtype))
3209
+ used_wells.add(well)
3210
+ rename_log.append({"Original File": file, "Renamed TIFF": filename})
3211
+
3212
+ ### **Process Standard Images (.tif, .tiff, .png, .jpg, .bmp, etc.)**
3213
+ elif ext in ['tif', 'tiff', 'png', 'jpg', 'jpeg', 'bmp'] and not file.startswith("plate"):
3214
+ with tifffile.TiffFile(path) as tif:
3215
+ well = _get_next_well(used_wells)
3216
+ num_pages = len(tif.pages)
3217
+
3218
+ if num_pages > 1:
3219
+ # Assume multi-channel or z-stack
3220
+ images = tif.asarray()
3221
+ if images.ndim == 4: # (T, Z, C, Y, X)
3222
+ timepoints = range(images.shape[0])
3223
+ z_levels = range(images.shape[1])
3224
+ channels = range(images.shape[2])
3225
+
3226
+ for t_idx in timepoints:
3227
+ for c_idx in channels:
3228
+ mip_image = np.max(images[t_idx, :, c_idx], axis=0)
3229
+ dtype = images.dtype
3230
+
3231
+ filename = f"{well}_T{t_idx+1:04d}F001L01C{c_idx+1:02d}.tif"
3232
+ filepath = os.path.join(folder, filename)
3233
+ tifffile.imwrite(filepath, mip_image.astype(dtype))
3234
+ used_wells.add(well)
3235
+ rename_log.append({"Original File": file, "Renamed TIFF": filename})
3236
+
3237
+ elif images.ndim == 3: # (Z, Y, X) or (C, Y, X)
3238
+ z_stack = images if images.shape[0] > 1 else [images]
3239
+ mip_image = np.max(np.stack(z_stack), axis=0)
3240
+ dtype = images.dtype
3241
+
3242
+ filename = f"{well}_T0001F001L01C01.tif"
3243
+ filepath = os.path.join(folder, filename)
3244
+ tifffile.imwrite(filepath, mip_image.astype(dtype))
3245
+ used_wells.add(well)
3246
+ rename_log.append({"Original File": file, "Renamed TIFF": filename})
3247
+ else:
3248
+ image = tif.pages[0].asarray()
3249
+ dtype = image.dtype
3250
+
3251
+ filename = f"{well}_T0001F001L01C01.tif"
3252
+ filepath = os.path.join(folder, filename)
3253
+ tifffile.imwrite(filepath, image.astype(dtype))
3254
+ used_wells.add(well)
3255
+ rename_log.append({"Original File": file, "Renamed TIFF": filename})
3256
+
3257
+ # Save rename log as CSV
3258
+ pd.DataFrame(rename_log).to_csv(csv_path, index=False)
3259
+ print(f"Processing complete. Files saved in {folder} and rename log saved as {csv_path}.")
3260
+
3261
+ def convert_to_yokogawa(folder):
3262
+ """
3263
+ Detects file type in the folder and converts them
3264
+ to Yokogawa-style naming with Maximum Intensity Projection (MIP).
3265
+ """
3266
+
3267
+ def _get_next_well(used_wells):
3268
+ """
3269
+ Determines the next available well position in a 384-well format.
3270
+ Iterates wells, and after P24, switches to plate2.
3271
+ """
3272
+ plate = 1
3273
+ for well in WELLS:
3274
+ well_name = f"plate{plate}_{well}"
3275
+ if well_name not in used_wells:
3276
+ return well_name
3277
+ if well == "P24":
3278
+ plate += 1
3279
+ return f"plate{plate}_A01"
3280
+
3281
+ # Define 384-well plate format
3282
+ ROWS = "ABCDEFGHIJKLMNOP"
3283
+ COLS = [f"{i:02d}" for i in range(1, 25)]
3284
+ WELLS = [f"{r}{c}" for r in ROWS for c in COLS]
3285
+
3286
+ filenames = []
3287
+ rename_log = []
3288
+ csv_path = os.path.join(folder, "rename_log.csv")
3289
+ used_wells = set(os.listdir(folder))
3290
+
3291
+ # **Dictionary to store well assignments per original file**
3292
+ file_to_well = {}
3293
+
3294
+ for file in os.listdir(folder):
3295
+ path = os.path.join(folder, file)
3296
+ ext = file.lower().split('.')[-1]
3297
+
3298
+ # **Assign a well only once per original file**
3299
+ if file not in file_to_well:
3300
+ file_to_well[file] = _get_next_well(used_wells)
3301
+ used_wells.add(file_to_well[file]) # Mark it as used
3302
+
3303
+ well = file_to_well[file] # Use the same well for all channels/times
3304
+
3305
+ ### **Process Nikon ND2 Files**
3306
+ if ext == 'nd2':
3307
+ nd2 = ND2Reader(path)
3308
+ metadata = nd2.metadata
3309
+
3310
+ timepoints = metadata.get("frames", [0])
3311
+ fields = metadata.get("fields_of_view", [0])
3312
+ z_levels = list(metadata.get("z_levels", range(1)))
3313
+ channels = metadata.get("channels", [])
3314
+
3315
+ for t_idx in timepoints:
3316
+ for f_idx in fields:
3317
+ for c_idx, channel in enumerate(channels):
3318
+ z_stack = [nd2.get_frame_2D(t=t_idx, v=f_idx, z=z_idx, c=c_idx) for z_idx in z_levels]
3319
+ mip_image = np.max(np.stack(z_stack), axis=0)
3320
+ dtype = z_stack[0].dtype
3321
+
3322
+ filename = f"{well}_T{t_idx+1:04d}F{f_idx+1:03d}L01C{c_idx+1:02d}.tif"
3323
+ filepath = os.path.join(folder, filename)
3324
+ tifffile.imwrite(filepath, mip_image.astype(dtype))
3325
+
3326
+ rename_log.append({"Original File": file, "Renamed TIFF": filename})
3327
+
3328
+ ### **Process Zeiss CZI Files**
3329
+ elif ext == 'czi':
3330
+ with czifile.CziFile(path) as czi:
3331
+ shape = czi.shape
3332
+ timepoints = range(shape[0])
3333
+ z_levels = range(shape[1])
3334
+ channels = range(shape[2])
3335
+
3336
+ for t_idx in timepoints:
3337
+ for c_idx in channels:
3338
+ z_stack = [czi.asarray()[t_idx, z_idx, c_idx] for z_idx in z_levels]
3339
+ mip_image = np.max(np.stack(z_stack), axis=0)
3340
+ dtype = z_stack[0].dtype
3341
+
3342
+ filename = f"{well}_T{t_idx+1:04d}F001L01C{c_idx+1:02d}.tif"
3343
+ filepath = os.path.join(folder, filename)
3344
+ tifffile.imwrite(filepath, mip_image.astype(dtype))
3345
+
3346
+ rename_log.append({"Original File": file, "Renamed TIFF": filename})
3347
+
3348
+ ### **Process Leica LIF Files**
3349
+ elif ext == 'lif':
3350
+ lif_file = readlif.Reader(path)
3351
+
3352
+ for image in lif_file.getIterImage():
3353
+ timepoints = range(image.dims.t)
3354
+ z_levels = range(image.dims.z)
3355
+ channels = range(image.dims.c)
3356
+
3357
+ for t_idx in timepoints:
3358
+ for c_idx in channels:
3359
+ z_stack = [image.getFrame(z=z_idx, t=t_idx, c=c_idx) for z_idx in z_levels]
3360
+ mip_image = np.max(np.stack(z_stack), axis=0)
3361
+ dtype = z_stack[0].dtype
3362
+
3363
+ filename = f"{well}_T{t_idx+1:04d}F001L01C{c_idx+1:02d}.tif"
3364
+ filepath = os.path.join(folder, filename)
3365
+ tifffile.imwrite(filepath, mip_image.astype(dtype))
3366
+
3367
+ rename_log.append({"Original File": file, "Renamed TIFF": filename})
3368
+
3369
+ ### **Process Standard Image Files**
3370
+ elif ext in ['tif', 'tiff', 'png', 'jpg', 'jpeg', 'bmp'] and not file.startswith("plate"):
3371
+ with tifffile.TiffFile(path) as tif:
3372
+ num_pages = len(tif.pages)
3373
+
3374
+ if num_pages > 1:
3375
+ images = tif.asarray()
3376
+ if images.ndim == 4:
3377
+ timepoints = range(images.shape[0])
3378
+ channels = range(images.shape[2])
3379
+
3380
+ for t_idx in timepoints:
3381
+ for c_idx in channels:
3382
+ mip_image = np.max(images[t_idx, :, c_idx], axis=0)
3383
+ dtype = images.dtype
3384
+
3385
+ filename = f"{well}_T{t_idx+1:04d}F001L01C{c_idx+1:02d}.tif"
3386
+ filepath = os.path.join(folder, filename)
3387
+ tifffile.imwrite(filepath, mip_image.astype(dtype))
3388
+
3389
+ rename_log.append({"Original File": file, "Renamed TIFF": filename})
3390
+ else:
3391
+ image = tif.pages[0].asarray()
3392
+ dtype = image.dtype
3393
+
3394
+ filename = f"{well}_T0001F001L01C01.tif"
3395
+ filepath = os.path.join(folder, filename)
3396
+ tifffile.imwrite(filepath, image.astype(dtype))
3397
+
3398
+ rename_log.append({"Original File": file, "Renamed TIFF": filename})
3399
+
3400
+ # Save rename log as CSV
3401
+ pd.DataFrame(rename_log).to_csv(csv_path, index=False)
3402
+ print(f"Processing complete. Files saved in {folder} and rename log saved as {csv_path}.")
spacr/measure.py CHANGED
@@ -945,20 +945,25 @@ def measure_crop(settings):
945
945
 
946
946
  from .io import _save_settings_to_db
947
947
  from .timelapse import _timelapse_masks_to_gif
948
- from .utils import measure_test_mode, print_progress, delete_intermedeate_files, save_settings
948
+ from .utils import measure_test_mode, print_progress, delete_intermedeate_files, save_settings, format_path_for_system, normalize_src_path
949
949
  from .settings import get_measure_crop_settings
950
950
 
951
951
  if not isinstance(settings['src'], (str, list)):
952
952
  ValueError(f'src must be a string or a list of strings')
953
953
  return
954
954
 
955
+ settings['src'] = normalize_src_path(settings['src'])
956
+
955
957
  if isinstance(settings['src'], str):
956
958
  settings['src'] = [settings['src']]
957
959
 
958
960
  if isinstance(settings['src'], list):
959
961
  source_folders = settings['src']
962
+
960
963
  for source_folder in source_folders:
961
964
  print(f'Processing folder: {source_folder}')
965
+
966
+ source_folder = format_path_for_system(source_folder)
962
967
  settings['src'] = source_folder
963
968
  src = source_folder
964
969
 
@@ -966,15 +971,12 @@ def measure_crop(settings):
966
971
  settings = measure_test_mode(settings)
967
972
 
968
973
  src_fldr = settings['src']
974
+
969
975
  if not os.path.basename(src_fldr).endswith('merged'):
970
976
  print(f"WARNING: Source folder, settings: src: {src_fldr} should end with '/merged'")
971
977
  src_fldr = os.path.join(src_fldr, 'merged')
978
+ settings['src'] = src_fldr
972
979
  print(f"Changed source folder to: {src_fldr}")
973
-
974
- #if settings['save_measurements']:
975
- #source_folder = os.path.dirname(settings['src'])
976
- #os.makedirs(source_folder+'/measurements', exist_ok=True)
977
- #_create_database(source_folder+'/measurements/measurements.db')
978
980
 
979
981
  if settings['cell_mask_dim'] is None:
980
982
  settings['uninfected'] = True
@@ -995,12 +997,9 @@ def measure_crop(settings):
995
997
  print(f'Warning reserving 6 CPU cores for other processes, setting n_jobs to {spacr_cores}')
996
998
  settings['n_jobs'] = spacr_cores
997
999
 
998
- #dirname = os.path.dirname(settings['src'])
999
- #settings_df = pd.DataFrame(list(settings.items()), columns=['Key', 'Value'])
1000
- #settings_csv = os.path.join(dirname,'settings','measure_crop_settings.csv')
1001
- #os.makedirs(os.path.join(dirname,'settings'), exist_ok=True)
1002
- #settings_df.to_csv(settings_csv, index=False)
1003
- save_settings(settings, name='measure_crop_settings', show=True)
1000
+ settings_save = settings.copy()
1001
+ settings_save['src'] = os.path.dirname(settings['src'])
1002
+ save_settings(settings_save, name='measure_crop_settings', show=True)
1004
1003
 
1005
1004
  if settings['timelapse_objects'] == 'nucleus':
1006
1005
  if not settings['cell_mask_dim'] is None:
spacr/plot.py CHANGED
@@ -2705,7 +2705,7 @@ class spacrGraph:
2705
2705
  def perform_posthoc_tests(self, is_normal, unique_groups):
2706
2706
  """Perform post-hoc tests for multiple groups based on all_to_all flag."""
2707
2707
 
2708
- from .stats import choose_p_adjust_method
2708
+ from .sp_stats import choose_p_adjust_method
2709
2709
 
2710
2710
  posthoc_results = []
2711
2711
  if is_normal and len(unique_groups) > 2 and self.all_to_all:
@@ -3815,7 +3815,7 @@ def plot_proportion_stacked_bars(settings, df, group_column, bin_column, prc_col
3815
3815
  - pairwise_results (list): Pairwise test results from `chi_pairwise`.
3816
3816
  """
3817
3817
 
3818
- from .stats import chi_pairwise
3818
+ from .sp_stats import chi_pairwise
3819
3819
 
3820
3820
  # Calculate contingency table for overall chi-squared test
3821
3821
  raw_counts = df.groupby([group_column, bin_column]).size().unstack(fill_value=0)