celldetective 1.1.1.post3__py3-none-any.whl → 1.2.0__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.
Files changed (42) hide show
  1. celldetective/__init__.py +2 -1
  2. celldetective/__main__.py +17 -0
  3. celldetective/extra_properties.py +62 -34
  4. celldetective/gui/__init__.py +1 -0
  5. celldetective/gui/analyze_block.py +2 -1
  6. celldetective/gui/classifier_widget.py +18 -10
  7. celldetective/gui/control_panel.py +57 -6
  8. celldetective/gui/layouts.py +14 -11
  9. celldetective/gui/neighborhood_options.py +21 -13
  10. celldetective/gui/plot_signals_ui.py +39 -11
  11. celldetective/gui/process_block.py +413 -95
  12. celldetective/gui/retrain_segmentation_model_options.py +17 -4
  13. celldetective/gui/retrain_signal_model_options.py +106 -6
  14. celldetective/gui/signal_annotator.py +110 -30
  15. celldetective/gui/signal_annotator2.py +2708 -0
  16. celldetective/gui/signal_annotator_options.py +3 -1
  17. celldetective/gui/survival_ui.py +15 -6
  18. celldetective/gui/tableUI.py +248 -43
  19. celldetective/io.py +598 -416
  20. celldetective/measure.py +919 -969
  21. celldetective/models/pair_signal_detection/blank +0 -0
  22. celldetective/neighborhood.py +482 -340
  23. celldetective/preprocessing.py +81 -61
  24. celldetective/relative_measurements.py +648 -0
  25. celldetective/scripts/analyze_signals.py +1 -1
  26. celldetective/scripts/measure_cells.py +28 -8
  27. celldetective/scripts/measure_relative.py +103 -0
  28. celldetective/scripts/segment_cells.py +5 -5
  29. celldetective/scripts/track_cells.py +4 -1
  30. celldetective/scripts/train_segmentation_model.py +23 -18
  31. celldetective/scripts/train_signal_model.py +33 -0
  32. celldetective/segmentation.py +67 -29
  33. celldetective/signals.py +402 -8
  34. celldetective/tracking.py +8 -2
  35. celldetective/utils.py +144 -12
  36. {celldetective-1.1.1.post3.dist-info → celldetective-1.2.0.dist-info}/METADATA +8 -8
  37. {celldetective-1.1.1.post3.dist-info → celldetective-1.2.0.dist-info}/RECORD +42 -38
  38. {celldetective-1.1.1.post3.dist-info → celldetective-1.2.0.dist-info}/WHEEL +1 -1
  39. tests/test_segmentation.py +1 -1
  40. {celldetective-1.1.1.post3.dist-info → celldetective-1.2.0.dist-info}/LICENSE +0 -0
  41. {celldetective-1.1.1.post3.dist-info → celldetective-1.2.0.dist-info}/entry_points.txt +0 -0
  42. {celldetective-1.1.1.post3.dist-info → celldetective-1.2.0.dist-info}/top_level.txt +0 -0
celldetective/io.py CHANGED
@@ -26,10 +26,11 @@ from stardist import fill_label_holes
26
26
  from celldetective.utils import interpolate_nan
27
27
  from scipy.interpolate import griddata
28
28
 
29
+
29
30
  def get_experiment_wells(experiment):
30
31
 
31
32
  """
32
- Retrieves the list of well directories from a given experiment directory, sorted
33
+ Retrieves the list of well directories from a given experiment directory, sorted
33
34
  naturally and returned as a NumPy array of strings.
34
35
 
35
36
  Parameters
@@ -40,104 +41,107 @@ def get_experiment_wells(experiment):
40
41
  Returns
41
42
  -------
42
43
  np.ndarray
43
- An array of strings, each representing the full path to a well directory within the specified
44
+ An array of strings, each representing the full path to a well directory within the specified
44
45
  experiment. The array is empty if no well directories are found.
45
46
 
46
47
  Notes
47
48
  -----
48
- - The function assumes well directories are prefixed with 'W' and uses this to filter directories
49
+ - The function assumes well directories are prefixed with 'W' and uses this to filter directories
49
50
  within the experiment folder.
50
51
 
51
- - Natural sorting is applied to the list of wells to ensure that the order is intuitive (e.g., 'W2'
52
- comes before 'W10'). This sorting method is especially useful when dealing with numerical sequences
52
+ - Natural sorting is applied to the list of wells to ensure that the order is intuitive (e.g., 'W2'
53
+ comes before 'W10'). This sorting method is especially useful when dealing with numerical sequences
53
54
  that are part of the directory names.
54
-
55
+
55
56
  """
56
57
 
57
58
  if not experiment.endswith(os.sep):
58
59
  experiment += os.sep
59
-
60
+
60
61
  wells = natsorted(glob(experiment + "W*" + os.sep))
61
- return np.array(wells,dtype=str)
62
+ return np.array(wells, dtype=str)
63
+
62
64
 
63
65
  def get_config(experiment):
64
66
 
65
67
  if not experiment.endswith(os.sep):
66
68
  experiment += os.sep
67
-
69
+
68
70
  config = experiment + 'config.ini'
69
71
  config = rf"{config}"
70
- assert os.path.exists(config),'The experiment configuration could not be located...'
71
- return config
72
+ assert os.path.exists(config), 'The experiment configuration could not be located...'
73
+ return config
72
74
 
73
75
 
74
76
  def get_spatial_calibration(experiment):
75
77
 
76
78
 
77
79
  config = get_config(experiment)
78
- PxToUm = float(ConfigSectionMap(config,"MovieSettings")["pxtoum"])
79
-
80
+ PxToUm = float(ConfigSectionMap(config, "MovieSettings")["pxtoum"])
81
+
80
82
  return PxToUm
81
83
 
84
+
82
85
  def get_temporal_calibration(experiment):
83
86
 
84
87
  config = get_config(experiment)
85
- FrameToMin = float(ConfigSectionMap(config,"MovieSettings")["frametomin"])
86
-
88
+ FrameToMin = float(ConfigSectionMap(config, "MovieSettings")["frametomin"])
89
+
87
90
  return FrameToMin
88
91
 
92
+
89
93
  def get_experiment_concentrations(experiment, dtype=str):
90
94
 
91
95
 
92
96
  config = get_config(experiment)
93
97
  wells = get_experiment_wells(experiment)
94
98
  nbr_of_wells = len(wells)
95
-
96
- concentrations = ConfigSectionMap(config,"Labels")["concentrations"].split(",")
99
+
100
+ concentrations = ConfigSectionMap(config, "Labels")["concentrations"].split(",")
97
101
  if nbr_of_wells != len(concentrations):
98
- concentrations = [str(s) for s in np.linspace(0,nbr_of_wells-1,nbr_of_wells)]
99
-
102
+ concentrations = [str(s) for s in np.linspace(0, nbr_of_wells - 1, nbr_of_wells)]
103
+
100
104
  return np.array([dtype(c) for c in concentrations])
101
105
 
106
+
102
107
  def get_experiment_cell_types(experiment, dtype=str):
103
-
104
108
  config = get_config(experiment)
105
109
  wells = get_experiment_wells(experiment)
106
110
  nbr_of_wells = len(wells)
107
-
108
- cell_types = ConfigSectionMap(config,"Labels")["cell_types"].split(",")
111
+
112
+ cell_types = ConfigSectionMap(config, "Labels")["cell_types"].split(",")
109
113
  if nbr_of_wells != len(cell_types):
110
- cell_types = [str(s) for s in np.linspace(0,nbr_of_wells-1,nbr_of_wells)]
111
-
114
+ cell_types = [str(s) for s in np.linspace(0, nbr_of_wells - 1, nbr_of_wells)]
115
+
112
116
  return np.array([dtype(c) for c in cell_types])
113
117
 
118
+
114
119
  def get_experiment_antibodies(experiment, dtype=str):
115
120
 
116
121
  config = get_config(experiment)
117
122
  wells = get_experiment_wells(experiment)
118
123
  nbr_of_wells = len(wells)
119
-
120
- antibodies = ConfigSectionMap(config,"Labels")["antibodies"].split(",")
124
+
125
+ antibodies = ConfigSectionMap(config, "Labels")["antibodies"].split(",")
121
126
  if nbr_of_wells != len(antibodies):
122
- antibodies = [str(s) for s in np.linspace(0,nbr_of_wells-1,nbr_of_wells)]
123
-
127
+ antibodies = [str(s) for s in np.linspace(0, nbr_of_wells - 1, nbr_of_wells)]
128
+
124
129
  return np.array([dtype(c) for c in antibodies])
125
130
 
131
+
126
132
  def get_experiment_pharmaceutical_agents(experiment, dtype=str):
127
-
128
133
  config = get_config(experiment)
129
134
  wells = get_experiment_wells(experiment)
130
135
  nbr_of_wells = len(wells)
131
-
132
- pharmaceutical_agents = ConfigSectionMap(config,"Labels")["pharmaceutical_agents"].split(",")
136
+
137
+ pharmaceutical_agents = ConfigSectionMap(config, "Labels")["pharmaceutical_agents"].split(",")
133
138
  if nbr_of_wells != len(pharmaceutical_agents):
134
- pharmaceutical_agents = [str(s) for s in np.linspace(0,nbr_of_wells-1,nbr_of_wells)]
135
-
139
+ pharmaceutical_agents = [str(s) for s in np.linspace(0, nbr_of_wells - 1, nbr_of_wells)]
140
+
136
141
  return np.array([dtype(c) for c in pharmaceutical_agents])
137
142
 
138
143
 
139
144
  def interpret_wells_and_positions(experiment, well_option, position_option):
140
-
141
145
  """
142
146
  Interpret well and position options for a given experiment.
143
147
 
@@ -172,26 +176,26 @@ def interpret_wells_and_positions(experiment, well_option, position_option):
172
176
  >>> experiment = ... # Some experiment object
173
177
  >>> interpret_wells_and_positions(experiment, '*', '*')
174
178
  (array([0, 1, 2, ..., n-1]), None)
175
-
179
+
176
180
  >>> interpret_wells_and_positions(experiment, 2, '*')
177
181
  ([2], None)
178
-
182
+
179
183
  >>> interpret_wells_and_positions(experiment, [1, 3, 5], 2)
180
184
  ([1, 3, 5], array([2]))
181
185
 
182
186
  """
183
-
187
+
184
188
  wells = get_experiment_wells(experiment)
185
189
  nbr_of_wells = len(wells)
186
190
 
187
- if well_option=='*':
191
+ if well_option == '*':
188
192
  well_indices = np.arange(nbr_of_wells)
189
193
  elif isinstance(well_option, int) or isinstance(well_option, np.int_):
190
194
  well_indices = [int(well_option)]
191
195
  elif isinstance(well_option, list):
192
196
  well_indices = well_option
193
-
194
- if position_option=='*':
197
+
198
+ if position_option == '*':
195
199
  position_indices = None
196
200
  elif isinstance(position_option, int):
197
201
  position_indices = np.array([position_option], dtype=int)
@@ -199,9 +203,9 @@ def interpret_wells_and_positions(experiment, well_option, position_option):
199
203
  position_indices = position_option
200
204
 
201
205
  return well_indices, position_indices
202
-
206
+
207
+
203
208
  def extract_well_name_and_number(well):
204
-
205
209
  """
206
210
  Extract the well name and number from a given well path.
207
211
 
@@ -237,10 +241,11 @@ def extract_well_name_and_number(well):
237
241
  split_well_path = well.split(os.sep)
238
242
  split_well_path = list(filter(None, split_well_path))
239
243
  well_name = split_well_path[-1]
240
- well_number = int(split_well_path[-1].replace('W',''))
241
-
244
+ well_number = int(split_well_path[-1].replace('W', ''))
245
+
242
246
  return well_name, well_number
243
247
 
248
+
244
249
  def extract_position_name(pos):
245
250
 
246
251
  """
@@ -274,17 +279,18 @@ def extract_position_name(pos):
274
279
  split_pos_path = pos.split(os.sep)
275
280
  split_pos_path = list(filter(None, split_pos_path))
276
281
  pos_name = split_pos_path[-1]
277
-
282
+
278
283
  return pos_name
279
284
 
285
+
280
286
  def get_position_table(pos, population, return_path=False):
281
287
 
282
288
  """
283
289
  Retrieves the data table for a specified population at a given position, optionally returning the table's file path.
284
290
 
285
- This function locates and loads a CSV data table associated with a specific population (e.g., 'targets', 'cells')
286
- from a specified position directory. The position directory should contain an 'output/tables' subdirectory where
287
- the CSV file named 'trajectories_{population}.csv' is expected to be found. If the file exists, it is loaded into
291
+ This function locates and loads a CSV data table associated with a specific population (e.g., 'targets', 'cells')
292
+ from a specified position directory. The position directory should contain an 'output/tables' subdirectory where
293
+ the CSV file named 'trajectories_{population}.csv' is expected to be found. If the file exists, it is loaded into
288
294
  a pandas DataFrame; otherwise, None is returned.
289
295
 
290
296
  Parameters
@@ -292,17 +298,17 @@ def get_position_table(pos, population, return_path=False):
292
298
  pos : str
293
299
  The path to the position directory from which to load the data table.
294
300
  population : str
295
- The name of the population for which the data table is to be retrieved. This name is used to construct the
301
+ The name of the population for which the data table is to be retrieved. This name is used to construct the
296
302
  file name of the CSV file to be loaded.
297
303
  return_path : bool, optional
298
- If True, returns a tuple containing the loaded data table (or None) and the path to the CSV file. If False,
304
+ If True, returns a tuple containing the loaded data table (or None) and the path to the CSV file. If False,
299
305
  only the loaded data table (or None) is returned (default is False).
300
306
 
301
307
  Returns
302
308
  -------
303
309
  pandas.DataFrame or None, or (pandas.DataFrame or None, str)
304
- If return_path is False, returns the loaded data table as a pandas DataFrame, or None if the table file does
305
- not exist. If return_path is True, returns a tuple where the first element is the data table (or None) and the
310
+ If return_path is False, returns the loaded data table as a pandas DataFrame, or None if the table file does
311
+ not exist. If return_path is True, returns a tuple where the first element is the data table (or None) and the
306
312
  second element is the path to the CSV file.
307
313
 
308
314
  Examples
@@ -312,13 +318,13 @@ def get_position_table(pos, population, return_path=False):
312
318
 
313
319
  >>> df_pos, table_path = get_position_table('/path/to/position', 'targets', return_path=True)
314
320
  # This will load the 'trajectories_targets.csv' table and also return the path to the CSV file.
315
-
321
+
316
322
  """
317
-
323
+
318
324
  if not pos.endswith(os.sep):
319
- table = os.sep.join([pos,'output','tables',f'trajectories_{population}.csv'])
325
+ table = os.sep.join([pos, 'output', 'tables', f'trajectories_{population}.csv'])
320
326
  else:
321
- table = pos + os.sep.join(['output','tables',f'trajectories_{population}.csv'])
327
+ table = pos + os.sep.join(['output', 'tables', f'trajectories_{population}.csv'])
322
328
 
323
329
  if os.path.exists(table):
324
330
  df_pos = pd.read_csv(table, low_memory=False)
@@ -331,13 +337,13 @@ def get_position_table(pos, population, return_path=False):
331
337
  return df_pos
332
338
 
333
339
  def get_position_pickle(pos, population, return_path=False):
334
-
340
+
335
341
  """
336
342
  Retrieves the data table for a specified population at a given position, optionally returning the table's file path.
337
343
 
338
- This function locates and loads a CSV data table associated with a specific population (e.g., 'targets', 'cells')
339
- from a specified position directory. The position directory should contain an 'output/tables' subdirectory where
340
- the CSV file named 'trajectories_{population}.csv' is expected to be found. If the file exists, it is loaded into
344
+ This function locates and loads a CSV data table associated with a specific population (e.g., 'targets', 'cells')
345
+ from a specified position directory. The position directory should contain an 'output/tables' subdirectory where
346
+ the CSV file named 'trajectories_{population}.csv' is expected to be found. If the file exists, it is loaded into
341
347
  a pandas DataFrame; otherwise, None is returned.
342
348
 
343
349
  Parameters
@@ -345,17 +351,17 @@ def get_position_pickle(pos, population, return_path=False):
345
351
  pos : str
346
352
  The path to the position directory from which to load the data table.
347
353
  population : str
348
- The name of the population for which the data table is to be retrieved. This name is used to construct the
354
+ The name of the population for which the data table is to be retrieved. This name is used to construct the
349
355
  file name of the CSV file to be loaded.
350
356
  return_path : bool, optional
351
- If True, returns a tuple containing the loaded data table (or None) and the path to the CSV file. If False,
357
+ If True, returns a tuple containing the loaded data table (or None) and the path to the CSV file. If False,
352
358
  only the loaded data table (or None) is returned (default is False).
353
359
 
354
360
  Returns
355
361
  -------
356
362
  pandas.DataFrame or None, or (pandas.DataFrame or None, str)
357
- If return_path is False, returns the loaded data table as a pandas DataFrame, or None if the table file does
358
- not exist. If return_path is True, returns a tuple where the first element is the data table (or None) and the
363
+ If return_path is False, returns the loaded data table as a pandas DataFrame, or None if the table file does
364
+ not exist. If return_path is True, returns a tuple where the first element is the data table (or None) and the
359
365
  second element is the path to the CSV file.
360
366
 
361
367
  Examples
@@ -365,19 +371,19 @@ def get_position_pickle(pos, population, return_path=False):
365
371
 
366
372
  >>> df_pos, table_path = get_position_table('/path/to/position', 'targets', return_path=True)
367
373
  # This will load the 'trajectories_targets.csv' table and also return the path to the CSV file.
368
-
374
+
369
375
  """
370
-
376
+
371
377
  if not pos.endswith(os.sep):
372
378
  table = os.sep.join([pos,'output','tables',f'trajectories_{population}.pkl'])
373
379
  else:
374
- table = pos + os.sep.join(['output','tables',f'trajectories_{population}.pkl'])
380
+ table = pos + os.sep.join(['output','tables',f'trajectories_{population}.pkl'])
375
381
 
376
382
  if os.path.exists(table):
377
383
  df_pos = np.load(table, allow_pickle=True)
378
384
  else:
379
385
  df_pos = None
380
-
386
+
381
387
  if return_path:
382
388
  return df_pos, table
383
389
  else:
@@ -422,29 +428,27 @@ def get_position_movie_path(pos, prefix=''):
422
428
 
423
429
 
424
430
  if not pos.endswith(os.sep):
425
- pos+=os.sep
426
- movies = glob(pos+os.sep.join(['movie',prefix+'*.tif']))
427
- if len(movies)>0:
431
+ pos += os.sep
432
+ movies = glob(pos + os.sep.join(['movie', prefix + '*.tif']))
433
+ if len(movies) > 0:
428
434
  stack_path = movies[0]
429
435
  else:
430
436
  stack_path = None
431
-
432
- return stack_path
433
-
434
437
 
438
+ return stack_path
435
439
 
436
- def load_experiment_tables(experiment, population='targets', well_option='*',position_option='*', return_pos_info=False):
437
-
438
440
 
441
+ def load_experiment_tables(experiment, population='targets', well_option='*', position_option='*',
442
+ return_pos_info=False, load_pickle=False):
439
443
  """
440
- Loads and aggregates data tables for specified wells and positions within an experiment,
444
+ Loads and aggregates data tables for specified wells and positions within an experiment,
441
445
  optionally returning position information alongside the aggregated data table.
442
446
 
443
- This function collects data from tables associated with specific population types across
444
- various wells and positions within an experiment. It uses the experiment's configuration
445
- to gather metadata such as movie prefix, concentrations, cell types, antibodies, and
446
- pharmaceutical agents. Users can specify which wells and positions to include in the
447
- aggregation through pattern matching, and whether to include detailed position information
447
+ This function collects data from tables associated with specific population types across
448
+ various wells and positions within an experiment. It uses the experiment's configuration
449
+ to gather metadata such as movie prefix, concentrations, cell types, antibodies, and
450
+ pharmaceutical agents. Users can specify which wells and positions to include in the
451
+ aggregation through pattern matching, and whether to include detailed position information
448
452
  in the output.
449
453
 
450
454
  Parameters
@@ -458,15 +462,15 @@ def load_experiment_tables(experiment, population='targets', well_option='*',pos
458
462
  position_option : str, optional
459
463
  A pattern to specify which positions to include (default is '*', which includes all positions).
460
464
  return_pos_info : bool, optional
461
- If True, returns a tuple where the first element is the aggregated data table and the
465
+ If True, returns a tuple where the first element is the aggregated data table and the
462
466
  second element is detailed position information (default is False).
463
467
 
464
468
  Returns
465
469
  -------
466
470
  pandas.DataFrame or (pandas.DataFrame, pandas.DataFrame)
467
- If return_pos_info is False, returns a pandas DataFrame aggregating the data from the
468
- specified tables. If return_pos_info is True, returns a tuple where the first element
469
- is the aggregated data table and the second element is a DataFrame with detailed position
471
+ If return_pos_info is False, returns a pandas DataFrame aggregating the data from the
472
+ specified tables. If return_pos_info is True, returns a tuple where the first element
473
+ is the aggregated data table and the second element is a DataFrame with detailed position
470
474
  information.
471
475
 
472
476
  Raises
@@ -478,50 +482,49 @@ def load_experiment_tables(experiment, population='targets', well_option='*',pos
478
482
 
479
483
  Notes
480
484
  -----
481
- - This function assumes that the naming conventions and directory structure of the experiment
485
+ - This function assumes that the naming conventions and directory structure of the experiment
482
486
  follow a specific format, as outlined in the experiment's configuration file.
483
- - The function utilizes several helper functions to extract metadata, interpret well and
484
- position patterns, and load individual position tables. Errors in these helper functions
487
+ - The function utilizes several helper functions to extract metadata, interpret well and
488
+ position patterns, and load individual position tables. Errors in these helper functions
485
489
  may propagate up and affect the behavior of this function.
486
490
 
487
491
  Examples
488
492
  --------
489
493
  >>> load_experiment_tables('/path/to/experiment', population='targets', well_option='W1', position_option='1-*')
490
494
  # This will load and aggregate tables for the 'targets' population within well 'W1' and positions matching '1-*'.
491
-
492
- """
493
495
 
496
+ """
494
497
 
495
498
  config = get_config(experiment)
496
499
  wells = get_experiment_wells(experiment)
497
500
 
498
- movie_prefix = ConfigSectionMap(config,"MovieSettings")["movie_prefix"]
501
+ movie_prefix = ConfigSectionMap(config, "MovieSettings")["movie_prefix"]
499
502
  concentrations = get_experiment_concentrations(experiment, dtype=float)
500
503
  cell_types = get_experiment_cell_types(experiment)
501
504
  antibodies = get_experiment_antibodies(experiment)
502
505
  pharmaceutical_agents = get_experiment_pharmaceutical_agents(experiment)
503
- well_labels = _extract_labels_from_config(config,len(wells))
504
-
506
+ well_labels = _extract_labels_from_config(config, len(wells))
507
+
505
508
  well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, position_option)
506
509
 
507
510
  df = []
508
511
  df_pos_info = []
509
512
  real_well_index = 0
510
-
513
+
511
514
  for k, well_path in enumerate(tqdm(wells[well_indices])):
512
-
513
- any_table = False # assume no table
514
-
515
+
516
+ any_table = False # assume no table
517
+
515
518
  well_name, well_number = extract_well_name_and_number(well_path)
516
519
  widx = well_indices[k]
517
520
 
518
521
  well_alias = well_labels[widx]
519
-
522
+
520
523
  well_concentration = concentrations[widx]
521
524
  well_antibody = antibodies[widx]
522
525
  well_cell_type = cell_types[widx]
523
526
  well_pharmaceutical_agent = pharmaceutical_agents[widx]
524
-
527
+
525
528
  positions = get_positions_in_well(well_path)
526
529
  if position_indices is not None:
527
530
  try:
@@ -531,13 +534,17 @@ def load_experiment_tables(experiment, population='targets', well_option='*',pos
531
534
  continue
532
535
 
533
536
  real_pos_index = 0
534
- for pidx,pos_path in enumerate(positions):
535
-
537
+ for pidx, pos_path in enumerate(positions):
538
+
536
539
  pos_name = extract_position_name(pos_path)
537
-
540
+
538
541
  stack_path = get_position_movie_path(pos_path, prefix=movie_prefix)
539
-
540
- df_pos,table = get_position_table(pos_path, population=population, return_path=True)
542
+
543
+ if not load_pickle:
544
+ df_pos, table = get_position_table(pos_path, population=population, return_path=True)
545
+ else:
546
+ df_pos, table = get_position_pickle(pos_path, population=population, return_path=True)
547
+
541
548
  if df_pos is not None:
542
549
 
543
550
  df_pos['position'] = pos_path
@@ -550,25 +557,28 @@ def load_experiment_tables(experiment, population='targets', well_option='*',pos
550
557
  df_pos['antibody'] = well_antibody
551
558
  df_pos['cell_type'] = well_cell_type
552
559
  df_pos['pharmaceutical_agent'] = well_pharmaceutical_agent
553
-
560
+
554
561
  df.append(df_pos)
555
562
  any_table = True
556
-
557
- df_pos_info.append({'pos_path': pos_path, 'pos_index': real_pos_index, 'pos_name': pos_name, 'table_path': table, 'stack_path': stack_path,
558
- 'well_path': well_path, 'well_index': real_well_index, 'well_name': well_name, 'well_number': well_number, 'well_alias': well_alias})
559
-
560
- real_pos_index+=1
561
-
563
+
564
+ df_pos_info.append(
565
+ {'pos_path': pos_path, 'pos_index': real_pos_index, 'pos_name': pos_name, 'table_path': table,
566
+ 'stack_path': stack_path,
567
+ 'well_path': well_path, 'well_index': real_well_index, 'well_name': well_name,
568
+ 'well_number': well_number, 'well_alias': well_alias})
569
+
570
+ real_pos_index += 1
571
+
562
572
  if any_table:
563
573
  real_well_index += 1
564
-
574
+
565
575
  df_pos_info = pd.DataFrame(df_pos_info)
566
- if len(df)>0:
576
+ if len(df) > 0:
567
577
  df = pd.concat(df)
568
578
  df = df.reset_index(drop=True)
569
579
  else:
570
580
  df = None
571
-
581
+
572
582
  if return_pos_info:
573
583
  return df, df_pos_info
574
584
  else:
@@ -614,15 +624,15 @@ def locate_stack(position, prefix='Aligned'):
614
624
  """
615
625
 
616
626
  if not position.endswith(os.sep):
617
- position+=os.sep
627
+ position += os.sep
618
628
 
619
- stack_path = glob(position+os.sep.join(['movie', f'{prefix}*.tif']))
620
- assert len(stack_path)>0,f"No movie with prefix {prefix} found..."
621
- stack = imread(stack_path[0].replace('\\','/'))
622
- if stack.ndim==4:
629
+ stack_path = glob(position + os.sep.join(['movie', f'{prefix}*.tif']))
630
+ assert len(stack_path) > 0, f"No movie with prefix {prefix} found..."
631
+ stack = imread(stack_path[0].replace('\\', '/'), is_mmstack=False)
632
+ if stack.ndim == 4:
623
633
  stack = np.moveaxis(stack, 1, -1)
624
- elif stack.ndim==3:
625
- stack = stack[:,:,:,np.newaxis]
634
+ elif stack.ndim == 3:
635
+ stack = stack[:, :, :, np.newaxis]
626
636
 
627
637
  return stack
628
638
 
@@ -637,7 +647,7 @@ def locate_labels(position, population='target'):
637
647
  position : str
638
648
  The position folder within the well where the stack is located.
639
649
  population : str, optional
640
- The population for which to locate the labels.
650
+ The population for which to locate the labels.
641
651
  Valid options are 'target' and 'effector'.
642
652
  The default is 'target'.
643
653
 
@@ -661,13 +671,13 @@ def locate_labels(position, population='target'):
661
671
  """
662
672
 
663
673
  if not position.endswith(os.sep):
664
- position+=os.sep
665
-
666
- if population.lower()=="target" or population.lower()=="targets":
667
- label_path = natsorted(glob(position+os.sep.join(["labels_targets", "*.tif"])))
668
- elif population.lower()=="effector" or population.lower()=="effectors":
669
- label_path = natsorted(glob(position+os.sep.join(["labels_effectors", "*.tif"])))
670
- labels = np.array([imread(i.replace('\\','/')) for i in label_path])
674
+ position += os.sep
675
+
676
+ if population.lower() == "target" or population.lower() == "targets":
677
+ label_path = natsorted(glob(position + os.sep.join(["labels_targets", "*.tif"])))
678
+ elif population.lower() == "effector" or population.lower() == "effectors":
679
+ label_path = natsorted(glob(position + os.sep.join(["labels_effectors", "*.tif"])))
680
+ labels = np.array([imread(i.replace('\\', '/')) for i in label_path])
671
681
 
672
682
  return labels
673
683
 
@@ -710,20 +720,20 @@ def locate_stack_and_labels(position, prefix='Aligned', population="target"):
710
720
  --------
711
721
  >>> stack, labels = locate_stack_and_labels(position, prefix='Aligned', population="target")
712
722
  # Locate and load the stack and segmentation labels for further processing.
713
-
723
+
714
724
  """
715
725
 
716
- position = position.replace('\\','/')
726
+ position = position.replace('\\', '/')
717
727
  labels = locate_labels(position, population=population)
718
728
  stack = locate_stack(position, prefix=prefix)
719
- assert len(stack)==len(labels),f"The shape of the stack {stack.shape} does not match with the shape of the labels {labels.shape}"
729
+ assert len(stack) == len(
730
+ labels), f"The shape of the stack {stack.shape} does not match with the shape of the labels {labels.shape}"
720
731
 
721
- return stack,labels
732
+ return stack, labels
722
733
 
723
734
  def load_tracking_data(position, prefix="Aligned", population="target"):
724
-
725
735
  """
726
-
736
+
727
737
  Load the tracking data, labels, and stack for a given position and population.
728
738
 
729
739
  Parameters
@@ -758,15 +768,15 @@ def load_tracking_data(position, prefix="Aligned", population="target"):
758
768
 
759
769
  """
760
770
 
761
- position = position.replace('\\','/')
762
- if population.lower()=="target" or population.lower()=="targets":
763
- trajectories = pd.read_csv(position+os.sep.join(['output', 'tables', 'trajectories_targets.csv']))
764
- elif population.lower()=="effector" or population.lower()=="effectors":
765
- trajectories = pd.read_csv(position+os.sep.join(['output', 'tables', 'trajectories_effectors.csv']))
771
+ position = position.replace('\\', '/')
772
+ if population.lower() == "target" or population.lower() == "targets":
773
+ trajectories = pd.read_csv(position + os.sep.join(['output', 'tables', 'trajectories_targets.csv']))
774
+ elif population.lower() == "effector" or population.lower() == "effectors":
775
+ trajectories = pd.read_csv(position + os.sep.join(['output', 'tables', 'trajectories_effectors.csv']))
766
776
 
767
- stack,labels = locate_stack_and_labels(position, prefix=prefix, population=population)
777
+ stack, labels = locate_stack_and_labels(position, prefix=prefix, population=population)
768
778
 
769
- return trajectories,labels,stack
779
+ return trajectories, labels, stack
770
780
 
771
781
 
772
782
  def auto_load_number_of_frames(stack_path):
@@ -797,10 +807,14 @@ def auto_load_number_of_frames(stack_path):
797
807
  --------
798
808
  >>> len_movie = auto_load_number_of_frames(stack_path)
799
809
  # Automatically estimate the number of frames in the stack.
800
-
810
+
801
811
  """
802
812
 
803
813
  # Try to estimate automatically # frames
814
+
815
+ if stack_path is None:
816
+ return None
817
+
804
818
  stack_path = stack_path.replace('\\','/')
805
819
 
806
820
  with TiffFile(stack_path) as tif:
@@ -816,7 +830,7 @@ def auto_load_number_of_frames(stack_path):
816
830
  try:
817
831
  # Try nframes
818
832
  nslices = int(attr[np.argmax([s.startswith("frames") for s in attr])].split("=")[-1])
819
- if nslices>1:
833
+ if nslices > 1:
820
834
  len_movie = nslices
821
835
  print(f"Auto-detected movie length movie: {len_movie}")
822
836
  else:
@@ -836,27 +850,35 @@ def auto_load_number_of_frames(stack_path):
836
850
  del img_desc;
837
851
  except:
838
852
  pass
853
+
854
+ if 'len_movie' not in locals():
855
+ stack = imread(stack_path)
856
+ len_movie = len(stack)
857
+ del stack
839
858
  gc.collect()
840
859
 
860
+ print(f'Automatically detected stack length: {len_movie}...')
861
+
841
862
  return len_movie if 'len_movie' in locals() else None
842
863
 
864
+
843
865
  def parse_isotropic_radii(string):
844
866
  sections = re.split(',| ', string)
845
867
  radii = []
846
- for k,s in enumerate(sections):
868
+ for k, s in enumerate(sections):
847
869
  if s.isdigit():
848
870
  radii.append(int(s))
849
871
  if '[' in s:
850
- ring = [int(s.replace('[','')), int(sections[k+1].replace(']',''))]
872
+ ring = [int(s.replace('[', '')), int(sections[k + 1].replace(']', ''))]
851
873
  radii.append(ring)
852
874
  else:
853
875
  pass
854
876
  return radii
855
877
 
856
- def get_tracking_configs_list(return_path=False):
857
878
 
879
+ def get_tracking_configs_list(return_path=False):
858
880
  """
859
-
881
+
860
882
  Retrieve a list of available tracking configurations.
861
883
 
862
884
  Parameters
@@ -887,26 +909,30 @@ def get_tracking_configs_list(return_path=False):
887
909
 
888
910
  """
889
911
 
890
- modelpath = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "models", "tracking_configs", os.sep])
891
- available_models = glob(modelpath+'*.json')
892
- available_models = [m.replace('\\','/').split('/')[-1] for m in available_models]
893
- available_models = [m.replace('\\','/').split('.')[0] for m in available_models]
894
-
912
+ modelpath = os.sep.join(
913
+ [os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "models", "tracking_configs",
914
+ os.sep])
915
+ available_models = glob(modelpath + '*.json')
916
+ available_models = [m.replace('\\', '/').split('/')[-1] for m in available_models]
917
+ available_models = [m.replace('\\', '/').split('.')[0] for m in available_models]
895
918
 
896
919
  if not return_path:
897
920
  return available_models
898
921
  else:
899
922
  return available_models, modelpath
900
923
 
924
+
901
925
  def interpret_tracking_configuration(config):
902
926
 
903
927
  if isinstance(config, str):
904
928
  if os.path.exists(config):
905
929
  return config
906
930
  else:
907
- modelpath = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "models", "tracking_configs", os.sep])
908
- if os.path.exists(modelpath+config+'.json'):
909
- return modelpath+config+'.json'
931
+ modelpath = os.sep.join(
932
+ [os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "models",
933
+ "tracking_configs", os.sep])
934
+ if os.path.exists(modelpath + config + '.json'):
935
+ return modelpath + config + '.json'
910
936
  else:
911
937
  config = cell_config()
912
938
  elif config is None:
@@ -914,10 +940,11 @@ def interpret_tracking_configuration(config):
914
940
 
915
941
  return config
916
942
 
943
+
917
944
  def get_signal_models_list(return_path=False):
918
945
 
919
946
  """
920
-
947
+
921
948
  Retrieve a list of available signal detection models.
922
949
 
923
950
  Parameters
@@ -948,11 +975,13 @@ def get_signal_models_list(return_path=False):
948
975
 
949
976
  """
950
977
 
951
- modelpath = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "models", "signal_detection", os.sep])
952
- repository_models = get_zenodo_files(cat=os.sep.join(["models","signal_detection"]))
978
+ modelpath = os.sep.join(
979
+ [os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "models", "signal_detection",
980
+ os.sep])
981
+ repository_models = get_zenodo_files(cat=os.sep.join(["models", "signal_detection"]))
953
982
 
954
- available_models = glob(modelpath+f'*{os.sep}')
955
- available_models = [m.replace('\\','/').split('/')[-2] for m in available_models]
983
+ available_models = glob(modelpath + f'*{os.sep}')
984
+ available_models = [m.replace('\\', '/').split('/')[-2] for m in available_models]
956
985
  for rm in repository_models:
957
986
  if rm not in available_models:
958
987
  available_models.append(rm)
@@ -962,21 +991,72 @@ def get_signal_models_list(return_path=False):
962
991
  else:
963
992
  return available_models, modelpath
964
993
 
994
+ def get_pair_signal_models_list(return_path=False):
995
+ """
965
996
 
966
- def locate_signal_model(name, path=None):
967
-
968
- main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective"])
997
+ Retrieve a list of available signal detection models.
998
+
999
+ Parameters
1000
+ ----------
1001
+ return_path : bool, optional
1002
+ If True, also returns the path to the models. Default is False.
1003
+
1004
+ Returns
1005
+ -------
1006
+ list or tuple
1007
+ If return_path is False, returns a list of available signal detection models.
1008
+ If return_path is True, returns a tuple containing the list of models and the path to the models.
1009
+
1010
+ Notes
1011
+ -----
1012
+ This function retrieves the list of available signal detection models by searching for model directories
1013
+ in the predefined model path. The model path is derived from the parent directory of the current script
1014
+ location and the path to the model directory. By default, it returns only the names of the models.
1015
+ If return_path is set to True, it also returns the path to the models.
1016
+
1017
+ Examples
1018
+ --------
1019
+ >>> models = get_signal_models_list()
1020
+ # Retrieve a list of available signal detection models.
1021
+
1022
+ >>> models, path = get_signal_models_list(return_path=True)
1023
+ # Retrieve a list of available signal detection models and the path to the models.
1024
+
1025
+ """
1026
+
1027
+ modelpath = os.sep.join(
1028
+ [os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "models", "pair_signal_detection",
1029
+ os.sep])
1030
+ #repository_models = get_zenodo_files(cat=os.sep.join(["models", "pair_signal_detection"]))
1031
+
1032
+ available_models = glob(modelpath + f'*{os.sep}')
1033
+ available_models = [m.replace('\\', '/').split('/')[-2] for m in available_models]
1034
+ #for rm in repository_models:
1035
+ # if rm not in available_models:
1036
+ # available_models.append(rm)
1037
+
1038
+ if not return_path:
1039
+ return available_models
1040
+ else:
1041
+ return available_models, modelpath
1042
+
1043
+
1044
+ def locate_signal_model(name, path=None, pairs=False):
1045
+
1046
+ main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective"])
969
1047
  modelpath = os.sep.join([main_dir, "models", "signal_detection", os.sep])
1048
+ if pairs:
1049
+ modelpath = os.sep.join([main_dir, "models", "pair_signal_detection", os.sep])
970
1050
  print(f'Looking for {name} in {modelpath}')
971
- models = glob(modelpath+f'*{os.sep}')
1051
+ models = glob(modelpath + f'*{os.sep}')
972
1052
  if path is not None:
973
1053
  if not path.endswith(os.sep):
974
1054
  path += os.sep
975
- models += glob(path+f'*{os.sep}')
1055
+ models += glob(path + f'*{os.sep}')
976
1056
 
977
- match=None
1057
+ match = None
978
1058
  for m in models:
979
- if name==m.replace('\\',os.sep).split(os.sep)[-2]:
1059
+ if name == m.replace('\\', os.sep).split(os.sep)[-2]:
980
1060
  match = m
981
1061
  return match
982
1062
  # else no match, try zenodo
@@ -985,9 +1065,18 @@ def locate_signal_model(name, path=None):
985
1065
  index = files.index(name)
986
1066
  cat = categories[index]
987
1067
  download_zenodo_file(name, os.sep.join([main_dir, cat]))
988
- match = os.sep.join([main_dir, cat, name])+os.sep
1068
+ match = os.sep.join([main_dir, cat, name]) + os.sep
989
1069
  return match
990
1070
 
1071
+ def locate_pair_signal_model(name, path=None):
1072
+ main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective"])
1073
+ modelpath = os.sep.join([main_dir, "models", "pair_signal_detection", os.sep])
1074
+ print(f'Looking for {name} in {modelpath}')
1075
+ models = glob(modelpath + f'*{os.sep}')
1076
+ if path is not None:
1077
+ if not path.endswith(os.sep):
1078
+ path += os.sep
1079
+ models += glob(path + f'*{os.sep}')
991
1080
 
992
1081
  def relabel_segmentation(labels, data, properties, column_labels={'track': "track", 'frame': 'frame', 'y': 'y', 'x': 'x', 'label': 'class_id'}, threads=1):
993
1082
 
@@ -1029,7 +1118,11 @@ def relabel_segmentation(labels, data, properties, column_labels={'track': "trac
1029
1118
 
1030
1119
 
1031
1120
  n_threads = threads
1032
- df = pd.DataFrame(data,columns=[column_labels['track'],column_labels['frame'],column_labels['y'],column_labels['x']])
1121
+ if data.shape[1]==4:
1122
+ df = pd.DataFrame(data,columns=[column_labels['track'],column_labels['frame'],column_labels['y'],column_labels['x']])
1123
+ else:
1124
+ df = pd.DataFrame(data,columns=[column_labels['track'],column_labels['frame'],'z', column_labels['y'],column_labels['x']])
1125
+ df = df.drop(columns=['z'])
1033
1126
  df = df.merge(pd.DataFrame(properties),left_index=True, right_index=True)
1034
1127
  df = df.sort_values(by=[column_labels['track'],column_labels['frame']])
1035
1128
 
@@ -1039,15 +1132,15 @@ def relabel_segmentation(labels, data, properties, column_labels={'track': "trac
1039
1132
 
1040
1133
  for t in tqdm(indices):
1041
1134
  f = int(t)
1042
- tracks_at_t = df.loc[df[column_labels['frame']]==f, column_labels['track']].to_numpy()
1043
- identities = df.loc[df[column_labels['frame']]==f, column_labels['label']].to_numpy()
1135
+ tracks_at_t = df.loc[df[column_labels['frame']] == f, column_labels['track']].to_numpy()
1136
+ identities = df.loc[df[column_labels['frame']] == f, column_labels['label']].to_numpy()
1044
1137
 
1045
- tracks_at_t = tracks_at_t[identities==identities]
1046
- identities = identities[identities==identities]
1138
+ tracks_at_t = tracks_at_t[identities == identities]
1139
+ identities = identities[identities == identities]
1047
1140
 
1048
1141
  for k in range(len(identities)):
1049
- loc_i,loc_j = np.where(labels[f]==identities[k])
1050
- new_labels[f,loc_i,loc_j] = int(tracks_at_t[k])
1142
+ loc_i, loc_j = np.where(labels[f] == identities[k])
1143
+ new_labels[f, loc_i, loc_j] = int(tracks_at_t[k])
1051
1144
 
1052
1145
  # Multithreading
1053
1146
  indices = list(df[column_labels['frame']].unique())
@@ -1147,11 +1240,13 @@ def control_tracking_btrack(position, prefix="Aligned", population="target", rel
1147
1240
 
1148
1241
  """
1149
1242
 
1150
- data,properties,graph,labels,stack = load_napari_data(position, prefix=prefix, population=population)
1151
- view_on_napari_btrack(data,properties,graph,labels=labels, stack=stack, relabel=relabel, flush_memory=flush_memory, threads=threads)
1243
+ data, properties, graph, labels, stack = load_napari_data(position, prefix=prefix, population=population)
1244
+ view_on_napari_btrack(data, properties, graph, labels=labels, stack=stack, relabel=relabel,
1245
+ flush_memory=flush_memory, threads=threads)
1152
1246
 
1153
- def view_on_napari_btrack(data,properties,graph,stack=None,labels=None,relabel=True, flush_memory=True, position=None, threads=1):
1154
-
1247
+
1248
+ def view_on_napari_btrack(data, properties, graph, stack=None, labels=None, relabel=True, flush_memory=True,
1249
+ position=None, threads=1):
1155
1250
  """
1156
1251
 
1157
1252
  Visualize btrack data, including stack, labels, points, and tracks, using the napari viewer.
@@ -1184,20 +1279,28 @@ def view_on_napari_btrack(data,properties,graph,stack=None,labels=None,relabel=T
1184
1279
 
1185
1280
  """
1186
1281
 
1187
- if (labels is not None)*relabel:
1282
+ if (labels is not None) * relabel:
1188
1283
  print('Relabeling the cell masks with the track ID.')
1189
1284
  labels = relabel_segmentation(labels, data, properties, threads=threads)
1190
1285
 
1191
- vertices = data[:, 1:]
1286
+ if data.shape[1]==4:
1287
+ vertices = data[:, 1:]
1288
+ else:
1289
+ vertices = data[:, 2:]
1192
1290
  viewer = napari.Viewer()
1193
1291
  if stack is not None:
1194
- viewer.add_image(stack,channel_axis=-1,colormap=["gray"]*stack.shape[-1])
1292
+ print(f'{stack.shape=}')
1293
+ viewer.add_image(stack, channel_axis=-1, colormap=["gray"] * stack.shape[-1])
1195
1294
  if labels is not None:
1196
- viewer.add_labels(labels, name='segmentation',opacity=0.4)
1295
+ viewer.add_labels(labels, name='segmentation', opacity=0.4)
1197
1296
  viewer.add_points(vertices, size=4, name='points', opacity=0.3)
1198
- viewer.add_tracks(data, properties=properties, graph=graph, name='tracks')
1297
+ if data.shape[1]==4:
1298
+ viewer.add_tracks(data, properties=properties, graph=graph, name='tracks')
1299
+ else:
1300
+ print(data)
1301
+ viewer.add_tracks(data[:,[0,1,3,4]], properties=properties, graph=graph, name='tracks')
1199
1302
  viewer.show(block=True)
1200
-
1303
+
1201
1304
  if flush_memory:
1202
1305
  # temporary fix for slight napari memory leak
1203
1306
  for i in range(10000):
@@ -1211,8 +1314,8 @@ def view_on_napari_btrack(data,properties,graph,stack=None,labels=None,relabel=T
1211
1314
  del labels
1212
1315
  gc.collect()
1213
1316
 
1214
- def load_napari_data(position, prefix="Aligned", population="target", return_stack=True):
1215
1317
 
1318
+ def load_napari_data(position, prefix="Aligned", population="target", return_stack=True):
1216
1319
  """
1217
1320
  Load the necessary data for visualization in napari.
1218
1321
 
@@ -1234,7 +1337,7 @@ def load_napari_data(position, prefix="Aligned", population="target", return_sta
1234
1337
  --------
1235
1338
  >>> data, properties, graph, labels, stack = load_napari_data("path/to/position")
1236
1339
  # Load the necessary data for visualization of target trajectories.
1237
-
1340
+
1238
1341
  """
1239
1342
  position = position.replace('\\','/')
1240
1343
  if population.lower()=="target" or population.lower()=="targets":
@@ -1256,14 +1359,16 @@ def load_napari_data(position, prefix="Aligned", population="target", return_sta
1256
1359
  properties = None
1257
1360
  graph = None
1258
1361
  if return_stack:
1259
- stack,labels = locate_stack_and_labels(position, prefix=prefix, population=population)
1362
+ stack, labels = locate_stack_and_labels(position, prefix=prefix, population=population)
1260
1363
  else:
1261
- labels=locate_labels(position,population=population)
1364
+ labels = locate_labels(position, population=population)
1262
1365
  stack = None
1263
- return data,properties,graph,labels,stack
1366
+ return data, properties, graph, labels, stack
1367
+
1264
1368
 
1265
1369
  from skimage.measure import label
1266
1370
 
1371
+
1267
1372
  def auto_correct_masks(masks):
1268
1373
 
1269
1374
  """
@@ -1295,25 +1400,25 @@ def auto_correct_masks(masks):
1295
1400
 
1296
1401
  """
1297
1402
 
1298
- props = pd.DataFrame(regionprops_table(masks,properties=('label','area','area_bbox')))
1403
+ props = pd.DataFrame(regionprops_table(masks, properties=('label', 'area', 'area_bbox')))
1299
1404
  max_lbl = props['label'].max()
1300
1405
  corrected_lbl = masks.copy().astype(int)
1301
1406
 
1302
1407
  for cell in props['label'].unique():
1303
1408
 
1304
- bbox_area = props.loc[props['label']==cell, 'area_bbox'].values
1305
- area = props.loc[props['label']==cell, 'area'].values
1409
+ bbox_area = props.loc[props['label'] == cell, 'area_bbox'].values
1410
+ area = props.loc[props['label'] == cell, 'area'].values
1306
1411
 
1307
- if bbox_area > 1.75*area: #condition for anomaly
1412
+ if bbox_area > 1.75 * area: # condition for anomaly
1308
1413
 
1309
- lbl = masks==cell
1414
+ lbl = masks == cell
1310
1415
  lbl = lbl.astype(int)
1311
1416
 
1312
- relabelled = label(lbl,connectivity=2)
1417
+ relabelled = label(lbl, connectivity=2)
1313
1418
  relabelled += max_lbl
1314
- relabelled[np.where(lbl==0)] = 0
1419
+ relabelled[np.where(lbl == 0)] = 0
1315
1420
 
1316
- corrected_lbl[np.where(relabelled != 0)] = relabelled[np.where(relabelled!=0)]
1421
+ corrected_lbl[np.where(relabelled != 0)] = relabelled[np.where(relabelled != 0)]
1317
1422
 
1318
1423
  max_lbl = np.amax(corrected_lbl)
1319
1424
 
@@ -1324,7 +1429,7 @@ def auto_correct_masks(masks):
1324
1429
  def control_segmentation_napari(position, prefix='Aligned', population="target", flush_memory=False):
1325
1430
 
1326
1431
  """
1327
-
1432
+
1328
1433
  Control the visualization of segmentation labels using the napari viewer.
1329
1434
 
1330
1435
  Parameters
@@ -1350,26 +1455,34 @@ def control_segmentation_napari(position, prefix='Aligned', population="target",
1350
1455
 
1351
1456
  def export_labels():
1352
1457
  labels_layer = viewer.layers['segmentation'].data
1353
- for t,im in enumerate(tqdm(labels_layer)):
1354
-
1458
+ for t, im in enumerate(tqdm(labels_layer)):
1459
+
1355
1460
  try:
1356
1461
  im = auto_correct_masks(im)
1357
1462
  except Exception as e:
1358
1463
  print(e)
1359
1464
 
1360
- save_tiff_imagej_compatible(output_folder+f"{str(t).zfill(4)}.tif", im.astype(np.int16), axes='YX')
1465
+ save_tiff_imagej_compatible(output_folder + f"{str(t).zfill(4)}.tif", im.astype(np.int16), axes='YX')
1361
1466
  print("The labels have been successfully rewritten.")
1362
1467
 
1363
1468
  def export_annotation():
1364
-
1469
+
1365
1470
  # Locate experiment config
1366
1471
  parent1 = Path(position).parent
1367
1472
  expfolder = parent1.parent
1368
- config = PurePath(expfolder,Path("config.ini"))
1473
+ config = PurePath(expfolder, Path("config.ini"))
1369
1474
  expfolder = str(expfolder)
1370
1475
  exp_name = os.path.split(expfolder)[-1]
1371
1476
  print(exp_name)
1372
1477
 
1478
+ wells = get_experiment_wells(expfolder)
1479
+ well_idx = list(wells).index(str(parent1)+os.sep)
1480
+ ab = get_experiment_antibodies(expfolder)[well_idx]
1481
+ conc = get_experiment_concentrations(expfolder)[well_idx]
1482
+ ct = get_experiment_cell_types(expfolder)[well_idx]
1483
+ pa = get_experiment_pharmaceutical_agents(expfolder)[well_idx]
1484
+
1485
+
1373
1486
  spatial_calibration = float(ConfigSectionMap(config,"MovieSettings")["pxtoum"])
1374
1487
  channel_names, channel_indices = extract_experiment_channels(config)
1375
1488
 
@@ -1379,7 +1492,7 @@ def control_segmentation_napari(position, prefix='Aligned', population="target",
1379
1492
 
1380
1493
  print('exporting!')
1381
1494
  t = viewer.dims.current_step[0]
1382
- labels_layer = viewer.layers['segmentation'].data[t] # at current time
1495
+ labels_layer = viewer.layers['segmentation'].data[t] # at current time
1383
1496
 
1384
1497
  try:
1385
1498
  labels_layer = auto_correct_masks(labels_layer)
@@ -1387,61 +1500,61 @@ def control_segmentation_napari(position, prefix='Aligned', population="target",
1387
1500
  print(e)
1388
1501
 
1389
1502
  fov_export = True
1390
-
1503
+
1391
1504
  if "Shapes" in viewer.layers:
1392
1505
  squares = viewer.layers['Shapes'].data
1393
- test_in_frame = np.array([squares[i][0,0]==t and len(squares[i])==4 for i in range(len(squares))])
1506
+ test_in_frame = np.array([squares[i][0, 0] == t and len(squares[i]) == 4 for i in range(len(squares))])
1394
1507
  squares = np.array(squares)
1395
1508
  squares = squares[test_in_frame]
1396
1509
  nbr_squares = len(squares)
1397
1510
  print(f"Found {nbr_squares} ROIS")
1398
- if nbr_squares>0:
1511
+ if nbr_squares > 0:
1399
1512
  # deactivate field of view mode
1400
1513
  fov_export = False
1401
1514
 
1402
- for k,sq in enumerate(squares):
1515
+ for k, sq in enumerate(squares):
1403
1516
  print(f"ROI: {sq}")
1404
- xmin = int(sq[0,1])
1405
- xmax = int(sq[2,1])
1406
- if xmax<xmin:
1407
- xmax,xmin = xmin,xmax
1408
- ymin = int(sq[0,2])
1409
- ymax = int(sq[1,2])
1410
- if ymax<ymin:
1411
- ymax,ymin = ymin,ymax
1517
+ xmin = int(sq[0, 1])
1518
+ xmax = int(sq[2, 1])
1519
+ if xmax < xmin:
1520
+ xmax, xmin = xmin, xmax
1521
+ ymin = int(sq[0, 2])
1522
+ ymax = int(sq[1, 2])
1523
+ if ymax < ymin:
1524
+ ymax, ymin = ymin, ymax
1412
1525
  print(f"{xmin=};{xmax=};{ymin=};{ymax=}")
1413
- frame = viewer.layers['Image'].data[t][xmin:xmax,ymin:ymax]
1526
+ frame = viewer.layers['Image'].data[t][xmin:xmax, ymin:ymax]
1414
1527
  if frame.shape[1] < 256 or frame.shape[0] < 256:
1415
1528
  print("crop too small!")
1416
1529
  continue
1417
1530
  multichannel = [frame]
1418
- for i in range(len(channel_indices)-1):
1531
+ for i in range(len(channel_indices) - 1):
1419
1532
  try:
1420
- frame = viewer.layers[f'Image [{i+1}]'].data[t][xmin:xmax,ymin:ymax]
1533
+ frame = viewer.layers[f'Image [{i + 1}]'].data[t][xmin:xmax, ymin:ymax]
1421
1534
  multichannel.append(frame)
1422
1535
  except:
1423
1536
  pass
1424
1537
  multichannel = np.array(multichannel)
1425
1538
  save_tiff_imagej_compatible(annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}_roi_{xmin}_{xmax}_{ymin}_{ymax}_labelled.tif", labels_layer[xmin:xmax,ymin:ymax].astype(np.int16), axes='YX')
1426
1539
  save_tiff_imagej_compatible(annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}_roi_{xmin}_{xmax}_{ymin}_{ymax}.tif", multichannel, axes='CYX')
1427
- info = {"spatial_calibration": spatial_calibration, "channels": list(channel_names)}
1540
+ info = {"spatial_calibration": spatial_calibration, "channels": list(channel_names), 'cell_type': ct, 'antibody': ab, 'concentration': conc, 'pharmaceutical_agent': pa}
1428
1541
  info_name = annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}_roi_{xmin}_{xmax}_{ymin}_{ymax}.json"
1429
1542
  with open(info_name, 'w') as f:
1430
1543
  json.dump(info, f, indent=4)
1431
-
1544
+
1432
1545
  if fov_export:
1433
1546
  frame = viewer.layers['Image'].data[t]
1434
1547
  multichannel = [frame]
1435
- for i in range(len(channel_indices)-1):
1548
+ for i in range(len(channel_indices) - 1):
1436
1549
  try:
1437
- frame = viewer.layers[f'Image [{i+1}]'].data[t]
1550
+ frame = viewer.layers[f'Image [{i + 1}]'].data[t]
1438
1551
  multichannel.append(frame)
1439
1552
  except:
1440
1553
  pass
1441
1554
  multichannel = np.array(multichannel)
1442
1555
  save_tiff_imagej_compatible(annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}_labelled.tif", labels_layer, axes='YX')
1443
1556
  save_tiff_imagej_compatible(annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}.tif", multichannel, axes='CYX')
1444
- info = {"spatial_calibration": spatial_calibration, "channels": list(channel_names)}
1557
+ info = {"spatial_calibration": spatial_calibration, "channels": list(channel_names), 'cell_type': ct, 'antibody': ab, 'concentration': conc, 'pharmaceutical_agent': pa}
1445
1558
  info_name = annotation_folder + f"{exp_name}_{position.split(os.sep)[-2]}_{str(t).zfill(4)}.json"
1446
1559
  with open(info_name, 'w') as f:
1447
1560
  json.dump(info, f, indent=4)
@@ -1455,15 +1568,15 @@ def control_segmentation_napari(position, prefix='Aligned', population="target",
1455
1568
  def export_widget():
1456
1569
  return export_annotation()
1457
1570
 
1458
- stack,labels = locate_stack_and_labels(position, prefix=prefix, population=population)
1571
+ stack, labels = locate_stack_and_labels(position, prefix=prefix, population=population)
1459
1572
 
1460
1573
  if not population.endswith('s'):
1461
- population+='s'
1462
- output_folder = position+f'labels_{population}{os.sep}'
1574
+ population += 's'
1575
+ output_folder = position + f'labels_{population}{os.sep}'
1463
1576
 
1464
1577
  viewer = napari.Viewer()
1465
- viewer.add_image(stack,channel_axis=-1,colormap=["gray"]*stack.shape[-1])
1466
- viewer.add_labels(labels.astype(int), name='segmentation',opacity=0.4)
1578
+ viewer.add_image(stack, channel_axis=-1, colormap=["gray"] * stack.shape[-1])
1579
+ viewer.add_labels(labels.astype(int), name='segmentation', opacity=0.4)
1467
1580
  viewer.window.add_dock_widget(save_widget, area='right')
1468
1581
  viewer.window.add_dock_widget(export_widget, area='right')
1469
1582
  viewer.show(block=True)
@@ -1481,6 +1594,59 @@ def control_segmentation_napari(position, prefix='Aligned', population="target",
1481
1594
  del labels
1482
1595
  gc.collect()
1483
1596
 
1597
+ def correct_annotation(filename):
1598
+
1599
+ """
1600
+ New function to reannotate an annotation image in post, using napari and save update inplace.
1601
+ """
1602
+
1603
+ def export_labels():
1604
+ labels_layer = viewer.layers['segmentation'].data
1605
+ for t,im in enumerate(tqdm(labels_layer)):
1606
+
1607
+ try:
1608
+ im = auto_correct_masks(im)
1609
+ except Exception as e:
1610
+ print(e)
1611
+
1612
+ save_tiff_imagej_compatible(existing_lbl, im.astype(np.int16), axes='YX')
1613
+ print("The labels have been successfully rewritten.")
1614
+
1615
+ @magicgui(call_button='Save the modified labels')
1616
+ def save_widget():
1617
+ return export_labels()
1618
+
1619
+ img = imread(filename.replace('\\','/'),is_mmstack=False)
1620
+ if img.ndim==3:
1621
+ img = np.moveaxis(img, 0, -1)
1622
+ elif img.ndim==2:
1623
+ img = img[:,:,np.newaxis]
1624
+
1625
+ existing_lbl = filename.replace('.tif','_labelled.tif')
1626
+ if os.path.exists(existing_lbl):
1627
+ labels = imread(existing_lbl)[np.newaxis,:,:].astype(int)
1628
+ else:
1629
+ labels = np.zeros_like(img[:,:,0]).astype(int)[np.newaxis,:,:]
1630
+
1631
+ stack = img[np.newaxis,:,:,:]
1632
+
1633
+ viewer = napari.Viewer()
1634
+ viewer.add_image(stack,channel_axis=-1,colormap=["gray"]*stack.shape[-1])
1635
+ viewer.add_labels(labels, name='segmentation',opacity=0.4)
1636
+ viewer.window.add_dock_widget(save_widget, area='right')
1637
+ viewer.show(block=True)
1638
+
1639
+ # temporary fix for slight napari memory leak
1640
+ for i in range(100):
1641
+ try:
1642
+ viewer.layers.pop()
1643
+ except:
1644
+ pass
1645
+ del viewer
1646
+ del stack
1647
+ del labels
1648
+ gc.collect()
1649
+
1484
1650
 
1485
1651
  def _view_on_napari(tracks=None, stack=None, labels=None):
1486
1652
 
@@ -1516,23 +1682,24 @@ def _view_on_napari(tracks=None, stack=None, labels=None):
1516
1682
  >>> labels = np.random.randint(0, 2, (100, 100))
1517
1683
  >>> view_on_napari(tracks, stack=stack, labels=labels)
1518
1684
  # Visualize tracks, stack, and labels using Napari.
1519
-
1685
+
1520
1686
  """
1521
1687
 
1522
1688
  viewer = napari.Viewer()
1523
1689
  if stack is not None:
1524
- viewer.add_image(stack,channel_axis=-1,colormap=["gray"]*stack.shape[-1])
1690
+ viewer.add_image(stack, channel_axis=-1, colormap=["gray"] * stack.shape[-1])
1525
1691
  if labels is not None:
1526
- viewer.add_labels(labels, name='segmentation',opacity=0.4)
1692
+ viewer.add_labels(labels, name='segmentation', opacity=0.4)
1527
1693
  if tracks is not None:
1528
1694
  viewer.add_tracks(tracks, name='tracks')
1529
1695
  viewer.show(block=True)
1530
1696
 
1531
- def control_tracking_table(position, calibration=1, prefix="Aligned", population="target",
1532
- column_labels={'track': "TRACK_ID", 'frame': 'FRAME', 'y': 'POSITION_Y', 'x': 'POSITION_X', 'label': 'class_id'}):
1533
1697
 
1698
+ def control_tracking_table(position, calibration=1, prefix="Aligned", population="target",
1699
+ column_labels={'track': "TRACK_ID", 'frame': 'FRAME', 'y': 'POSITION_Y', 'x': 'POSITION_X',
1700
+ 'label': 'class_id'}):
1534
1701
  """
1535
-
1702
+
1536
1703
  Control the tracking table and visualize tracks using Napari.
1537
1704
 
1538
1705
  Parameters
@@ -1567,27 +1734,33 @@ def control_tracking_table(position, calibration=1, prefix="Aligned", population
1567
1734
 
1568
1735
  """
1569
1736
 
1570
- position = position.replace('\\','/')
1571
- tracks,labels,stack = load_tracking_data(position, prefix=prefix, population=population)
1572
- tracks = tracks.loc[:, [column_labels['track'], column_labels['frame'], column_labels['y'], column_labels['x']]].to_numpy()
1573
- tracks[:,-2:] /= calibration
1574
- _view_on_napari(tracks,labels=labels, stack=stack)
1737
+ position = position.replace('\\', '/')
1738
+ tracks, labels, stack = load_tracking_data(position, prefix=prefix, population=population)
1739
+ tracks = tracks.loc[:,
1740
+ [column_labels['track'], column_labels['frame'], column_labels['y'], column_labels['x']]].to_numpy()
1741
+ tracks[:, -2:] /= calibration
1742
+ _view_on_napari(tracks, labels=labels, stack=stack)
1575
1743
 
1576
1744
 
1577
1745
  def get_segmentation_models_list(mode='targets', return_path=False):
1578
-
1579
- if mode=='targets':
1580
- modelpath = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "models", "segmentation_targets", os.sep])
1581
- repository_models = get_zenodo_files(cat=os.sep.join(["models","segmentation_targets"]))
1582
- elif mode=='effectors':
1583
- modelpath = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "models", "segmentation_effectors", os.sep])
1584
- repository_models = get_zenodo_files(cat=os.sep.join(["models","segmentation_effectors"]))
1585
- elif mode=='generic':
1586
- modelpath = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "models", "segmentation_generic", os.sep])
1587
- repository_models = get_zenodo_files(cat=os.sep.join(["models","segmentation_generic"]))
1588
-
1589
- available_models = natsorted(glob(modelpath+'*/'))
1590
- available_models = [m.replace('\\','/').split('/')[-2] for m in available_models]
1746
+ if mode == 'targets':
1747
+ modelpath = os.sep.join(
1748
+ [os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "models",
1749
+ "segmentation_targets", os.sep])
1750
+ repository_models = get_zenodo_files(cat=os.sep.join(["models", "segmentation_targets"]))
1751
+ elif mode == 'effectors':
1752
+ modelpath = os.sep.join(
1753
+ [os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "models",
1754
+ "segmentation_effectors", os.sep])
1755
+ repository_models = get_zenodo_files(cat=os.sep.join(["models", "segmentation_effectors"]))
1756
+ elif mode == 'generic':
1757
+ modelpath = os.sep.join(
1758
+ [os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "models",
1759
+ "segmentation_generic", os.sep])
1760
+ repository_models = get_zenodo_files(cat=os.sep.join(["models", "segmentation_generic"]))
1761
+
1762
+ available_models = natsorted(glob(modelpath + '*/'))
1763
+ available_models = [m.replace('\\', '/').split('/')[-2] for m in available_models]
1591
1764
  for rm in repository_models:
1592
1765
  if rm not in available_models:
1593
1766
  available_models.append(rm)
@@ -1597,16 +1770,17 @@ def get_segmentation_models_list(mode='targets', return_path=False):
1597
1770
  else:
1598
1771
  return available_models, modelpath
1599
1772
 
1773
+
1600
1774
  def locate_segmentation_model(name):
1601
1775
 
1602
1776
  """
1603
- Locates a specified segmentation model within the local 'celldetective' directory or
1777
+ Locates a specified segmentation model within the local 'celldetective' directory or
1604
1778
  downloads it from Zenodo if not found locally.
1605
1779
 
1606
- This function attempts to find a segmentation model by name within a predefined directory
1607
- structure starting from the 'celldetective/models/segmentation*' path. If the model is not
1608
- found locally, it then tries to locate and download the model from Zenodo, placing it into
1609
- the appropriate category directory within 'celldetective'. The function prints the search
1780
+ This function attempts to find a segmentation model by name within a predefined directory
1781
+ structure starting from the 'celldetective/models/segmentation*' path. If the model is not
1782
+ found locally, it then tries to locate and download the model from Zenodo, placing it into
1783
+ the appropriate category directory within 'celldetective'. The function prints the search
1610
1784
  directory path and returns the path to the found or downloaded model.
1611
1785
 
1612
1786
  Parameters
@@ -1617,7 +1791,7 @@ def locate_segmentation_model(name):
1617
1791
  Returns
1618
1792
  -------
1619
1793
  str or None
1620
- The full path to the located or downloaded segmentation model directory, or None if the
1794
+ The full path to the located or downloaded segmentation model directory, or None if the
1621
1795
  model could not be found or downloaded.
1622
1796
 
1623
1797
  Raises
@@ -1628,13 +1802,13 @@ def locate_segmentation_model(name):
1628
1802
  """
1629
1803
 
1630
1804
  main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective"])
1631
- modelpath = os.sep.join([main_dir, "models", "segmentation*", os.sep])
1805
+ modelpath = os.sep.join([main_dir, "models", "segmentation*"]) + os.sep
1632
1806
  print(f'Looking for {name} in {modelpath}')
1633
- models = glob(modelpath+f'*{os.sep}')
1807
+ models = glob(modelpath + f'*{os.sep}')
1634
1808
 
1635
- match=None
1809
+ match = None
1636
1810
  for m in models:
1637
- if name==m.replace('\\',os.sep).split(os.sep)[-2]:
1811
+ if name == m.replace('\\', os.sep).split(os.sep)[-2]:
1638
1812
  match = m
1639
1813
  return match
1640
1814
  # else no match, try zenodo
@@ -1643,42 +1817,42 @@ def locate_segmentation_model(name):
1643
1817
  index = files.index(name)
1644
1818
  cat = categories[index]
1645
1819
  download_zenodo_file(name, os.sep.join([main_dir, cat]))
1646
- match = os.sep.join([main_dir, cat, name])+os.sep
1820
+ match = os.sep.join([main_dir, cat, name]) + os.sep
1647
1821
  return match
1648
1822
 
1649
1823
 
1650
1824
  def get_segmentation_datasets_list(return_path=False):
1651
-
1652
1825
  """
1653
- Retrieves a list of available segmentation datasets from both the local 'celldetective/datasets/segmentation_annotations'
1826
+ Retrieves a list of available segmentation datasets from both the local 'celldetective/datasets/segmentation_annotations'
1654
1827
  directory and a Zenodo repository, optionally returning the path to the local datasets directory.
1655
1828
 
1656
- This function compiles a list of available segmentation datasets by first identifying datasets stored locally
1657
- within a specified path related to the script's directory. It then extends this list with datasets available
1658
- in a Zenodo repository, ensuring no duplicates are added. The function can return just the list of dataset
1829
+ This function compiles a list of available segmentation datasets by first identifying datasets stored locally
1830
+ within a specified path related to the script's directory. It then extends this list with datasets available
1831
+ in a Zenodo repository, ensuring no duplicates are added. The function can return just the list of dataset
1659
1832
  names or, if specified, also return the path to the local datasets directory.
1660
1833
 
1661
1834
  Parameters
1662
1835
  ----------
1663
1836
  return_path : bool, optional
1664
- If True, the function returns a tuple containing the list of available dataset names and the path to the
1837
+ If True, the function returns a tuple containing the list of available dataset names and the path to the
1665
1838
  local datasets directory. If False, only the list of dataset names is returned (default is False).
1666
1839
 
1667
1840
  Returns
1668
1841
  -------
1669
1842
  list or (list, str)
1670
- If return_path is False, returns a list of strings, each string being the name of an available dataset.
1671
- If return_path is True, returns a tuple where the first element is this list and the second element is a
1843
+ If return_path is False, returns a list of strings, each string being the name of an available dataset.
1844
+ If return_path is True, returns a tuple where the first element is this list and the second element is a
1672
1845
  string representing the path to the local datasets directory.
1673
-
1846
+
1674
1847
  """
1675
1848
 
1676
-
1677
- datasets_path = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "datasets", "segmentation_annotations", os.sep])
1678
- repository_datasets = get_zenodo_files(cat=os.sep.join(["datasets","segmentation_annotations"]))
1849
+ datasets_path = os.sep.join(
1850
+ [os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "datasets",
1851
+ "segmentation_annotations", os.sep])
1852
+ repository_datasets = get_zenodo_files(cat=os.sep.join(["datasets", "segmentation_annotations"]))
1679
1853
 
1680
- available_datasets = natsorted(glob(datasets_path+'*/'))
1681
- available_datasets = [m.replace('\\','/').split('/')[-2] for m in available_datasets]
1854
+ available_datasets = natsorted(glob(datasets_path + '*/'))
1855
+ available_datasets = [m.replace('\\', '/').split('/')[-2] for m in available_datasets]
1682
1856
  for rm in repository_datasets:
1683
1857
  if rm not in available_datasets:
1684
1858
  available_datasets.append(rm)
@@ -1693,12 +1867,12 @@ def get_segmentation_datasets_list(return_path=False):
1693
1867
  def locate_segmentation_dataset(name):
1694
1868
 
1695
1869
  """
1696
- Locates a specified segmentation dataset within the local 'celldetective/datasets/segmentation_annotations' directory
1870
+ Locates a specified segmentation dataset within the local 'celldetective/datasets/segmentation_annotations' directory
1697
1871
  or downloads it from Zenodo if not found locally.
1698
1872
 
1699
- This function attempts to find a segmentation dataset by name within a predefined directory structure. If the dataset
1700
- is not found locally, it then tries to locate and download the dataset from Zenodo, placing it into the appropriate
1701
- category directory within 'celldetective'. The function prints the search directory path and returns the path to the
1873
+ This function attempts to find a segmentation dataset by name within a predefined directory structure. If the dataset
1874
+ is not found locally, it then tries to locate and download the dataset from Zenodo, placing it into the appropriate
1875
+ category directory within 'celldetective'. The function prints the search directory path and returns the path to the
1702
1876
  found or downloaded dataset.
1703
1877
 
1704
1878
  Parameters
@@ -1709,24 +1883,24 @@ def locate_segmentation_dataset(name):
1709
1883
  Returns
1710
1884
  -------
1711
1885
  str or None
1712
- The full path to the located or downloaded segmentation dataset directory, or None if the dataset could not be
1886
+ The full path to the located or downloaded segmentation dataset directory, or None if the dataset could not be
1713
1887
  found or downloaded.
1714
1888
 
1715
1889
  Raises
1716
1890
  ------
1717
1891
  FileNotFoundError
1718
1892
  If the dataset cannot be found locally and also cannot be found or downloaded from Zenodo.
1719
-
1893
+
1720
1894
  """
1721
1895
 
1722
- main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective"])
1896
+ main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective"])
1723
1897
  modelpath = os.sep.join([main_dir, "datasets", "segmentation_annotations", os.sep])
1724
1898
  print(f'Looking for {name} in {modelpath}')
1725
- models = glob(modelpath+f'*{os.sep}')
1899
+ models = glob(modelpath + f'*{os.sep}')
1726
1900
 
1727
- match=None
1901
+ match = None
1728
1902
  for m in models:
1729
- if name==m.replace('\\',os.sep).split(os.sep)[-2]:
1903
+ if name == m.replace('\\', os.sep).split(os.sep)[-2]:
1730
1904
  match = m
1731
1905
  return match
1732
1906
  # else no match, try zenodo
@@ -1735,41 +1909,43 @@ def locate_segmentation_dataset(name):
1735
1909
  index = files.index(name)
1736
1910
  cat = categories[index]
1737
1911
  download_zenodo_file(name, os.sep.join([main_dir, cat]))
1738
- match = os.sep.join([main_dir, cat, name])+os.sep
1912
+ match = os.sep.join([main_dir, cat, name]) + os.sep
1739
1913
  return match
1740
1914
 
1741
1915
 
1742
1916
  def get_signal_datasets_list(return_path=False):
1743
1917
 
1744
1918
  """
1745
- Retrieves a list of available signal datasets from both the local 'celldetective/datasets/signal_annotations' directory
1919
+ Retrieves a list of available signal datasets from both the local 'celldetective/datasets/signal_annotations' directory
1746
1920
  and a Zenodo repository, optionally returning the path to the local datasets directory.
1747
1921
 
1748
- This function compiles a list of available signal datasets by first identifying datasets stored locally within a specified
1749
- path related to the script's directory. It then extends this list with datasets available in a Zenodo repository, ensuring
1750
- no duplicates are added. The function can return just the list of dataset names or, if specified, also return the path to
1922
+ This function compiles a list of available signal datasets by first identifying datasets stored locally within a specified
1923
+ path related to the script's directory. It then extends this list with datasets available in a Zenodo repository, ensuring
1924
+ no duplicates are added. The function can return just the list of dataset names or, if specified, also return the path to
1751
1925
  the local datasets directory.
1752
1926
 
1753
1927
  Parameters
1754
1928
  ----------
1755
1929
  return_path : bool, optional
1756
- If True, the function returns a tuple containing the list of available dataset names and the path to the local datasets
1930
+ If True, the function returns a tuple containing the list of available dataset names and the path to the local datasets
1757
1931
  directory. If False, only the list of dataset names is returned (default is False).
1758
1932
 
1759
1933
  Returns
1760
1934
  -------
1761
1935
  list or (list, str)
1762
- If return_path is False, returns a list of strings, each string being the name of an available dataset. If return_path
1763
- is True, returns a tuple where the first element is this list and the second element is a string representing the path
1936
+ If return_path is False, returns a list of strings, each string being the name of an available dataset. If return_path
1937
+ is True, returns a tuple where the first element is this list and the second element is a string representing the path
1764
1938
  to the local datasets directory.
1765
1939
 
1766
- """
1940
+ """
1767
1941
 
1768
- datasets_path = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective", "datasets", "signal_annotations", os.sep])
1769
- repository_datasets = get_zenodo_files(cat=os.sep.join(["datasets","signal_annotations"]))
1942
+ datasets_path = os.sep.join(
1943
+ [os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective", "datasets",
1944
+ "signal_annotations", os.sep])
1945
+ repository_datasets = get_zenodo_files(cat=os.sep.join(["datasets", "signal_annotations"]))
1770
1946
 
1771
- available_datasets = natsorted(glob(datasets_path+'*/'))
1772
- available_datasets = [m.replace('\\','/').split('/')[-2] for m in available_datasets]
1947
+ available_datasets = natsorted(glob(datasets_path + '*/'))
1948
+ available_datasets = [m.replace('\\', '/').split('/')[-2] for m in available_datasets]
1773
1949
  for rm in repository_datasets:
1774
1950
  if rm not in available_datasets:
1775
1951
  available_datasets.append(rm)
@@ -1779,15 +1955,16 @@ def get_signal_datasets_list(return_path=False):
1779
1955
  else:
1780
1956
  return available_datasets, datasets_path
1781
1957
 
1958
+
1782
1959
  def locate_signal_dataset(name):
1783
1960
 
1784
1961
  """
1785
- Locates a specified signal dataset within the local 'celldetective/datasets/signal_annotations' directory or downloads
1962
+ Locates a specified signal dataset within the local 'celldetective/datasets/signal_annotations' directory or downloads
1786
1963
  it from Zenodo if not found locally.
1787
1964
 
1788
- This function attempts to find a signal dataset by name within a predefined directory structure. If the dataset is not
1789
- found locally, it then tries to locate and download the dataset from Zenodo, placing it into the appropriate category
1790
- directory within 'celldetective'. The function prints the search directory path and returns the path to the found or
1965
+ This function attempts to find a signal dataset by name within a predefined directory structure. If the dataset is not
1966
+ found locally, it then tries to locate and download the dataset from Zenodo, placing it into the appropriate category
1967
+ directory within 'celldetective'. The function prints the search directory path and returns the path to the found or
1791
1968
  downloaded dataset.
1792
1969
 
1793
1970
  Parameters
@@ -1798,7 +1975,7 @@ def locate_signal_dataset(name):
1798
1975
  Returns
1799
1976
  -------
1800
1977
  str or None
1801
- The full path to the located or downloaded signal dataset directory, or None if the dataset could not be found or
1978
+ The full path to the located or downloaded signal dataset directory, or None if the dataset could not be found or
1802
1979
  downloaded.
1803
1980
 
1804
1981
  Raises
@@ -1808,14 +1985,14 @@ def locate_signal_dataset(name):
1808
1985
 
1809
1986
  """
1810
1987
 
1811
- main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective"])
1988
+ main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective"])
1812
1989
  modelpath = os.sep.join([main_dir, "datasets", "signal_annotations", os.sep])
1813
1990
  print(f'Looking for {name} in {modelpath}')
1814
- models = glob(modelpath+f'*{os.sep}')
1991
+ models = glob(modelpath + f'*{os.sep}')
1815
1992
 
1816
- match=None
1993
+ match = None
1817
1994
  for m in models:
1818
- if name==m.replace('\\',os.sep).split(os.sep)[-2]:
1995
+ if name == m.replace('\\', os.sep).split(os.sep)[-2]:
1819
1996
  match = m
1820
1997
  return match
1821
1998
  # else no match, try zenodo
@@ -1824,13 +2001,13 @@ def locate_signal_dataset(name):
1824
2001
  index = files.index(name)
1825
2002
  cat = categories[index]
1826
2003
  download_zenodo_file(name, os.sep.join([main_dir, cat]))
1827
- match = os.sep.join([main_dir, cat, name])+os.sep
2004
+ match = os.sep.join([main_dir, cat, name]) + os.sep
1828
2005
  return match
1829
2006
 
1830
2007
  def normalize(frame, percentiles=(0.0,99.99), values=None, ignore_gray_value=0., clip=False, amplification=None, dtype=float):
1831
2008
 
1832
2009
  """
1833
-
2010
+
1834
2011
  Normalize the intensity values of a frame.
1835
2012
 
1836
2013
  Parameters
@@ -1880,15 +2057,16 @@ def normalize(frame, percentiles=(0.0,99.99), values=None, ignore_gray_value=0.,
1880
2057
  frame = frame.astype(float)
1881
2058
 
1882
2059
  if ignore_gray_value is not None:
1883
- subframe = frame[frame!=ignore_gray_value]
2060
+ subframe = frame[frame != ignore_gray_value]
1884
2061
  else:
1885
2062
  subframe = frame.copy()
1886
2063
 
1887
2064
  if values is not None:
1888
- mi = values[0]; ma = values[1]
2065
+ mi = values[0];
2066
+ ma = values[1]
1889
2067
  else:
1890
- mi = np.nanpercentile(subframe.flatten(),percentiles[0],keepdims=True)
1891
- ma = np.nanpercentile(subframe.flatten(),percentiles[1],keepdims=True)
2068
+ mi = np.nanpercentile(subframe.flatten(), percentiles[0], keepdims=True)
2069
+ ma = np.nanpercentile(subframe.flatten(), percentiles[1], keepdims=True)
1892
2070
 
1893
2071
  frame0 = frame.copy()
1894
2072
  frame = normalize_mi_ma(frame0, mi, ma, clip=False, eps=1e-20, dtype=np.float32)
@@ -1897,41 +2075,42 @@ def normalize(frame, percentiles=(0.0,99.99), values=None, ignore_gray_value=0.,
1897
2075
  if clip:
1898
2076
  if amplification is None:
1899
2077
  amplification = 1.
1900
- frame[frame>=amplification] = amplification
1901
- frame[frame<=0.] = 0.
2078
+ frame[frame >= amplification] = amplification
2079
+ frame[frame <= 0.] = 0.
1902
2080
  if ignore_gray_value is not None:
1903
- frame[np.where(frame0)==ignore_gray_value] = ignore_gray_value
2081
+ frame[np.where(frame0) == ignore_gray_value] = ignore_gray_value
1904
2082
 
1905
2083
  return frame.copy().astype(dtype)
1906
2084
 
2085
+
1907
2086
  def normalize_multichannel(multichannel_frame, percentiles=None,
1908
2087
  values=None, ignore_gray_value=0., clip=False,
1909
2088
  amplification=None, dtype=float):
1910
2089
 
1911
2090
  """
1912
- Normalizes a multichannel frame by adjusting the intensity values of each channel based on specified percentiles,
2091
+ Normalizes a multichannel frame by adjusting the intensity values of each channel based on specified percentiles,
1913
2092
  direct value ranges, or amplification factors, with options to ignore a specific gray value and to clip the output.
1914
2093
 
1915
2094
  Parameters
1916
2095
  ----------
1917
2096
  multichannel_frame : ndarray
1918
- The input multichannel image frame to be normalized, expected to be a 3-dimensional array where the last dimension
2097
+ The input multichannel image frame to be normalized, expected to be a 3-dimensional array where the last dimension
1919
2098
  represents the channels.
1920
2099
  percentiles : list of tuples or tuple, optional
1921
- Percentile ranges (low, high) for each channel used to scale the intensity values. If a single tuple is provided,
2100
+ Percentile ranges (low, high) for each channel used to scale the intensity values. If a single tuple is provided,
1922
2101
  it is applied to all channels. If None, the default percentile range of (0., 99.99) is used for each channel.
1923
2102
  values : list of tuples or tuple, optional
1924
- Direct value ranges (min, max) for each channel to scale the intensity values. If a single tuple is provided, it
2103
+ Direct value ranges (min, max) for each channel to scale the intensity values. If a single tuple is provided, it
1925
2104
  is applied to all channels. This parameter overrides `percentiles` if provided.
1926
2105
  ignore_gray_value : float, optional
1927
2106
  A specific gray value to ignore during normalization (default is 0.).
1928
2107
  clip : bool, optional
1929
- If True, clips the output values to the range [0, 1] or the specified `dtype` range if `dtype` is not float
2108
+ If True, clips the output values to the range [0, 1] or the specified `dtype` range if `dtype` is not float
1930
2109
  (default is False).
1931
2110
  amplification : float, optional
1932
2111
  A factor by which to amplify the intensity values after normalization. If None, no amplification is applied.
1933
2112
  dtype : data-type, optional
1934
- The desired data-type for the output normalized frame. The default is float, but other types can be specified
2113
+ The desired data-type for the output normalized frame. The default is float, but other types can be specified
1935
2114
  to change the range of the output values.
1936
2115
 
1937
2116
  Returns
@@ -1942,12 +2121,12 @@ def normalize_multichannel(multichannel_frame, percentiles=None,
1942
2121
  Raises
1943
2122
  ------
1944
2123
  AssertionError
1945
- If the input `multichannel_frame` does not have 3 dimensions, or if the length of `values` does not match the
2124
+ If the input `multichannel_frame` does not have 3 dimensions, or if the length of `values` does not match the
1946
2125
  number of channels in `multichannel_frame`.
1947
2126
 
1948
2127
  Notes
1949
2128
  -----
1950
- - This function provides flexibility in normalization by allowing the use of percentile ranges, direct value ranges,
2129
+ - This function provides flexibility in normalization by allowing the use of percentile ranges, direct value ranges,
1951
2130
  or amplification factors.
1952
2131
  - The function makes a copy of the input frame to avoid altering the original data.
1953
2132
  - When both `percentiles` and `values` are provided, `values` takes precedence for normalization.
@@ -1957,21 +2136,22 @@ def normalize_multichannel(multichannel_frame, percentiles=None,
1957
2136
  >>> multichannel_frame = np.random.rand(100, 100, 3) # Example multichannel frame
1958
2137
  >>> normalized_frame = normalize_multichannel(multichannel_frame, percentiles=((1, 99), (2, 98), (0, 100)))
1959
2138
  # Normalizes each channel of the frame using specified percentile ranges.
1960
-
2139
+
1961
2140
  """
1962
2141
 
1963
2142
 
1964
2143
 
1965
2144
  mf = multichannel_frame.copy().astype(float)
1966
- assert mf.ndim==3,f'Wrong shape for the multichannel frame: {mf.shape}.'
2145
+ assert mf.ndim == 3, f'Wrong shape for the multichannel frame: {mf.shape}.'
1967
2146
  if percentiles is None:
1968
- percentiles = [(0.,99.99)]*mf.shape[-1]
1969
- elif isinstance(percentiles,tuple):
1970
- percentiles = [percentiles]*mf.shape[-1]
2147
+ percentiles = [(0., 99.99)] * mf.shape[-1]
2148
+ elif isinstance(percentiles, tuple):
2149
+ percentiles = [percentiles] * mf.shape[-1]
1971
2150
  if values is not None:
1972
2151
  if isinstance(values, tuple):
1973
- values = [values]*mf.shape[-1]
1974
- assert len(values)==mf.shape[-1],'Mismatch between the normalization values provided and the number of channels.'
2152
+ values = [values] * mf.shape[-1]
2153
+ assert len(values) == mf.shape[
2154
+ -1], 'Mismatch between the normalization values provided and the number of channels.'
1975
2155
 
1976
2156
  mf_new = []
1977
2157
  for c in range(mf.shape[-1]):
@@ -2000,9 +2180,9 @@ def load_frames(img_nums, stack_path, scale=None, normalize_input=True, dtype=fl
2000
2180
  """
2001
2181
  Loads and optionally normalizes and rescales specified frames from a stack located at a given path.
2002
2182
 
2003
- This function reads specified frames from a stack file, applying systematic adjustments to ensure
2004
- the channel axis is last. It supports optional normalization of the input frames and rescaling. An
2005
- artificial pixel modification is applied to frames with uniform values to prevent errors during
2183
+ This function reads specified frames from a stack file, applying systematic adjustments to ensure
2184
+ the channel axis is last. It supports optional normalization of the input frames and rescaling. An
2185
+ artificial pixel modification is applied to frames with uniform values to prevent errors during
2006
2186
  normalization.
2007
2187
 
2008
2188
  Parameters
@@ -2014,7 +2194,7 @@ def load_frames(img_nums, stack_path, scale=None, normalize_input=True, dtype=fl
2014
2194
  scale : float, optional
2015
2195
  The scaling factor to apply to the frames. If None, no scaling is applied (default is None).
2016
2196
  normalize_input : bool, optional
2017
- Whether to normalize the loaded frames. If True, normalization is applied according to
2197
+ Whether to normalize the loaded frames. If True, normalization is applied according to
2018
2198
  `normalize_kwargs` (default is True).
2019
2199
  dtype : data-type, optional
2020
2200
  The desired data-type for the output frames (default is float).
@@ -2030,38 +2210,39 @@ def load_frames(img_nums, stack_path, scale=None, normalize_input=True, dtype=fl
2030
2210
  Raises
2031
2211
  ------
2032
2212
  Exception
2033
- Prints an error message if the specified frames cannot be loaded or if there is a mismatch between
2213
+ Prints an error message if the specified frames cannot be loaded or if there is a mismatch between
2034
2214
  the provided experiment channel information and the stack format.
2035
2215
 
2036
2216
  Notes
2037
2217
  -----
2038
2218
  - The function uses scikit-image for reading frames and supports multi-frame TIFF stacks.
2039
2219
  - Normalization and scaling are optional and can be customized through function parameters.
2040
- - A workaround is implemented for frames with uniform pixel values to prevent normalization errors by
2220
+ - A workaround is implemented for frames with uniform pixel values to prevent normalization errors by
2041
2221
  adding a 'fake' pixel.
2042
2222
 
2043
2223
  Examples
2044
2224
  --------
2045
2225
  >>> frames = load_frames([0, 1, 2], '/path/to/stack.tif', scale=0.5, normalize_input=True, dtype=np.uint8)
2046
- # Loads the first three frames from '/path/to/stack.tif', normalizes them, rescales by a factor of 0.5,
2226
+ # Loads the first three frames from '/path/to/stack.tif', normalizes them, rescales by a factor of 0.5,
2047
2227
  # and converts them to uint8 data type.
2048
-
2228
+
2049
2229
  """
2050
2230
 
2051
2231
  try:
2052
2232
  frames = skio.imread(stack_path, key=img_nums, plugin="tifffile")
2053
2233
  except Exception as e:
2054
- print(f'Error in loading the frame {img_nums} {e}. Please check that the experiment channel information is consistent with the movie being read.')
2234
+ print(
2235
+ f'Error in loading the frame {img_nums} {e}. Please check that the experiment channel information is consistent with the movie being read.')
2055
2236
  return None
2056
2237
 
2057
- if frames.ndim==3:
2238
+ if frames.ndim == 3:
2058
2239
  # Systematically move channel axis to the end
2059
2240
  channel_axis = np.argmin(frames.shape)
2060
2241
  frames = np.moveaxis(frames, channel_axis, -1)
2061
2242
 
2062
2243
  if frames.ndim==2:
2063
2244
  frames = frames[:,:,np.newaxis].astype(float)
2064
-
2245
+
2065
2246
  if normalize_input:
2066
2247
  frames = normalize_multichannel(frames, **normalize_kwargs)
2067
2248
 
@@ -2072,9 +2253,9 @@ def load_frames(img_nums, stack_path, scale=None, normalize_input=True, dtype=fl
2072
2253
  # add a fake pixel to prevent auto normalization errors on images that are uniform
2073
2254
  # to revisit
2074
2255
  for k in range(frames.shape[2]):
2075
- unique_values = np.unique(frames[:,:,k])
2076
- if len(unique_values)==1:
2077
- frames[0,0,k] += 1
2256
+ unique_values = np.unique(frames[:, :, k])
2257
+ if len(unique_values) == 1:
2258
+ frames[0, 0, k] += 1
2078
2259
 
2079
2260
  return frames.astype(dtype)
2080
2261
 
@@ -2084,9 +2265,9 @@ def get_stack_normalization_values(stack, percentiles=None, ignore_gray_value=0.
2084
2265
  """
2085
2266
  Computes the normalization value ranges (minimum and maximum) for each channel in a 4D stack based on specified percentiles.
2086
2267
 
2087
- This function calculates the value ranges for normalizing each channel within a 4-dimensional stack, with dimensions
2088
- expected to be in the order of Time (T), Y (height), X (width), and Channels (C). The normalization values are determined
2089
- by the specified percentiles for each channel. An option to ignore a specific gray value during computation is provided,
2268
+ This function calculates the value ranges for normalizing each channel within a 4-dimensional stack, with dimensions
2269
+ expected to be in the order of Time (T), Y (height), X (width), and Channels (C). The normalization values are determined
2270
+ by the specified percentiles for each channel. An option to ignore a specific gray value during computation is provided,
2090
2271
  though its effect is not implemented in this snippet.
2091
2272
 
2092
2273
  Parameters
@@ -2094,30 +2275,30 @@ def get_stack_normalization_values(stack, percentiles=None, ignore_gray_value=0.
2094
2275
  stack : ndarray
2095
2276
  The input 4D stack with dimensions TYXC from which to calculate normalization values.
2096
2277
  percentiles : tuple, list of tuples, optional
2097
- The percentile values (low, high) used to calculate the normalization ranges for each channel. If a single tuple
2098
- is provided, it is applied to all channels. If a list of tuples is provided, each tuple is applied to the
2278
+ The percentile values (low, high) used to calculate the normalization ranges for each channel. If a single tuple
2279
+ is provided, it is applied to all channels. If a list of tuples is provided, each tuple is applied to the
2099
2280
  corresponding channel. If None, defaults to (0., 99.99) for each channel.
2100
2281
  ignore_gray_value : float, optional
2101
- A gray value to potentially ignore during the calculation. This parameter is provided for interface consistency
2282
+ A gray value to potentially ignore during the calculation. This parameter is provided for interface consistency
2102
2283
  but is not utilized in the current implementation (default is 0.).
2103
2284
 
2104
2285
  Returns
2105
2286
  -------
2106
2287
  list of tuples
2107
- A list where each tuple contains the (minimum, maximum) values for normalizing each channel based on the specified
2288
+ A list where each tuple contains the (minimum, maximum) values for normalizing each channel based on the specified
2108
2289
  percentiles.
2109
2290
 
2110
2291
  Raises
2111
2292
  ------
2112
2293
  AssertionError
2113
- If the input stack does not have 4 dimensions, or if the length of the `percentiles` list does not match the number
2294
+ If the input stack does not have 4 dimensions, or if the length of the `percentiles` list does not match the number
2114
2295
  of channels in the stack.
2115
2296
 
2116
2297
  Notes
2117
2298
  -----
2118
- - The function assumes the input stack is in TYXC format, where T is the time dimension, Y and X are spatial dimensions,
2299
+ - The function assumes the input stack is in TYXC format, where T is the time dimension, Y and X are spatial dimensions,
2119
2300
  and C is the channel dimension.
2120
- - Memory management via `gc.collect()` is employed after calculating normalization values for each channel to mitigate
2301
+ - Memory management via `gc.collect()` is employed after calculating normalization values for each channel to mitigate
2121
2302
  potential memory issues with large datasets.
2122
2303
 
2123
2304
  Examples
@@ -2125,23 +2306,24 @@ def get_stack_normalization_values(stack, percentiles=None, ignore_gray_value=0.
2125
2306
  >>> stack = np.random.rand(5, 100, 100, 3) # Example 4D stack with 3 channels
2126
2307
  >>> normalization_values = get_stack_normalization_values(stack, percentiles=((1, 99), (2, 98), (0, 100)))
2127
2308
  # Calculates normalization ranges for each channel using the specified percentiles.
2128
-
2309
+
2129
2310
  """
2130
2311
 
2131
- assert stack.ndim==4,f'Wrong number of dimensions for the stack, expect TYXC (4) got {stack.ndim}.'
2312
+ assert stack.ndim == 4, f'Wrong number of dimensions for the stack, expect TYXC (4) got {stack.ndim}.'
2132
2313
  if percentiles is None:
2133
- percentiles = [(0.,99.99)]*stack.shape[-1]
2134
- elif isinstance(percentiles,tuple):
2135
- percentiles = [percentiles]*stack.shape[-1]
2136
- elif isinstance(percentiles,list):
2137
- assert len(percentiles)==stack.shape[-1],f'Mismatch between the provided percentiles and the number of channels {stack.shape[-1]}. If you meant to apply the same percentiles to all channels, please provide a single tuple.'
2314
+ percentiles = [(0., 99.99)] * stack.shape[-1]
2315
+ elif isinstance(percentiles, tuple):
2316
+ percentiles = [percentiles] * stack.shape[-1]
2317
+ elif isinstance(percentiles, list):
2318
+ assert len(percentiles) == stack.shape[
2319
+ -1], f'Mismatch between the provided percentiles and the number of channels {stack.shape[-1]}. If you meant to apply the same percentiles to all channels, please provide a single tuple.'
2138
2320
 
2139
2321
  values = []
2140
2322
  for c in range(stack.shape[-1]):
2141
2323
  perc = percentiles[c]
2142
- mi = np.nanpercentile(stack[:,:,:,c].flatten(),perc[0],keepdims=True)[0]
2143
- ma = np.nanpercentile(stack[:,:,:,c].flatten(),perc[1],keepdims=True)[0]
2144
- values.append(tuple((mi,ma)))
2324
+ mi = np.nanpercentile(stack[:, :, :, c].flatten(), perc[0], keepdims=True)[0]
2325
+ ma = np.nanpercentile(stack[:, :, :, c].flatten(), perc[1], keepdims=True)[0]
2326
+ values.append(tuple((mi, ma)))
2145
2327
  gc.collect()
2146
2328
 
2147
2329
  return values
@@ -2150,15 +2332,15 @@ def get_stack_normalization_values(stack, percentiles=None, ignore_gray_value=0.
2150
2332
  def get_positions_in_well(well):
2151
2333
 
2152
2334
  """
2153
- Retrieves the list of position directories within a specified well directory,
2335
+ Retrieves the list of position directories within a specified well directory,
2154
2336
  formatted as a NumPy array of strings.
2155
2337
 
2156
- This function identifies position directories based on their naming convention,
2157
- which must include a numeric identifier following the well's name. The well's name
2158
- is expected to start with 'W' (e.g., 'W1'), followed by a numeric identifier. Position
2159
- directories are assumed to be named with this numeric identifier directly after the well
2160
- identifier, without the 'W'. For example, positions within well 'W1' might be named
2161
- '101', '102', etc. This function will glob these directories and return their full
2338
+ This function identifies position directories based on their naming convention,
2339
+ which must include a numeric identifier following the well's name. The well's name
2340
+ is expected to start with 'W' (e.g., 'W1'), followed by a numeric identifier. Position
2341
+ directories are assumed to be named with this numeric identifier directly after the well
2342
+ identifier, without the 'W'. For example, positions within well 'W1' might be named
2343
+ '101', '102', etc. This function will glob these directories and return their full
2162
2344
  paths as a NumPy array.
2163
2345
 
2164
2346
  Parameters
@@ -2169,13 +2351,13 @@ def get_positions_in_well(well):
2169
2351
  Returns
2170
2352
  -------
2171
2353
  np.ndarray
2172
- An array of strings, each representing the full path to a position directory within
2354
+ An array of strings, each representing the full path to a position directory within
2173
2355
  the specified well. The array is empty if no position directories are found.
2174
2356
 
2175
2357
  Notes
2176
2358
  -----
2177
2359
  - This function relies on a specific naming convention for wells and positions. It assumes
2178
- that each well directory is prefixed with 'W' followed by a numeric identifier, and
2360
+ that each well directory is prefixed with 'W' followed by a numeric identifier, and
2179
2361
  position directories are named starting with this numeric identifier directly.
2180
2362
 
2181
2363
  Examples
@@ -2183,54 +2365,54 @@ def get_positions_in_well(well):
2183
2365
  >>> get_positions_in_well('/path/to/experiment/W1')
2184
2366
  # This might return an array like array(['/path/to/experiment/W1/101', '/path/to/experiment/W1/102'])
2185
2367
  if position directories '101' and '102' exist within the well 'W1' directory.
2186
-
2368
+
2187
2369
  """
2188
2370
 
2189
2371
  if well.endswith(os.sep):
2190
2372
  well = well[:-1]
2191
2373
 
2192
- w_numeric = os.path.split(well)[-1].replace('W','')
2193
- positions = natsorted(glob(os.sep.join([well,f'{w_numeric}*{os.sep}'])))
2374
+ w_numeric = os.path.split(well)[-1].replace('W', '')
2375
+ positions = natsorted(glob(os.sep.join([well, f'{w_numeric}*{os.sep}'])))
2194
2376
 
2195
- return np.array(positions,dtype=str)
2377
+ return np.array(positions, dtype=str)
2196
2378
 
2197
2379
 
2198
2380
  def extract_experiment_folder_output(experiment_folder, destination_folder):
2199
2381
 
2200
2382
  """
2201
- Copies the output subfolder and associated tables from an experiment folder to a new location,
2383
+ Copies the output subfolder and associated tables from an experiment folder to a new location,
2202
2384
  making the experiment folder much lighter by only keeping essential data.
2203
-
2204
- This function takes the path to an experiment folder and a destination folder as input.
2205
- It creates a copy of the experiment folder at the destination, but only includes the output subfolders
2206
- and their associated tables for each well and position within the experiment.
2207
- This operation significantly reduces the size of the experiment data by excluding non-essential files.
2208
-
2209
- The structure of the copied experiment folder is preserved, including the configuration file,
2210
- well directories, and position directories within each well.
2385
+
2386
+ This function takes the path to an experiment folder and a destination folder as input.
2387
+ It creates a copy of the experiment folder at the destination, but only includes the output subfolders
2388
+ and their associated tables for each well and position within the experiment.
2389
+ This operation significantly reduces the size of the experiment data by excluding non-essential files.
2390
+
2391
+ The structure of the copied experiment folder is preserved, including the configuration file,
2392
+ well directories, and position directories within each well.
2211
2393
  Only the 'output' subfolder and its 'tables' subdirectory are copied for each position.
2212
-
2394
+
2213
2395
  Parameters
2214
2396
  ----------
2215
2397
  experiment_folder : str
2216
- The path to the source experiment folder from which to extract data.
2398
+ The path to the source experiment folder from which to extract data.
2217
2399
  destination_folder : str
2218
- The path to the destination folder where the reduced copy of the experiment
2400
+ The path to the destination folder where the reduced copy of the experiment
2219
2401
  will be created.
2220
-
2402
+
2221
2403
  Notes
2222
2404
  -----
2223
- - This function assumes that the structure of the experiment folder is consistent,
2224
- with wells organized in subdirectories and each containing a position subdirectory.
2405
+ - This function assumes that the structure of the experiment folder is consistent,
2406
+ with wells organized in subdirectories and each containing a position subdirectory.
2225
2407
  Each position subdirectory should have an 'output' folder and a 'tables' subfolder within it.
2226
-
2227
- - The function also assumes the existence of a configuration file in the root of the
2408
+
2409
+ - The function also assumes the existence of a configuration file in the root of the
2228
2410
  experiment folder, which is copied to the root of the destination experiment folder.
2229
-
2411
+
2230
2412
  Examples
2231
2413
  --------
2232
2414
  >>> extract_experiment_folder_output('/path/to/experiment_folder', '/path/to/destination_folder')
2233
- # This will copy the 'experiment_folder' to 'destination_folder', including only
2415
+ # This will copy the 'experiment_folder' to 'destination_folder', including only
2234
2416
  # the output subfolders and their tables for each well and position.
2235
2417
 
2236
2418
  """
@@ -2240,21 +2422,21 @@ def extract_experiment_folder_output(experiment_folder, destination_folder):
2240
2422
  experiment_folder = experiment_folder[:-1]
2241
2423
  if destination_folder.endswith(os.sep):
2242
2424
  destination_folder = destination_folder[:-1]
2243
-
2425
+
2244
2426
  exp_name = experiment_folder.split(os.sep)[-1]
2245
2427
  output_path = os.sep.join([destination_folder, exp_name])
2246
2428
  if not os.path.exists(output_path):
2247
2429
  os.mkdir(output_path)
2248
2430
 
2249
2431
  config = get_config(experiment_folder)
2250
- copyfile(config,os.sep.join([output_path,os.path.split(config)[-1]]))
2251
-
2432
+ copyfile(config, os.sep.join([output_path, os.path.split(config)[-1]]))
2433
+
2252
2434
  wells_src = get_experiment_wells(experiment_folder)
2253
2435
  wells = [w.split(os.sep)[-2] for w in wells_src]
2254
2436
 
2255
- for k,w in enumerate(wells):
2256
-
2257
- well_output_path = os.sep.join([output_path,w])
2437
+ for k, w in enumerate(wells):
2438
+
2439
+ well_output_path = os.sep.join([output_path, w])
2258
2440
  if not os.path.exists(well_output_path):
2259
2441
  os.mkdir(well_output_path)
2260
2442
 
@@ -2270,16 +2452,16 @@ def extract_experiment_folder_output(experiment_folder, destination_folder):
2270
2452
 
2271
2453
  if not os.path.exists(output_folder):
2272
2454
  os.mkdir(output_folder)
2273
-
2455
+
2274
2456
  if not os.path.exists(output_tables_folder):
2275
- os.mkdir(output_tables_folder)
2457
+ os.mkdir(output_tables_folder)
2276
2458
 
2277
- tab_path = glob(pos+os.sep.join(['output','tables',f'*']))
2278
-
2279
- for t in tab_path:
2280
- copyfile(t,os.sep.join([output_tables_folder,os.path.split(t)[-1]]))
2459
+ tab_path = glob(pos + os.sep.join(['output', 'tables', f'*']))
2281
2460
 
2461
+ for t in tab_path:
2462
+ copyfile(t, os.sep.join([output_tables_folder, os.path.split(t)[-1]]))
2282
2463
 
2283
2464
 
2284
2465
  if __name__ == '__main__':
2285
- control_segmentation_napari("/home/limozin/Documents/Experiments/MinimumJan/W4/401/", prefix='Aligned', population="target", flush_memory=False)
2466
+ control_segmentation_napari("/home/limozin/Documents/Experiments/MinimumJan/W4/401/", prefix='Aligned',
2467
+ population="target", flush_memory=False)