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