spacr 0.9.3__py3-none-any.whl → 0.9.7__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
@@ -196,11 +196,12 @@ def preprocess_generate_masks(settings):
196
196
 
197
197
  def generate_cellpose_masks(src, settings, object_type):
198
198
 
199
- from .utils import _masks_to_masks_stack, _filter_cp_masks, _get_cellpose_batch_size, _get_cellpose_channels, _choose_model, mask_object_count, all_elements_match, prepare_batch_for_segmentation
199
+ from .utils import _masks_to_masks_stack, _filter_cp_masks, _get_cellpose_batch_size, _get_cellpose_channels, _choose_model, all_elements_match, prepare_batch_for_segmentation
200
200
  from .io import _create_database, _save_object_counts_to_database, _check_masks, _get_avg_object_size
201
201
  from .timelapse import _npz_to_movie, _btrack_track_cells, _trackpy_track_cells
202
- from .plot import plot_masks
202
+ from .plot import plot_cellpose4_output
203
203
  from .settings import set_default_settings_preprocess_generate_masks, _get_object_settings
204
+ from .spacr_cellpose import parse_cellpose4_output
204
205
 
205
206
  gc.collect()
206
207
  if not torch.cuda.is_available():
@@ -239,9 +240,12 @@ def generate_cellpose_masks(src, settings, object_type):
239
240
  cellpose_channels = _get_cellpose_channels(src, settings['nucleus_channel'], settings['pathogen_channel'], settings['cell_channel'])
240
241
  if settings['verbose']:
241
242
  print(cellpose_channels)
242
-
243
+
244
+ if object_type not in cellpose_channels:
245
+ raise ValueError(f"Error: No channels were specified for object_type '{object_type}'. Check your settings.")
243
246
  channels = cellpose_channels[object_type]
244
- cellpose_batch_size = _get_cellpose_batch_size()
247
+
248
+ #cellpose_batch_size = _get_cellpose_batch_size()
245
249
  device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
246
250
 
247
251
  if object_type == 'pathogen' and not settings['pathogen_model'] is None:
@@ -249,7 +253,8 @@ def generate_cellpose_masks(src, settings, object_type):
249
253
 
250
254
  model = _choose_model(model_name, device, object_type=object_type, restore_type=None, object_settings=object_settings)
251
255
 
252
- chans = [2, 1] if model_name == 'cyto2' else [0,0] if model_name == 'nucleus' else [2,0] if model_name == 'cyto' else [2, 0] if model_name == 'cyto3' else [2, 0]
256
+ #chans = [2, 1] if model_name == 'cyto2' else [0,0] if model_name == 'nucleus' else [2,0] if model_name == 'cyto' else [2, 0] if model_name == 'cyto3' else [2, 0]
257
+
253
258
  paths = [os.path.join(src, file) for file in os.listdir(src) if file.endswith('.npz')]
254
259
 
255
260
  count_loc = os.path.dirname(src)+'/measurements/measurements.db'
@@ -257,6 +262,7 @@ def generate_cellpose_masks(src, settings, object_type):
257
262
  _create_database(count_loc)
258
263
 
259
264
  average_sizes = []
265
+ average_count = []
260
266
  time_ls = []
261
267
 
262
268
  for file_index, path in enumerate(paths):
@@ -310,49 +316,30 @@ def generate_cellpose_masks(src, settings, object_type):
310
316
  continue
311
317
 
312
318
  batch = prepare_batch_for_segmentation(batch)
313
-
314
-
315
- #if settings['denoise']:
316
- # if object_type == 'cell':
317
- # model_type = "denoise_cyto3"
318
- # elif object_type == 'nucleus':
319
- # model_type = "denoise_nucleus"
320
- # else:
321
- # raise ValueError(f"No denoise model for object_type: {object_type}")
322
- # dn = denoise.DenoiseModel(model_type=model_type, gpu=device)
323
- # batch = dn.eval(imgs=batch, channels=chans, diameter=object_settings['diameter'])
319
+ batch_list = [batch[i] for i in range(batch.shape[0])]
324
320
 
325
321
  if timelapse:
326
322
  movie_path = os.path.join(os.path.dirname(src), 'movies')
327
323
  os.makedirs(movie_path, exist_ok=True)
328
324
  save_path = os.path.join(movie_path, f'timelapse_{object_type}_{name}.mp4')
329
325
  _npz_to_movie(batch, batch_filenames, save_path, fps=2)
330
-
331
- output = model.eval(x=batch,
332
- batch_size=cellpose_batch_size,
326
+
327
+ output = model.eval(x=batch_list,
328
+ batch_size=batch_size,
333
329
  normalize=False,
334
- channels=chans,
335
- channel_axis=3,
330
+ channel_axis=-1,
331
+ channels=channels,
336
332
  diameter=object_settings['diameter'],
337
333
  flow_threshold=flow_threshold,
338
334
  cellprob_threshold=cellprob_threshold,
339
335
  rescale=None,
340
336
  resample=object_settings['resample'])
341
-
342
- if len(output) == 4:
343
- masks, flows, _, _ = output
344
- elif len(output) == 3:
345
- masks, flows, _ = output
346
- else:
347
- raise ValueError(f"Unexpected number of return values from model.eval(). Expected 3 or 4, got {len(output)}")
337
+
338
+ masks, flows, _, _, _ = parse_cellpose4_output(output)
348
339
 
349
340
  if timelapse:
350
341
  if settings['plot']:
351
- for idx, (mask, flow, image) in enumerate(zip(masks, flows[0], batch)):
352
- if idx == 0:
353
- num_objects = mask_object_count(mask)
354
- print(f'Number of objects: {num_objects}')
355
- plot_masks(batch=image, masks=mask, flows=flow, cmap='inferno', figuresize=figuresize, nr=1, file_type='.npz', print_object_number=True)
342
+ plot_cellpose4_output(batch_list, masks, flows, cmap='inferno', figuresize=figuresize, nr=1, print_object_number=True)
356
343
 
357
344
  _save_object_counts_to_database(masks, object_type, batch_filenames, count_loc, added_string='_timelapse')
358
345
  if object_type in timelapse_objects:
@@ -431,24 +418,23 @@ def generate_cellpose_masks(src, settings, object_type):
431
418
  mask_stack = _masks_to_masks_stack(masks)
432
419
 
433
420
  if settings['plot']:
434
- for idx, (mask, flow, image) in enumerate(zip(masks, flows[0], batch)):
435
- if idx == 0:
436
- num_objects = mask_object_count(mask)
437
- print(f'Number of objects, : {num_objects}')
438
- plot_masks(batch=image, masks=mask, flows=flow, cmap='inferno', figuresize=figuresize, nr=1, file_type='.npz', print_object_number=True)
421
+ plot_cellpose4_output(batch_list, masks, flows, cmap='inferno', figuresize=figuresize, nr=1, print_object_number=True)
439
422
 
440
423
  if not np.any(mask_stack):
441
- average_obj_size = 0
424
+ avg_num_objects_per_image, average_obj_size = 0, 0
442
425
  else:
443
- average_obj_size = _get_avg_object_size(mask_stack)
444
-
426
+ avg_num_objects_per_image, average_obj_size = _get_avg_object_size(mask_stack)
427
+
428
+ average_count.append(avg_num_objects_per_image)
445
429
  average_sizes.append(average_obj_size)
446
430
  overall_average_size = np.mean(average_sizes) if len(average_sizes) > 0 else 0
447
- print(f'object_size:{object_type}: {overall_average_size:.3f} px2')
431
+ overall_average_count = np.mean(average_count) if len(average_count) > 0 else 0
432
+ print(f'Found {overall_average_count} {object_type}/FOV. average size: {overall_average_size:.3f} px2')
448
433
 
449
434
  if not timelapse:
450
435
  if settings['plot']:
451
- plot_masks(batch, mask_stack, flows, figuresize=figuresize, cmap='inferno', nr=batch_size)
436
+ plot_cellpose4_output(batch_list, masks, flows, cmap='inferno', figuresize=figuresize, nr=batch_size)
437
+
452
438
  if settings['save']:
453
439
  for mask_index, mask in enumerate(mask_stack):
454
440
  output_filename = os.path.join(output_folder, batch_filenames[mask_index])
spacr/gui_core.py CHANGED
@@ -103,6 +103,11 @@ def display_figure(fig):
103
103
  new_canvas = FigureCanvasTkAgg(fig, master=canvas_widget.master)
104
104
  new_canvas.draw()
105
105
  new_canvas.get_tk_widget().grid(row=0, column=0, sticky="nsew")
106
+
107
+ # Store existing text labels on each axis for zoom visibility control (new feature)
108
+ for ax in fig.get_axes():
109
+ texts = ax.texts
110
+ ax._label_annotations = texts
106
111
 
107
112
  # Update the global canvas and canvas_widget references
108
113
  canvas = new_canvas
@@ -169,14 +174,100 @@ def display_figure(fig):
169
174
  else:
170
175
  #flash_feedback("right")
171
176
  show_next_figure()
177
+
178
+ def zoom(event):
179
+ # Define zoom factors
180
+ zoom_in_factor = 1 / 1.2
181
+ zoom_out_factor = 1.2
182
+
183
+ if event.num == 4 or (hasattr(event, 'delta') and event.delta > 0):
184
+ factor = zoom_in_factor
185
+ elif event.num == 5 or (hasattr(event, 'delta') and event.delta < 0):
186
+ factor = zoom_out_factor
187
+ else:
188
+ return
189
+
190
+ for ax in canvas.figure.get_axes():
191
+ # Convert canvas pixel (event.x, event.y) to axis data coordinates
192
+ # EVEN IF the mouse is over a different axis, we use the same pixel to data mapping for each
193
+ inv = ax.transData.inverted()
194
+ try:
195
+ data_x, data_y = inv.transform((event.x, event.y))
196
+ except ValueError:
197
+ continue # e.g. axis has no data
198
+
199
+ xlim = ax.get_xlim()
200
+ ylim = ax.get_ylim()
201
+
202
+ # Zoom around (data_x, data_y) in *that axis's* data space
203
+ new_xlim = [data_x - (data_x - xlim[0]) * factor,
204
+ data_x + (xlim[1] - data_x) * factor]
205
+ new_ylim = [data_y - (data_y - ylim[0]) * factor,
206
+ data_y + (ylim[1] - data_y) * factor]
207
+
208
+ ax.set_xlim(new_xlim)
209
+ ax.set_ylim(new_ylim)
210
+
211
+ # Clip text labels
212
+ for label in ax.texts:
213
+ label.set_clip_on(True)
214
+
215
+ # Update label visibility
216
+ if hasattr(ax, '_label_annotations'):
217
+ for label in ax._label_annotations:
218
+ x, y = label.get_position()
219
+ is_visible = (new_xlim[0] <= x <= new_xlim[1]) and (new_ylim[0] <= y <= new_ylim[1])
220
+ label.set_visible(is_visible)
221
+
222
+ canvas.draw_idle()
172
223
 
173
- def zoom_test(event):
174
- if event.num == 4: # Scroll up
175
- print("zoom in")
176
- elif event.num == 5: # Scroll down
177
- print("zoom out")
224
+ def zoom_v2(event):
225
+ # Define zoom factors
226
+ zoom_in_factor = 1 / 1.2
227
+ zoom_out_factor = 1.2
228
+
229
+ if event.num == 4 or (hasattr(event, 'delta') and event.delta > 0):
230
+ factor = zoom_in_factor
231
+ elif event.num == 5 or (hasattr(event, 'delta') and event.delta < 0):
232
+ factor = zoom_out_factor
233
+ else:
234
+ return
235
+
236
+ for ax in canvas.figure.get_axes():
237
+ # Translate mouse position to figure coordinates
238
+ mouse_x, mouse_y = event.x, event.y
239
+ inv = ax.transData.inverted()
240
+ data_x, data_y = inv.transform((mouse_x, mouse_y))
241
+
242
+ xlim = ax.get_xlim()
243
+ ylim = ax.get_ylim()
244
+
245
+ # Compute new limits centered on the mouse data position
246
+ new_width = (xlim[1] - xlim[0]) * factor
247
+ new_height = (ylim[1] - ylim[0]) * factor
248
+
249
+ new_xlim = [data_x - (data_x - xlim[0]) * factor,
250
+ data_x + (xlim[1] - data_x) * factor]
251
+ new_ylim = [data_y - (data_y - ylim[0]) * factor,
252
+ data_y + (ylim[1] - data_y) * factor]
253
+
254
+ ax.set_xlim(new_xlim)
255
+ ax.set_ylim(new_ylim)
256
+
257
+ # Clip all text labels to the axes area
258
+ for label in ax.texts:
259
+ label.set_clip_on(True)
260
+
261
+ # Update label visibility based on new limits
262
+ if hasattr(ax, '_label_annotations'):
263
+ for label in ax._label_annotations:
264
+ x, y = label.get_position()
265
+ is_visible = (new_xlim[0] <= x <= new_xlim[1]) and (new_ylim[0] <= y <= new_ylim[1])
266
+ label.set_visible(is_visible)
267
+
268
+ canvas.draw_idle()
178
269
 
179
- def zoom(event):
270
+ def zoom_v1(event):
180
271
  # Fixed zoom factors (adjust these if you want faster or slower zoom)
181
272
  zoom_in_factor = 0.9 # When zooming in, ranges shrink by 10%
182
273
  zoom_out_factor = 1.1 # When zooming out, ranges increase by 10%
spacr/io.py CHANGED
@@ -1587,7 +1587,7 @@ def preprocess_img_data(settings):
1587
1587
  print(f"Found {extension_counts[most_common_extension]} {most_common_extension} files")
1588
1588
 
1589
1589
  else:
1590
- print(f"Could not find any {valid_ext} files in {src} only found {extension_counts[0]}")
1590
+ print(f"Could not find any {valid_ext} files in {src}")
1591
1591
  print(f"{files} in {src}")
1592
1592
  print(f"Please check the folder and try again")
1593
1593
 
@@ -1699,6 +1699,50 @@ def _check_masks(batch, batch_filenames, output_folder):
1699
1699
  return np.array(filtered_batch), filtered_filenames
1700
1700
 
1701
1701
  def _get_avg_object_size(masks):
1702
+ """
1703
+ Calculate:
1704
+ - average number of objects per image
1705
+ - average object size over all objects
1706
+
1707
+ Parameters:
1708
+ masks (list): A list of 2D or 3D masks with labeled objects.
1709
+
1710
+ Returns:
1711
+ tuple:
1712
+ avg_num_objects_per_image (float)
1713
+ avg_object_size (float)
1714
+ """
1715
+ per_image_counts = []
1716
+ all_areas = []
1717
+
1718
+ for idx, mask in enumerate(masks):
1719
+ if mask.ndim in [2, 3] and np.any(mask):
1720
+ props = measure.regionprops(mask)
1721
+ areas = [prop.area for prop in props]
1722
+ per_image_counts.append(len(areas))
1723
+ all_areas.extend(areas)
1724
+ else:
1725
+ per_image_counts.append(0)
1726
+ if not np.any(mask):
1727
+ print(f"Warning: Mask {idx} is empty.")
1728
+ elif mask.ndim not in [2, 3]:
1729
+ print(f"Warning: Mask {idx} has invalid dimension: {mask.ndim}")
1730
+
1731
+ # Average number of objects per image
1732
+ if per_image_counts:
1733
+ avg_num_objects_per_image = sum(per_image_counts) / len(per_image_counts)
1734
+ else:
1735
+ avg_num_objects_per_image = 0
1736
+
1737
+ # Average object size over all objects
1738
+ if all_areas:
1739
+ avg_object_size = sum(all_areas) / len(all_areas)
1740
+ else:
1741
+ avg_object_size = 0
1742
+
1743
+ return avg_num_objects_per_image, avg_object_size
1744
+
1745
+ def _get_avg_object_size_v1(masks):
1702
1746
  """
1703
1747
  Calculate the average size of objects in a list of masks.
1704
1748
 
spacr/plot.py CHANGED
@@ -34,7 +34,6 @@ from collections import defaultdict
34
34
  from matplotlib.gridspec import GridSpec
35
35
  from matplotlib_venn import venn2
36
36
 
37
- #filter_dict={'cell':[(0,100000), (0, 65000)],'nucleus':[(3000,100000), (1500, 65000)],'pathogen':[(500,100000), (0, 65000)]}
38
37
  def plot_image_mask_overlay(
39
38
  file,
40
39
  channels,
@@ -367,6 +366,54 @@ def plot_image_mask_overlay(
367
366
 
368
367
  return fig
369
368
 
369
+ def plot_cellpose4_output(batch, masks, flows, cmap='inferno', figuresize=10, nr=1, print_object_number=True):
370
+ """
371
+ Plot the masks and flows for a given batch of images.
372
+
373
+ Args:
374
+ batch (numpy.ndarray): The batch of images.
375
+ masks (list or numpy.ndarray): The masks corresponding to the images.
376
+ flows (list or numpy.ndarray): The flows corresponding to the images.
377
+ cmap (str, optional): The colormap to use for displaying the images. Defaults to 'inferno'.
378
+ figuresize (int, optional): The size of the figure. Defaults to 20.
379
+ nr (int, optional): The maximum number of images to plot. Defaults to 1.
380
+ file_type (str, optional): The file type of the flows. Defaults to '.npz'.
381
+ print_object_number (bool, optional): Whether to print the object number on the mask. Defaults to True.
382
+
383
+ Returns:
384
+ None
385
+ """
386
+
387
+ from .utils import _generate_mask_random_cmap, mask_object_count
388
+
389
+ font = figuresize/2
390
+ index = 0
391
+
392
+ for image, mask, flow in zip(batch, masks, flows):
393
+ #if print_object_number:
394
+ # num_objects = mask_object_count(mask)
395
+ # print(f'Number of objects: {num_objects}')
396
+ random_cmap = _generate_mask_random_cmap(mask)
397
+
398
+ if index < nr:
399
+ index += 1
400
+ chans = image.shape[-1]
401
+ fig, ax = plt.subplots(1, image.shape[-1] + 2, figsize=(4 * figuresize, figuresize))
402
+ for v in range(0, image.shape[-1]):
403
+ ax[v].imshow(image[..., v], cmap=cmap, interpolation='nearest')
404
+ ax[v].set_title('Image - Channel'+str(v))
405
+ ax[chans].imshow(mask, cmap=random_cmap, interpolation='nearest')
406
+ ax[chans].set_title('Mask')
407
+ if print_object_number:
408
+ unique_objects = np.unique(mask)[1:]
409
+ for obj in unique_objects:
410
+ cy, cx = ndi.center_of_mass(mask == obj)
411
+ ax[chans].text(cx, cy, str(obj), color='white', fontsize=font, ha='center', va='center')
412
+ ax[chans+1].imshow(flow, cmap='viridis', interpolation='nearest')
413
+ ax[chans+1].set_title('Flow')
414
+ plt.show()
415
+ return
416
+
370
417
  def plot_masks(batch, masks, flows, cmap='inferno', figuresize=10, nr=1, file_type='.npz', print_object_number=True):
371
418
  """
372
419
  Plot the masks and flows for a given batch of images.
spacr/settings.py CHANGED
@@ -64,9 +64,9 @@ def set_default_settings_preprocess_generate_masks(settings={}):
64
64
  settings.setdefault('nucleus_background', 100)
65
65
  settings.setdefault('nucleus_Signal_to_noise', 10)
66
66
  settings.setdefault('nucleus_CP_prob', 0)
67
- settings.setdefault('nucleus_FT', 100)
68
- settings.setdefault('cell_FT', 100)
69
- settings.setdefault('pathogen_FT', 100)
67
+ settings.setdefault('nucleus_FT', 1.0)
68
+ settings.setdefault('cell_FT', 1.0)
69
+ settings.setdefault('pathogen_FT', 1.0)
70
70
 
71
71
  # Plot settings
72
72
  settings.setdefault('plot', False)
@@ -97,6 +97,10 @@ def set_default_settings_preprocess_generate_masks(settings={}):
97
97
  settings.setdefault('upscale', False)
98
98
  settings.setdefault('upscale_factor', 2.0)
99
99
  settings.setdefault('adjust_cells', False)
100
+ settings.setdefault('use_sam_cell', False)
101
+ settings.setdefault('use_sam_nucleus', False)
102
+ settings.setdefault('use_sam_pathogen', False)
103
+
100
104
  return settings
101
105
 
102
106
  def set_default_plot_data_from_db(settings):
@@ -173,6 +177,8 @@ def _get_object_settings(object_type, settings):
173
177
  object_settings['maximum_size'] = (object_settings['diameter']**2)*10
174
178
  else:
175
179
  print(f'Cell diameter must be an integer or float, got {settings["cell_diamiter"]}')
180
+ if settings['use_sam_cell']:
181
+ object_settings['model_name'] = 'sam'
176
182
 
177
183
  elif object_type == 'nucleus':
178
184
  object_settings['model_name'] = 'nuclei'
@@ -187,6 +193,8 @@ def _get_object_settings(object_type, settings):
187
193
  object_settings['maximum_size'] = (object_settings['diameter']**2)*10
188
194
  else:
189
195
  print(f'Nucleus diameter must be an integer or float, got {settings["nucleus_diamiter"]}')
196
+ if settings['use_sam_nucleus']:
197
+ object_settings['model_name'] = 'sam'
190
198
 
191
199
  elif object_type == 'pathogen':
192
200
  object_settings['model_name'] = 'cyto'
@@ -203,10 +211,13 @@ def _get_object_settings(object_type, settings):
203
211
  object_settings['maximum_size'] = (object_settings['diameter']**2)*10
204
212
  else:
205
213
  print(f'Pathogen diameter must be an integer or float, got {settings["pathogen_diamiter"]}')
214
+
215
+ if settings['use_sam_pathogen']:
216
+ object_settings['model_name'] = 'sam'
206
217
 
207
218
  else:
208
219
  print(f'Object type: {object_type} not supported. Supported object types are : cell, nucleus and pathogen')
209
-
220
+
210
221
  if settings['verbose']:
211
222
  print(object_settings)
212
223
 
@@ -712,7 +723,7 @@ expected_types = {
712
723
  "nucleus_background": int,
713
724
  "nucleus_Signal_to_noise": float,
714
725
  "nucleus_CP_prob": float,
715
- "nucleus_FT": float,
726
+ "nucleus_FT": (int, float),
716
727
  "cell_channel": (int, type(None)),
717
728
  "cell_background": (int, float),
718
729
  "cell_Signal_to_noise": (int, float),
@@ -1003,11 +1014,14 @@ expected_types = {
1003
1014
  "nucleus_diamiter":int,
1004
1015
  "pathogen_diamiter":int,
1005
1016
  "consolidate":bool,
1017
+ 'use_sam_cell':bool,
1018
+ 'use_sam_nucleus':bool,
1019
+ 'use_sam_pathogen':bool,
1006
1020
  "distance_gaussian_sigma": (int, type(None))
1007
1021
  }
1008
1022
 
1009
1023
  categories = {"Paths":[ "src", "grna", "barcodes", "custom_model_path", "dataset","model_path","grna_csv","row_csv","column_csv", "metadata_files", "score_data","count_data"],
1010
- "General": ["cell_mask_dim", "cytoplasm", "cell_chann_dim", "cell_channel", "nucleus_chann_dim", "nucleus_channel", "nucleus_mask_dim", "pathogen_mask_dim", "pathogen_chann_dim", "pathogen_channel", "test_mode", "plot", "metadata_type", "custom_regex", "experiment", "channels", "magnification", "channel_dims", "apply_model_to_dataset", "generate_training_dataset", "train_DL_model", "delete_intermediate", "uninfected", ],
1024
+ "General": ["cell_mask_dim", "cytoplasm", "cell_chann_dim", "cell_channel", "nucleus_chann_dim", "nucleus_channel", "nucleus_mask_dim", "pathogen_mask_dim", "pathogen_chann_dim", "pathogen_channel", "test_mode", "plot", "metadata_type", "custom_regex", "experiment", "channels", "magnification", "channel_dims", "apply_model_to_dataset", "generate_training_dataset", "train_DL_model", "delete_intermediate", "uninfected", ],
1011
1025
  "Cellpose":["denoise","fill_in","from_scratch", "n_epochs", "width_height", "model_name", "custom_model", "resample", "rescale", "CP_prob", "flow_threshold", "percentiles", "invert", "diameter", "grayscale", "Signal_to_noise", "resize", "target_height", "target_width"],
1012
1026
  "Cell": ["cell_diamiter","cell_intensity_range", "cell_size_range", "cell_background", "cell_Signal_to_noise", "cell_CP_prob", "cell_FT", "remove_background_cell", "cell_min_size", "cytoplasm_min_size", "adjust_cells", "cells", "cell_loc"],
1013
1027
  "Nucleus": ["nucleus_diamiter","nucleus_intensity_range", "nucleus_size_range", "nucleus_background", "nucleus_Signal_to_noise", "nucleus_CP_prob", "nucleus_FT", "remove_background_nucleus", "nucleus_min_size", "nucleus_loc"],
@@ -1025,7 +1039,7 @@ categories = {"Paths":[ "src", "grna", "barcodes", "custom_model_path", "dataset
1025
1039
  "Plot": ["split_axis_lims", "x_lim","log_x","log_y", "plot_control", "plot_nr", "examples_to_plot", "normalize_plots", "cmap", "figuresize", "plot_cluster_grids", "img_zoom", "row_limit", "color_by", "plot_images", "smooth_lines", "plot_points", "plot_outlines", "black_background", "plot_by_cluster", "heatmap_feature","grouping","min_max","cmap","save_figure"],
1026
1040
  "Timelapse": ["timelapse", "fps", "timelapse_displacement", "timelapse_memory", "timelapse_frame_limits", "timelapse_remove_transient", "timelapse_mode", "timelapse_objects", "compartments"],
1027
1041
  "Advanced": ["merge_edge_pathogen_cells", "test_images", "random_test", "test_nr", "test", "test_split", "normalize", "target_unique_count","threshold_multiplier", "threshold_method", "min_n","shuffle", "target_intensity_min", "cells_per_well", "nuclei_limit", "pathogen_limit", "background", "backgrounds", "schedule", "test_size","exclude","n_repeats","top_features", "model_type_ml", "model_type","minimum_cell_count","n_estimators","preprocess", "remove_background", "normalize", "lower_percentile", "merge_pathogens", "batch_size", "filter", "save", "masks", "verbose", "randomize", "n_jobs"],
1028
- "Beta": ["all_to_mip", "upscale", "upscale_factor", "consolidate", "distance_gaussian_sigma"]
1042
+ "Beta": ["all_to_mip", "upscale", "upscale_factor", "consolidate", "distance_gaussian_sigma","use_sam_pathogen","use_sam_nucleus", "use_sam_cell"]
1029
1043
  }
1030
1044
 
1031
1045
 
@@ -1053,7 +1067,7 @@ def check_settings(vars_dict, expected_types, q=None):
1053
1067
  expected_type = expected_types.get(key, str)
1054
1068
 
1055
1069
  try:
1056
- if key in ["cell_plate_metadata", "timelapse_frame_limits", "png_size", "png_dims", "pathogen_plate_metadata", "treatment_plate_metadata", "timelapse_objects", "class_metadata", "crop_mode"]:
1070
+ if key in ["cell_plate_metadata", "timelapse_frame_limits", "png_size", "png_dims", "pathogen_plate_metadata", "treatment_plate_metadata", "timelapse_objects", "class_metadata", "crop_mode", "dialate_png_ratios"]:
1057
1071
  if value is None:
1058
1072
  parsed_value = None
1059
1073
  else:
@@ -1415,6 +1429,9 @@ def generate_fields(variables, scrollable_frame):
1415
1429
  "overlay": "(bool) - Overlay activation maps on the images.",
1416
1430
  "shuffle": "(bool) - Shuffle the dataset bufore generating the activation maps",
1417
1431
  "correlation": "(bool) - Calculate correlation between image channels and activation maps. Data is saved to .db.",
1432
+ "use_sam_cell": "(bool) - Whether to use SAM for cell segmentation.",
1433
+ "use_sam_nucleus": "(bool) - Whether to use SAM for nucleus segmentation.",
1434
+ "use_sam_pathogen": "(bool) - Whether to use SAM for pathogen segmentation.",
1418
1435
  "normalize_input": "(bool) - Normalize the input images before passing them to the model.",
1419
1436
  "normalize_plots": "(bool) - Normalize images before plotting.",
1420
1437
  }
spacr/sp_stats.py CHANGED
@@ -7,7 +7,6 @@ from scipy.stats import chi2_contingency, fisher_exact
7
7
  import itertools
8
8
  from statsmodels.stats.multitest import multipletests
9
9
 
10
-
11
10
  def choose_p_adjust_method(num_groups, num_data_points):
12
11
  """
13
12
  Selects the most appropriate p-value adjustment method based on data characteristics.
spacr/spacr_cellpose.py CHANGED
@@ -7,6 +7,65 @@ from multiprocessing import Pool
7
7
  from skimage.transform import resize as resizescikit
8
8
  from scipy.ndimage import binary_fill_holes
9
9
 
10
+ def parse_cellpose4_output(output):
11
+ """
12
+ General parser for Cellpose eval output.
13
+ Handles:
14
+ - batched format (list of 4 arrays)
15
+ - per-image list of flows
16
+ Returns:
17
+ masks, flows0, flows1, flows2, flows3
18
+ """
19
+
20
+ masks = output[0]
21
+ flows = output[1]
22
+
23
+ if not isinstance(flows, (list, tuple)):
24
+ raise ValueError(f"Unrecognized Cellpose flows type: {type(flows)}")
25
+
26
+ # Determine number of images
27
+ try:
28
+ num_images = len(masks)
29
+ except TypeError:
30
+ raise ValueError(f"Cannot determine number of images in masks (type={type(masks)})")
31
+
32
+ # Case A: batched format (4 arrays stacked over batch)
33
+ if len(flows) == 4 and all(isinstance(f, np.ndarray) for f in flows):
34
+ flow0_array, flow1_array, flow2_array, flow3_array = flows
35
+
36
+ flows0 = [flow0_array[i] for i in range(num_images)]
37
+ flows1 = [flow1_array[:, i] for i in range(num_images)]
38
+ flows2 = [flow2_array[i] for i in range(num_images)]
39
+ flows3 = [flow3_array[i] for i in range(num_images)]
40
+
41
+ return masks, flows0, flows1, flows2, flows3
42
+
43
+ # Case B: per-image format
44
+ elif len(flows) == num_images:
45
+ flows0, flows1, flows2, flows3 = [], [], [], []
46
+
47
+ for item in flows:
48
+ if isinstance(item, (list, tuple)):
49
+ n = len(item)
50
+ f0 = item[0] if n > 0 else None
51
+ f1 = item[1] if n > 1 else None
52
+ f2 = item[2] if n > 2 else None
53
+ f3 = item[3] if n > 3 else None
54
+ elif isinstance(item, np.ndarray):
55
+ f0, f1, f2, f3 = item, None, None, None
56
+ else:
57
+ f0 = f1 = f2 = f3 = None
58
+
59
+ flows0.append(f0)
60
+ flows1.append(f1)
61
+ flows2.append(f2)
62
+ flows3.append(f3)
63
+
64
+ return masks, flows0, flows1, flows2, flows3
65
+
66
+ # Unrecognized structure
67
+ raise ValueError(f"Unrecognized Cellpose flows format: type={type(flows)}, len={len(flows) if hasattr(flows,'__len__') else 'unknown'}")
68
+
10
69
  def identify_masks_finetune(settings):
11
70
 
12
71
  from .plot import print_mask_and_flows
spacr/utils.py CHANGED
@@ -42,6 +42,7 @@ from torchvision.utils import make_grid
42
42
  import seaborn as sns
43
43
  import matplotlib.pyplot as plt
44
44
  from matplotlib.offsetbox import OffsetImage, AnnotationBbox
45
+ import matplotlib as mpl
45
46
 
46
47
  from scipy import stats
47
48
  import scipy.ndimage as ndi
@@ -69,6 +70,24 @@ from spacr import __file__ as spacr_path
69
70
  import umap.umap_ as umap
70
71
  #import umap
71
72
 
73
+ def _generate_mask_random_cmap(mask):
74
+ """
75
+ Generate a random colormap based on the unique labels in the given mask.
76
+
77
+ Parameters:
78
+ mask (ndarray): The mask array containing unique labels.
79
+
80
+ Returns:
81
+ ListedColormap: A random colormap generated based on the unique labels in the mask.
82
+ """
83
+ unique_labels = np.unique(mask)
84
+ num_objects = len(unique_labels[unique_labels != 0])
85
+ random_colors = np.random.rand(num_objects+1, 4)
86
+ random_colors[:, 3] = 1
87
+ random_colors[0, :] = [0, 0, 0, 1]
88
+ random_cmap = mpl.colors.ListedColormap(random_colors)
89
+ return random_cmap
90
+
72
91
  def filepaths_to_database(img_paths, settings, source_folder, crop_mode):
73
92
 
74
93
  png_df = pd.DataFrame(img_paths, columns=['png_path'])
@@ -1265,6 +1284,34 @@ def _pivot_counts_table(db_path):
1265
1284
  conn.close()
1266
1285
 
1267
1286
  def _get_cellpose_channels(src, nucleus_channel, pathogen_channel, cell_channel):
1287
+ cell_mask_path = os.path.join(src, 'masks', 'cell_mask_stack')
1288
+ nucleus_mask_path = os.path.join(src, 'masks', 'nucleus_mask_stack')
1289
+ pathogen_mask_path = os.path.join(src, 'masks', 'pathogen_mask_stack')
1290
+
1291
+ if any(os.path.exists(p) for p in [cell_mask_path, nucleus_mask_path, pathogen_mask_path]):
1292
+ if any(c is None for c in [nucleus_channel, pathogen_channel, cell_channel]):
1293
+ print('Warning: Cellpose masks already exist. Unexpected behaviour if any channel is None while masks exist.')
1294
+
1295
+ cellpose_channels = {}
1296
+
1297
+ # Nucleus: always duplicated single channel
1298
+ if nucleus_channel is not None:
1299
+ cellpose_channels['nucleus'] = [nucleus_channel, nucleus_channel]
1300
+
1301
+ # Pathogen: always duplicated single channel
1302
+ if pathogen_channel is not None:
1303
+ cellpose_channels['pathogen'] = [pathogen_channel, pathogen_channel]
1304
+
1305
+ # Cell: prefer nucleus as second if available
1306
+ if cell_channel is not None:
1307
+ if nucleus_channel is not None:
1308
+ cellpose_channels['cell'] = [nucleus_channel, cell_channel]
1309
+ else:
1310
+ cellpose_channels['cell'] = [cell_channel, cell_channel]
1311
+
1312
+ return cellpose_channels
1313
+
1314
+ def _get_cellpose_channels_v1(src, nucleus_channel, pathogen_channel, cell_channel):
1268
1315
 
1269
1316
  cell_mask_path = os.path.join(src, 'masks', 'cell_mask_stack')
1270
1317
  nucleus_mask_path = os.path.join(src, 'masks', 'nucleus_mask_stack')
@@ -3150,6 +3197,58 @@ def _run_test_mode(src, regex, timelapse=False, test_images=10, random_test=True
3150
3197
  return test_folder_path
3151
3198
 
3152
3199
  def _choose_model(model_name, device, object_type='cell', restore_type=None, object_settings={}):
3200
+ if object_type == 'pathogen':
3201
+ if model_name == 'toxo_pv_lumen':
3202
+ diameter = object_settings['diameter']
3203
+ current_dir = os.path.dirname(__file__)
3204
+ model_path = os.path.join(current_dir, 'models', 'cp', 'toxo_pv_lumen.CP_model')
3205
+ print(model_path)
3206
+ model = cp_models.CellposeModel(
3207
+ gpu=torch.cuda.is_available(),
3208
+ model_type=None,
3209
+ pretrained_model=model_path,
3210
+ diam_mean=diameter,
3211
+ device=device
3212
+ )
3213
+ print('Using Toxoplasma PV lumen model to generate pathogen masks')
3214
+ return model
3215
+
3216
+ restore_list = ['denoise', 'deblur', 'upsample', None]
3217
+ if restore_type not in restore_list:
3218
+ print(f"Invalid restore type. Choose from {restore_list}, defaulting to None")
3219
+ restore_type = None
3220
+
3221
+ if restore_type is None:
3222
+ if model_name == 'sam':
3223
+ model = cp_models.CellposeModel(gpu=torch.cuda.is_available(), device=device, pretrained_model='cpsam',)
3224
+ return model
3225
+ if model_name in ['cyto', 'cyto2', 'cyto3', 'nuclei']:
3226
+ model = cp_models.CellposeModel(gpu=torch.cuda.is_available(), model_type=model_name, device=device)
3227
+ return model
3228
+ else:
3229
+ if object_type == 'nucleus':
3230
+ restore = f'{restore_type}_nuclei'
3231
+ model = denoise.CellposeDenoiseModel(
3232
+ gpu=torch.cuda.is_available(),
3233
+ model_type="nuclei",
3234
+ restore_type=restore,
3235
+ chan2_restore=False,
3236
+ device=device
3237
+ )
3238
+ return model
3239
+ else:
3240
+ restore = f'{restore_type}_cyto3'
3241
+ chan2_restore = (model_name == 'cyto2')
3242
+ model = denoise.CellposeDenoiseModel(
3243
+ gpu=torch.cuda.is_available(),
3244
+ model_type="cyto3",
3245
+ restore_type=restore,
3246
+ chan2_restore=chan2_restore,
3247
+ device=device
3248
+ )
3249
+ return model
3250
+
3251
+ def _choose_model_v1(model_name, device, object_type='cell', restore_type=None, object_settings={}):
3153
3252
 
3154
3253
  if object_type == 'pathogen':
3155
3254
  if model_name == 'toxo_pv_lumen':
@@ -3168,16 +3267,16 @@ def _choose_model(model_name, device, object_type='cell', restore_type=None, obj
3168
3267
 
3169
3268
  if restore_type == None:
3170
3269
  if model_name in ['cyto', 'cyto2', 'cyto3', 'nuclei']:
3171
- model = cp_models.Cellpose(gpu=torch.cuda.is_available(), model_type=model_name, device=device)
3270
+ model = cp_models.CellposeModel(gpu=torch.cuda.is_available(), model_type=model_name, device=device)
3172
3271
  return model
3173
3272
  else:
3174
3273
  if object_type == 'nucleus':
3175
- restore = f'{type}_nuclei'
3274
+ restore = f'{restore_type}_nuclei'
3176
3275
  model = denoise.CellposeDenoiseModel(gpu=torch.cuda.is_available(), model_type="nuclei",restore_type=restore, chan2_restore=False, device=device)
3177
3276
  return model
3178
3277
 
3179
3278
  else:
3180
- restore = f'{type}_cyto3'
3279
+ restore = f'{restore_type}_cyto3'
3181
3280
  if model_name =='cyto2':
3182
3281
  chan2_restore = True
3183
3282
  if model_name =='cyto':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spacr
3
- Version: 0.9.3
3
+ Version: 0.9.7
4
4
  Summary: Spatial phenotype analysis of crisp screens (SpaCr)
5
5
  Home-page: https://github.com/EinarOlafsson/spacr
6
6
  Author: Einar Birnir Olafsson
@@ -13,7 +13,8 @@ License-File: LICENSE
13
13
  Requires-Dist: numpy <2.0,>=1.26.4
14
14
  Requires-Dist: pandas <3.0,>=2.2.1
15
15
  Requires-Dist: scipy <2.0,>=1.12.0
16
- Requires-Dist: cellpose <4.0,>=3.0.6
16
+ Requires-Dist: cellpose <5.0,>=4.0
17
+ Requires-Dist: segment-anything
17
18
  Requires-Dist: scikit-image <1.0,>=0.22.0
18
19
  Requires-Dist: scikit-learn <2.0,>=1.4.1
19
20
  Requires-Dist: scikit-posthocs <0.20,>=0.10.0
@@ -195,6 +196,13 @@ The following example Jupyter notebooks illustrate common workflows using spaCR.
195
196
  - `Finetune cellpose models <https://github.com/EinarOlafsson/spacr/blob/main/Notebooks/5_spacr_train_cellpose.ipynb>`_
196
197
  *Finetune Cellpose models using your own annotated training data for improved segmentation accuracy.*
197
198
 
199
+ Interactive Tutorial (under construction)
200
+ -----------------------------------------
201
+
202
+ Click below to explore the step-by-step GUI and Notebook tutorials for spaCR:
203
+
204
+ `spaCR Tutorial Page <https://einarolafsson.github.io/spacr/tutorial/>`_
205
+
198
206
  License
199
207
  -------
200
208
  spaCR is distributed under the terms of the MIT License.
@@ -8,28 +8,28 @@ spacr/app_measure.py,sha256=_K7APYIeOKpV6e_LcqabBjvEi7mfq9Fch8175x1x0k8,162
8
8
  spacr/app_sequencing.py,sha256=DjG26jy4cpddnV8WOOAIiExtOe9MleVMY4MFa5uTo5w,157
9
9
  spacr/app_umap.py,sha256=ZWAmf_OsIKbYvolYuWPMYhdlVe-n2CADoJulAizMiEo,153
10
10
  spacr/chat_bot.py,sha256=n3Fhqg3qofVXHmh3H9sUcmfYy9MmgRnr48663MVdY9E,1244
11
- spacr/core.py,sha256=w4E3Pg-ZnA8BOK0iUMTjiNO0GeR5YCEs8fUTbESzqjY,47392
11
+ spacr/core.py,sha256=SDsJsgarbMHDw2i4OOMCKtMtVx9t1tjsGCVs6iHPj7s,46638
12
12
  spacr/deep_spacr.py,sha256=055tIo3WP3elGFiIuSZaLURgu2XyUDxAdbw5ezASEqM,54526
13
13
  spacr/gui.py,sha256=NhMh96KoArrSAaJBV6PhDQpIC1cQpxgb6SclhRbYG8s,8122
14
- spacr/gui_core.py,sha256=RtpdB8S8yF9WARRsUjrZ1szZi4ZMfG7R_W34BTBEGYo,52729
14
+ spacr/gui_core.py,sha256=pXTswrqPMTb2mgF_mvnCzBgmXEaf9w7wDnSD6uMA67w,56228
15
15
  spacr/gui_elements.py,sha256=OTU7aeLrPiMUTnyCT-J7ygng3beI9tdA0MmypOavEkw,156123
16
16
  spacr/gui_utils.py,sha256=F6KfNY3OqNkvfkOP1rxwBha5IOdLVyBgqZYPw3xPLes,42293
17
- spacr/io.py,sha256=SYLhupKnOJJscNSGE4N67E32-ywhwrjRccIfZrL38Uk,157966
17
+ spacr/io.py,sha256=g6vybQeGLdTXrAqEjM6X1aoB6lyZVUq6pTI0ASppX4g,159257
18
18
  spacr/logger.py,sha256=lJhTqt-_wfAunCPl93xE65Wr9Y1oIHJWaZMjunHUeIw,1538
19
19
  spacr/measure.py,sha256=nYvrfVfCIqD1AUk4QBE2jtpeSFtLdfUcnkhkqf9G4xQ,60877
20
20
  spacr/mediar.py,sha256=p0F515eFbm6_rePSnChsgqrgH-H5Sr_3zWrghtOnAUg,14863
21
21
  spacr/ml.py,sha256=XCRZeX7UkbMctQICIoskeWVx8CCmmCoHNauUOAkfFq0,91692
22
22
  spacr/openai.py,sha256=5vBZ3Jl2llYcW3oaTEXgdyCB2aJujMUIO5K038z7w_A,1246
23
- spacr/plot.py,sha256=Y1ON8Bu-FsZZZasXIK7nvnOohFzucCvFhyPE2bDGz1A,167340
23
+ spacr/plot.py,sha256=76E1CZpsmNeNtbnkXJtgcVOesq4voL7XkaUnD74RDMk,169418
24
24
  spacr/sequencing.py,sha256=EY12RdW5QRKpHDRQCw1QoAlxCq8FK2v6WoVa5uuDBXQ,26745
25
- spacr/settings.py,sha256=gT0FEP6anfhM6sbFofmLRhOwaQptgpcI18VX6nRqmtQ,87661
25
+ spacr/settings.py,sha256=38MClzEv-eP4Kfo_UJ_jlIzj0Vos3TY5-scPlBpolYI,88520
26
26
  spacr/sim.py,sha256=1xKhXimNU3ukzIw-3l9cF3Znc_brW8h20yv8fSTzvss,71173
27
- spacr/sp_stats.py,sha256=mbhwsyIqt5upsSD346qGjdCw7CFBa0tIS7zHU9e0jNI,9536
28
- spacr/spacr_cellpose.py,sha256=RBHMs2vwXcfkj0xqAULpALyzJYXddSRycgZSzmwI7v0,14755
27
+ spacr/sp_stats.py,sha256=C93Xe5fphQOKthw4Tmj8pHx-Nb1houIL-YYVIfmnQPg,9535
28
+ spacr/spacr_cellpose.py,sha256=AvnyD2qoj-lUqhICeTpfhyk9T2hCjZrpBXn2iKh1EYE,16785
29
29
  spacr/submodules.py,sha256=Z2i4kv_rWdxqoXsOKCF7BaSXtvaCZB69Ow8_FQBnZsY,83093
30
30
  spacr/timelapse.py,sha256=-5ZupTsCCpbenIQ2zsUmnwXh45B82fO-gPrSXOxu2s8,42980
31
31
  spacr/toxo.py,sha256=GoNfgyH-NJx3WOzNQPgzODir7Jp65fs7UM46XpzcrUo,26056
32
- spacr/utils.py,sha256=cw5zM6zpFWWUZQKwtYvXc_rNfBMW2ldbnlw8s6f6bFQ,234397
32
+ spacr/utils.py,sha256=PulmDqENBFYKgN2fjcVSB39B_9CwwGDiBi9FOI55zQs,238470
33
33
  spacr/version.py,sha256=axH5tnGwtgSnJHb5IDhiu4Zjk5GhLyAEDRe-rnaoFOA,409
34
34
  spacr/resources/data/lopit.csv,sha256=ERI5f9W8RdJGiSx_khoaylD374f8kmvLia1xjhD_mII,4421709
35
35
  spacr/resources/data/toxoplasma_metadata.csv,sha256=9TXx0VlClDHAxQmaLhoklE8NuETduXaGHZjhR_6lZfs,2969409
@@ -103,9 +103,9 @@ spacr/resources/icons/umap.png,sha256=dOLF3DeLYy9k0nkUybiZMe1wzHQwLJFRmgccppw-8b
103
103
  spacr/resources/images/plate1_E01_T0001F001L01A01Z01C02.tif,sha256=Tl0ZUfZ_AYAbu0up_nO0tPRtF1BxXhWQ3T3pURBCCRo,7958528
104
104
  spacr/resources/images/plate1_E01_T0001F001L01A02Z01C01.tif,sha256=m8N-V71rA1TT4dFlENNg8s0Q0YEXXs8slIn7yObmZJQ,7958528
105
105
  spacr/resources/images/plate1_E01_T0001F001L01A03Z01C03.tif,sha256=Pbhk7xn-KUP6RSIhJsxQcrHFImBm3GEpLkzx7WOc-5M,7958528
106
- spacr-0.9.3.dist-info/LICENSE,sha256=t0Pov6pnK8thLteoF4xZGmdCwe5mhNwl3OXxLYTGD9U,1081
107
- spacr-0.9.3.dist-info/METADATA,sha256=67z09Wghste6IUXWFchysvsqH38M6GVnr9IgHYixVNg,10088
108
- spacr-0.9.3.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
109
- spacr-0.9.3.dist-info/entry_points.txt,sha256=BMC0ql9aNNpv8lUZ8sgDLQMsqaVnX5L535gEhKUP5ho,296
110
- spacr-0.9.3.dist-info/top_level.txt,sha256=GJPU8FgwRXGzKeut6JopsSRY2R8T3i9lDgya42tLInY,6
111
- spacr-0.9.3.dist-info/RECORD,,
106
+ spacr-0.9.7.dist-info/LICENSE,sha256=t0Pov6pnK8thLteoF4xZGmdCwe5mhNwl3OXxLYTGD9U,1081
107
+ spacr-0.9.7.dist-info/METADATA,sha256=qvWrnXZ6_xNlVmfV-z3aCfoL406UiqnI1o4hn55NR64,10356
108
+ spacr-0.9.7.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
109
+ spacr-0.9.7.dist-info/entry_points.txt,sha256=BMC0ql9aNNpv8lUZ8sgDLQMsqaVnX5L535gEhKUP5ho,296
110
+ spacr-0.9.7.dist-info/top_level.txt,sha256=GJPU8FgwRXGzKeut6JopsSRY2R8T3i9lDgya42tLInY,6
111
+ spacr-0.9.7.dist-info/RECORD,,
File without changes
File without changes