celldetective 1.3.4__py3-none-any.whl → 1.3.5__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.
celldetective/io.py CHANGED
@@ -1,30 +1,65 @@
1
1
  from natsort import natsorted
2
+ from PyQt5.QtWidgets import QMessageBox
2
3
  from glob import glob
3
4
  from tifffile import imread, TiffFile
4
5
  import numpy as np
5
6
  import os
6
7
  import pandas as pd
7
8
  import napari
9
+ import json
10
+
8
11
  import gc
9
12
  from tqdm import tqdm
13
+ import threading
14
+ import concurrent.futures
15
+
10
16
  from csbdeep.utils import normalize_mi_ma
17
+ from csbdeep.io import save_tiff_imagej_compatible
18
+
11
19
  import skimage.io as skio
20
+ from skimage.measure import regionprops_table, label
21
+
12
22
  from scipy.ndimage import zoom
13
23
  from btrack.datasets import cell_config
14
24
  from magicgui import magicgui
15
- from csbdeep.io import save_tiff_imagej_compatible
16
25
  from pathlib import Path, PurePath
17
26
  from shutil import copyfile, rmtree
18
- from celldetective.utils import ConfigSectionMap, extract_experiment_channels, _extract_labels_from_config, get_zenodo_files, download_zenodo_file
19
- import json
20
- from skimage.measure import regionprops_table
21
- from celldetective.utils import _estimate_scale_factor, _extract_channel_indices_from_config, _extract_channel_indices, ConfigSectionMap, _extract_nbr_channels_from_config, _get_img_num_per_channel, normalize_per_channel
22
- from celldetective.utils import interpolate_nan
23
- import concurrent.futures
24
- from tifffile import imwrite
27
+
28
+ from celldetective.utils import ConfigSectionMap, extract_experiment_channels, _extract_labels_from_config, get_zenodo_files, download_zenodo_file, interpolate_nan
29
+ from celldetective.utils import _estimate_scale_factor, _extract_channel_indices_from_config, _extract_channel_indices, _extract_nbr_channels_from_config, _get_img_num_per_channel, normalize_per_channel
30
+
25
31
  from stardist import fill_label_holes
26
32
 
33
+
27
34
  def extract_experiment_from_well(well_path):
35
+
36
+ """
37
+ Extracts the experiment directory path from a given well directory path.
38
+
39
+ Parameters
40
+ ----------
41
+ well_path : str
42
+ The file system path to a well directory. The path should end with the well folder,
43
+ but it does not need to include a trailing separator.
44
+
45
+ Returns
46
+ -------
47
+ str
48
+ The path to the experiment directory, which is assumed to be two levels above the well directory.
49
+
50
+ Notes
51
+ -----
52
+ - This function expects the well directory to be organized such that the experiment directory is
53
+ two levels above it in the file system hierarchy.
54
+ - If the input path does not end with a file separator (`os.sep`), one is appended before processing.
55
+
56
+ Example
57
+ -------
58
+ >>> well_path = "/path/to/experiment/plate/well"
59
+ >>> extract_experiment_from_well(well_path)
60
+ '/path/to/experiment'
61
+ """
62
+
28
63
  if not well_path.endswith(os.sep):
29
64
  well_path += os.sep
30
65
  exp_path_blocks = well_path.split(os.sep)[:-2]
@@ -32,6 +67,35 @@ def extract_experiment_from_well(well_path):
32
67
  return experiment
33
68
 
34
69
  def extract_well_from_position(pos_path):
70
+
71
+ """
72
+ Extracts the well directory path from a given position directory path.
73
+
74
+ Parameters
75
+ ----------
76
+ pos_path : str
77
+ The file system path to a position directory. The path should end with the position folder,
78
+ but it does not need to include a trailing separator.
79
+
80
+ Returns
81
+ -------
82
+ str
83
+ The path to the well directory, which is assumed to be two levels above the position directory,
84
+ with a trailing separator appended.
85
+
86
+ Notes
87
+ -----
88
+ - This function expects the position directory to be organized such that the well directory is
89
+ two levels above it in the file system hierarchy.
90
+ - If the input path does not end with a file separator (`os.sep`), one is appended before processing.
91
+
92
+ Example
93
+ -------
94
+ >>> pos_path = "/path/to/experiment/plate/well/position"
95
+ >>> extract_well_from_position(pos_path)
96
+ '/path/to/experiment/plate/well/'
97
+ """
98
+
35
99
  if not pos_path.endswith(os.sep):
36
100
  pos_path += os.sep
37
101
  well_path_blocks = pos_path.split(os.sep)[:-2]
@@ -39,14 +103,92 @@ def extract_well_from_position(pos_path):
39
103
  return well_path
40
104
 
41
105
  def extract_experiment_from_position(pos_path):
42
- if not pos_path.endswith(os.sep):
43
- pos_path += os.sep
44
- exp_path_blocks = pos_path.split(os.sep)[:-3]
106
+
107
+ """
108
+ Extracts the experiment directory path from a given position directory path.
109
+
110
+ Parameters
111
+ ----------
112
+ pos_path : str
113
+ The file system path to a position directory. The path should end with the position folder,
114
+ but it does not need to include a trailing separator.
115
+
116
+ Returns
117
+ -------
118
+ str
119
+ The path to the experiment directory, which is assumed to be three levels above the position directory.
120
+
121
+ Notes
122
+ -----
123
+ - This function expects the position directory to be organized hierarchically such that the experiment directory
124
+ is three levels above it in the file system hierarchy.
125
+ - If the input path does not end with a file separator (`os.sep`), one is appended before processing.
126
+
127
+ Example
128
+ -------
129
+ >>> pos_path = "/path/to/experiment/plate/well/position"
130
+ >>> extract_experiment_from_position(pos_path)
131
+ '/path/to/experiment'
132
+ """
133
+
134
+ pos_path = pos_path.replace(os.sep, '/')
135
+ if not pos_path.endswith('/'):
136
+ pos_path += '/'
137
+ exp_path_blocks = pos_path.split('/')[:-3]
45
138
  experiment = os.sep.join(exp_path_blocks)
139
+
46
140
  return experiment
47
141
 
48
142
  def collect_experiment_metadata(pos_path=None, well_path=None):
49
143
 
144
+ """
145
+ Collects and organizes metadata for an experiment based on a given position or well directory path.
146
+
147
+ Parameters
148
+ ----------
149
+ pos_path : str, optional
150
+ The file system path to a position directory. If provided, it will be used to extract metadata.
151
+ This parameter takes precedence over `well_path`.
152
+ well_path : str, optional
153
+ The file system path to a well directory. If `pos_path` is not provided, this path will be used to extract metadata.
154
+
155
+ Returns
156
+ -------
157
+ dict
158
+ A dictionary containing the following metadata:
159
+ - `"pos_path"`: The path to the position directory (or `None` if not provided).
160
+ - `"position"`: The same as `pos_path`.
161
+ - `"pos_name"`: The name of the position (or `0` if `pos_path` is not provided).
162
+ - `"well_path"`: The path to the well directory.
163
+ - `"well_name"`: The name of the well.
164
+ - `"well_nbr"`: The numerical identifier of the well.
165
+ - `"experiment"`: The path to the experiment directory.
166
+ - `"antibody"`: The antibody associated with the well.
167
+ - `"concentration"`: The concentration associated with the well.
168
+ - `"cell_type"`: The cell type associated with the well.
169
+ - `"pharmaceutical_agent"`: The pharmaceutical agent associated with the well.
170
+
171
+ Notes
172
+ -----
173
+ - At least one of `pos_path` or `well_path` must be provided.
174
+ - The function determines the experiment path by navigating the directory structure and extracts metadata for the
175
+ corresponding well and position.
176
+ - The metadata is derived using helper functions like `extract_experiment_from_position`, `extract_well_from_position`,
177
+ and `get_experiment_*` family of functions.
178
+
179
+ Example
180
+ -------
181
+ >>> pos_path = "/path/to/experiment/plate/well/position"
182
+ >>> metadata = collect_experiment_metadata(pos_path=pos_path)
183
+ >>> metadata["well_name"]
184
+ 'W1'
185
+
186
+ >>> well_path = "/path/to/experiment/plate/well"
187
+ >>> metadata = collect_experiment_metadata(well_path=well_path)
188
+ >>> metadata["concentration"]
189
+ 10.0
190
+ """
191
+
50
192
  if pos_path is not None:
51
193
  if not pos_path.endswith(os.sep):
52
194
  pos_path += os.sep
@@ -109,6 +251,38 @@ def get_experiment_wells(experiment):
109
251
 
110
252
  def get_config(experiment):
111
253
 
254
+ """
255
+ Retrieves the path to the configuration file for a given experiment.
256
+
257
+ Parameters
258
+ ----------
259
+ experiment : str
260
+ The file system path to the experiment directory.
261
+
262
+ Returns
263
+ -------
264
+ str
265
+ The full path to the configuration file (`config.ini`) within the experiment directory.
266
+
267
+ Raises
268
+ ------
269
+ AssertionError
270
+ If the `config.ini` file does not exist in the specified experiment directory.
271
+
272
+ Notes
273
+ -----
274
+ - The function ensures that the provided experiment path ends with the appropriate file separator (`os.sep`)
275
+ before appending `config.ini` to locate the configuration file.
276
+ - The configuration file is expected to be named `config.ini` and located at the root of the experiment directory.
277
+
278
+ Example
279
+ -------
280
+ >>> experiment = "/path/to/experiment"
281
+ >>> config_path = get_config(experiment)
282
+ >>> print(config_path)
283
+ '/path/to/experiment/config.ini'
284
+ """
285
+
112
286
  if not experiment.endswith(os.sep):
113
287
  experiment += os.sep
114
288
 
@@ -120,7 +294,42 @@ def get_config(experiment):
120
294
 
121
295
  def get_spatial_calibration(experiment):
122
296
 
123
-
297
+ """
298
+ Retrieves the spatial calibration factor for an experiment.
299
+
300
+ Parameters
301
+ ----------
302
+ experiment : str
303
+ The file system path to the experiment directory.
304
+
305
+ Returns
306
+ -------
307
+ float
308
+ The spatial calibration factor (pixels to micrometers conversion), extracted from the experiment's configuration file.
309
+
310
+ Raises
311
+ ------
312
+ AssertionError
313
+ If the configuration file (`config.ini`) does not exist in the specified experiment directory.
314
+ KeyError
315
+ If the "pxtoum" key is not found under the "MovieSettings" section in the configuration file.
316
+ ValueError
317
+ If the retrieved "pxtoum" value cannot be converted to a float.
318
+
319
+ Notes
320
+ -----
321
+ - The function retrieves the calibration factor by first locating the configuration file for the experiment using `get_config()`.
322
+ - It expects the configuration file to have a section named `MovieSettings` containing the key `pxtoum`.
323
+ - This factor defines the conversion from pixels to micrometers for spatial measurements.
324
+
325
+ Example
326
+ -------
327
+ >>> experiment = "/path/to/experiment"
328
+ >>> calibration = get_spatial_calibration(experiment)
329
+ >>> print(calibration)
330
+ 0.325 # pixels-to-micrometers conversion factor
331
+ """
332
+
124
333
  config = get_config(experiment)
125
334
  PxToUm = float(ConfigSectionMap(config, "MovieSettings")["pxtoum"])
126
335
 
@@ -129,6 +338,42 @@ def get_spatial_calibration(experiment):
129
338
 
130
339
  def get_temporal_calibration(experiment):
131
340
 
341
+ """
342
+ Retrieves the temporal calibration factor for an experiment.
343
+
344
+ Parameters
345
+ ----------
346
+ experiment : str
347
+ The file system path to the experiment directory.
348
+
349
+ Returns
350
+ -------
351
+ float
352
+ The temporal calibration factor (frames to minutes conversion), extracted from the experiment's configuration file.
353
+
354
+ Raises
355
+ ------
356
+ AssertionError
357
+ If the configuration file (`config.ini`) does not exist in the specified experiment directory.
358
+ KeyError
359
+ If the "frametomin" key is not found under the "MovieSettings" section in the configuration file.
360
+ ValueError
361
+ If the retrieved "frametomin" value cannot be converted to a float.
362
+
363
+ Notes
364
+ -----
365
+ - The function retrieves the calibration factor by locating the configuration file for the experiment using `get_config()`.
366
+ - It expects the configuration file to have a section named `MovieSettings` containing the key `frametomin`.
367
+ - This factor defines the conversion from frames to minutes for temporal measurements.
368
+
369
+ Example
370
+ -------
371
+ >>> experiment = "/path/to/experiment"
372
+ >>> calibration = get_temporal_calibration(experiment)
373
+ >>> print(calibration)
374
+ 0.5 # frames-to-minutes conversion factor
375
+ """
376
+
132
377
  config = get_config(experiment)
133
378
  FrameToMin = float(ConfigSectionMap(config, "MovieSettings")["frametomin"])
134
379
 
@@ -137,6 +382,46 @@ def get_temporal_calibration(experiment):
137
382
 
138
383
  def get_experiment_concentrations(experiment, dtype=str):
139
384
 
385
+ """
386
+ Retrieves the concentrations associated with each well in an experiment.
387
+
388
+ Parameters
389
+ ----------
390
+ experiment : str
391
+ The file system path to the experiment directory.
392
+ dtype : type, optional
393
+ The data type to which the concentrations should be converted (default is `str`).
394
+
395
+ Returns
396
+ -------
397
+ numpy.ndarray
398
+ An array of concentrations for each well, converted to the specified data type.
399
+
400
+ Raises
401
+ ------
402
+ AssertionError
403
+ If the configuration file (`config.ini`) does not exist in the specified experiment directory.
404
+ KeyError
405
+ If the "concentrations" key is not found under the "Labels" section in the configuration file.
406
+ ValueError
407
+ If the retrieved concentrations cannot be converted to the specified data type.
408
+
409
+ Notes
410
+ -----
411
+ - The function retrieves the configuration file using `get_config()` and expects a section `Labels` containing
412
+ a key `concentrations`.
413
+ - The concentrations are assumed to be comma-separated values.
414
+ - If the number of wells does not match the number of concentrations, the function generates a default set
415
+ of values ranging from 0 to the number of wells minus 1.
416
+ - The resulting concentrations are converted to the specified `dtype` before being returned.
417
+
418
+ Example
419
+ -------
420
+ >>> experiment = "/path/to/experiment"
421
+ >>> concentrations = get_experiment_concentrations(experiment, dtype=float)
422
+ >>> print(concentrations)
423
+ [0.1, 0.2, 0.5, 1.0]
424
+ """
140
425
 
141
426
  config = get_config(experiment)
142
427
  wells = get_experiment_wells(experiment)
@@ -150,6 +435,48 @@ def get_experiment_concentrations(experiment, dtype=str):
150
435
 
151
436
 
152
437
  def get_experiment_cell_types(experiment, dtype=str):
438
+
439
+ """
440
+ Retrieves the cell types associated with each well in an experiment.
441
+
442
+ Parameters
443
+ ----------
444
+ experiment : str
445
+ The file system path to the experiment directory.
446
+ dtype : type, optional
447
+ The data type to which the cell types should be converted (default is `str`).
448
+
449
+ Returns
450
+ -------
451
+ numpy.ndarray
452
+ An array of cell types for each well, converted to the specified data type.
453
+
454
+ Raises
455
+ ------
456
+ AssertionError
457
+ If the configuration file (`config.ini`) does not exist in the specified experiment directory.
458
+ KeyError
459
+ If the "cell_types" key is not found under the "Labels" section in the configuration file.
460
+ ValueError
461
+ If the retrieved cell types cannot be converted to the specified data type.
462
+
463
+ Notes
464
+ -----
465
+ - The function retrieves the configuration file using `get_config()` and expects a section `Labels` containing
466
+ a key `cell_types`.
467
+ - The cell types are assumed to be comma-separated values.
468
+ - If the number of wells does not match the number of cell types, the function generates a default set
469
+ of values ranging from 0 to the number of wells minus 1.
470
+ - The resulting cell types are converted to the specified `dtype` before being returned.
471
+
472
+ Example
473
+ -------
474
+ >>> experiment = "/path/to/experiment"
475
+ >>> cell_types = get_experiment_cell_types(experiment, dtype=str)
476
+ >>> print(cell_types)
477
+ ['TypeA', 'TypeB', 'TypeC', 'TypeD']
478
+ """
479
+
153
480
  config = get_config(experiment)
154
481
  wells = get_experiment_wells(experiment)
155
482
  nbr_of_wells = len(wells)
@@ -163,6 +490,44 @@ def get_experiment_cell_types(experiment, dtype=str):
163
490
 
164
491
  def get_experiment_antibodies(experiment, dtype=str):
165
492
 
493
+ """
494
+ Retrieve the list of antibodies used in an experiment.
495
+
496
+ This function extracts antibody labels for the wells in the given experiment
497
+ based on the configuration file. If the number of wells does not match the
498
+ number of antibody labels provided in the configuration, it generates a
499
+ sequence of default numeric labels.
500
+
501
+ Parameters
502
+ ----------
503
+ experiment : str
504
+ The identifier or name of the experiment to retrieve antibodies for.
505
+ dtype : type, optional
506
+ The data type to which the antibody labels should be cast. Default is `str`.
507
+
508
+ Returns
509
+ -------
510
+ numpy.ndarray
511
+ An array of antibody labels with the specified data type. If no antibodies
512
+ are specified or there is a mismatch, numeric labels are generated instead.
513
+
514
+ Notes
515
+ -----
516
+ - The function assumes the experiment's configuration can be loaded using
517
+ `get_config` and that the antibodies are listed under the "Labels" section
518
+ with the key `"antibodies"`.
519
+ - A mismatch between the number of wells and antibody labels will result in
520
+ numeric labels generated using `numpy.linspace`.
521
+
522
+ Examples
523
+ --------
524
+ >>> get_experiment_antibodies("path/to/experiment1")
525
+ array(['A1', 'A2', 'A3'], dtype='<U2')
526
+
527
+ >>> get_experiment_antibodies("path/to/experiment2", dtype=int)
528
+ array([0, 1, 2])
529
+ """
530
+
166
531
  config = get_config(experiment)
167
532
  wells = get_experiment_wells(experiment)
168
533
  nbr_of_wells = len(wells)
@@ -175,6 +540,48 @@ def get_experiment_antibodies(experiment, dtype=str):
175
540
 
176
541
 
177
542
  def get_experiment_pharmaceutical_agents(experiment, dtype=str):
543
+
544
+ """
545
+ Retrieves the antibodies associated with each well in an experiment.
546
+
547
+ Parameters
548
+ ----------
549
+ experiment : str
550
+ The file system path to the experiment directory.
551
+ dtype : type, optional
552
+ The data type to which the antibodies should be converted (default is `str`).
553
+
554
+ Returns
555
+ -------
556
+ numpy.ndarray
557
+ An array of antibodies for each well, converted to the specified data type.
558
+
559
+ Raises
560
+ ------
561
+ AssertionError
562
+ If the configuration file (`config.ini`) does not exist in the specified experiment directory.
563
+ KeyError
564
+ If the "antibodies" key is not found under the "Labels" section in the configuration file.
565
+ ValueError
566
+ If the retrieved antibody values cannot be converted to the specified data type.
567
+
568
+ Notes
569
+ -----
570
+ - The function retrieves the configuration file using `get_config()` and expects a section `Labels` containing
571
+ a key `antibodies`.
572
+ - The antibody names are assumed to be comma-separated values.
573
+ - If the number of wells does not match the number of antibodies, the function generates a default set
574
+ of values ranging from 0 to the number of wells minus 1.
575
+ - The resulting antibody names are converted to the specified `dtype` before being returned.
576
+
577
+ Example
578
+ -------
579
+ >>> experiment = "/path/to/experiment"
580
+ >>> antibodies = get_experiment_antibodies(experiment, dtype=str)
581
+ >>> print(antibodies)
582
+ ['AntibodyA', 'AntibodyB', 'AntibodyC', 'AntibodyD']
583
+ """
584
+
178
585
  config = get_config(experiment)
179
586
  wells = get_experiment_wells(experiment)
180
587
  nbr_of_wells = len(wells)
@@ -485,59 +892,61 @@ def get_position_movie_path(pos, prefix=''):
485
892
 
486
893
  def load_experiment_tables(experiment, population='targets', well_option='*', position_option='*',
487
894
  return_pos_info=False, load_pickle=False):
895
+
488
896
  """
489
- Loads and aggregates data tables for specified wells and positions within an experiment,
490
- optionally returning position information alongside the aggregated data table.
897
+ Load tabular data for an experiment, optionally including position-level information.
491
898
 
492
- This function collects data from tables associated with specific population types across
493
- various wells and positions within an experiment. It uses the experiment's configuration
494
- to gather metadata such as movie prefix, concentrations, cell types, antibodies, and
495
- pharmaceutical agents. Users can specify which wells and positions to include in the
496
- aggregation through pattern matching, and whether to include detailed position information
497
- in the output.
899
+ This function retrieves and processes tables associated with positions in an experiment.
900
+ It supports filtering by wells and positions, and can load either CSV data or pickle files.
498
901
 
499
902
  Parameters
500
903
  ----------
501
904
  experiment : str
502
- The path to the experiment directory.
905
+ Path to the experiment folder to load data for.
503
906
  population : str, optional
504
- The population type to filter the tables by (default is 'targets' among 'targets and "effectors').
505
- well_option : str, optional
506
- A pattern to specify which wells to include (default is '*', which includes all wells).
507
- position_option : str, optional
508
- A pattern to specify which positions to include (default is '*', which includes all positions).
907
+ The population to extract from the position tables (`'targets'` or `'effectors'`). Default is `'targets'`.
908
+ well_option : str or list, optional
909
+ Specifies which wells to include. Default is `'*'`, meaning all wells.
910
+ position_option : str or list, optional
911
+ Specifies which positions to include within selected wells. Default is `'*'`, meaning all positions.
509
912
  return_pos_info : bool, optional
510
- If True, returns a tuple where the first element is the aggregated data table and the
511
- second element is detailed position information (default is False).
913
+ If `True`, also returns a DataFrame containing position-level metadata. Default is `False`.
914
+ load_pickle : bool, optional
915
+ If `True`, loads pre-processed pickle files for the positions instead of raw data. Default is `False`.
512
916
 
513
917
  Returns
514
918
  -------
515
- pandas.DataFrame or (pandas.DataFrame, pandas.DataFrame)
516
- If return_pos_info is False, returns a pandas DataFrame aggregating the data from the
517
- specified tables. If return_pos_info is True, returns a tuple where the first element
518
- is the aggregated data table and the second element is a DataFrame with detailed position
519
- information.
520
-
521
- Raises
522
- ------
523
- FileNotFoundError
524
- If the experiment directory does not exist or specified files within the directory cannot be found.
525
- ValueError
526
- If the specified well or position patterns do not match any directories.
919
+ df : pandas.DataFrame or None
920
+ A DataFrame containing aggregated data for the specified wells and positions, or `None` if no data is found.
921
+ The DataFrame includes metadata such as well and position identifiers, concentrations, antibodies, and other
922
+ experimental parameters.
923
+ df_pos_info : pandas.DataFrame, optional
924
+ A DataFrame with metadata for each position, including file paths and experimental details. Returned only
925
+ if `return_pos_info=True`.
527
926
 
528
927
  Notes
529
928
  -----
530
- - This function assumes that the naming conventions and directory structure of the experiment
531
- follow a specific format, as outlined in the experiment's configuration file.
532
- - The function utilizes several helper functions to extract metadata, interpret well and
533
- position patterns, and load individual position tables. Errors in these helper functions
534
- may propagate up and affect the behavior of this function.
929
+ - The function assumes the experiment's configuration includes details about movie prefixes, concentrations,
930
+ cell types, antibodies, and pharmaceutical agents.
931
+ - Wells and positions can be filtered using `well_option` and `position_option`, respectively. If filtering
932
+ fails or is invalid, those specific wells/positions are skipped.
933
+ - Position-level metadata is assembled into `df_pos_info` and includes paths to data and movies.
535
934
 
536
935
  Examples
537
936
  --------
538
- >>> load_experiment_tables('/path/to/experiment', population='targets', well_option='W1', position_option='1-*')
539
- # This will load and aggregate tables for the 'targets' population within well 'W1' and positions matching '1-*'.
937
+ Load all data for an experiment:
540
938
 
939
+ >>> df = load_experiment_tables("path/to/experiment1")
940
+
941
+ Load data for specific wells and positions, including position metadata:
942
+
943
+ >>> df, df_pos_info = load_experiment_tables(
944
+ ... "experiment_01", well_option=["A1", "B1"], position_option=[0, 1], return_pos_info=True
945
+ ... )
946
+
947
+ Use pickle files for faster loading:
948
+
949
+ >>> df = load_experiment_tables("experiment_01", load_pickle=True)
541
950
  """
542
951
 
543
952
  config = get_config(experiment)
@@ -630,7 +1039,6 @@ def load_experiment_tables(experiment, population='targets', well_option='*', po
630
1039
  return df
631
1040
 
632
1041
 
633
-
634
1042
  def locate_stack(position, prefix='Aligned'):
635
1043
 
636
1044
  """
@@ -694,35 +1102,52 @@ def locate_stack(position, prefix='Aligned'):
694
1102
  def locate_labels(position, population='target', frames=None):
695
1103
 
696
1104
  """
1105
+ Locate and load label images for a given position and population in an experiment.
697
1106
 
698
- Locate and load labels for a specific population.
1107
+ This function retrieves and optionally loads labeled images (e.g., targets or effectors)
1108
+ for a specified position in an experiment. It supports loading all frames, a specific
1109
+ frame, or a list of frames.
699
1110
 
700
1111
  Parameters
701
1112
  ----------
702
1113
  position : str
703
- The position folder within the well where the stack is located.
1114
+ Path to the position directory containing label images.
704
1115
  population : str, optional
705
- The population for which to locate the labels.
706
- Valid options are 'target' and 'effector'.
707
- The default is 'target'.
1116
+ The population to load labels for. Options are `'target'` (or `'targets'`) and
1117
+ `'effector'` (or `'effectors'`). Default is `'target'`.
1118
+ frames : int, list of int, numpy.ndarray, or None, optional
1119
+ Specifies which frames to load:
1120
+ - `None`: Load all frames (default).
1121
+ - `int`: Load a single frame, identified by its index.
1122
+ - `list` or `numpy.ndarray`: Load multiple specific frames.
708
1123
 
709
1124
  Returns
710
1125
  -------
711
- labels : ndarray
712
- The loaded labels as a NumPy array.
1126
+ numpy.ndarray or list of numpy.ndarray
1127
+ If `frames` is `None` or a single integer, returns a NumPy array of the corresponding
1128
+ labels. If `frames` is a list or array, returns a list of NumPy arrays for each frame.
1129
+ If a frame is not found, `None` is returned for that frame.
713
1130
 
714
1131
  Notes
715
1132
  -----
716
- This function locates and loads the labels for a specific population based on the specified position.
717
- It assumes that the labels are stored in a directory named 'labels' or 'labels_effectors'
718
- within the specified position, depending on the population.
719
- The function loads the labels as a NumPy array.
1133
+ - The function assumes label images are stored in subdirectories named `"labels_targets"`
1134
+ or `"labels_effectors"`, with filenames formatted as `####.tif` (e.g., `0001.tif`).
1135
+ - Frame indices are zero-padded to four digits for matching.
1136
+ - If `frames` is invalid or a frame is not found, `None` is returned for that frame.
720
1137
 
721
1138
  Examples
722
1139
  --------
723
- >>> labels = locate_labels(position, population='target')
724
- # Locate and load labels for the target population.
1140
+ Load all label images for a position:
725
1141
 
1142
+ >>> labels = locate_labels("/path/to/position", population="target")
1143
+
1144
+ Load a single frame (frame index 3):
1145
+
1146
+ >>> label = locate_labels("/path/to/position", population="effector", frames=3)
1147
+
1148
+ Load multiple specific frames:
1149
+
1150
+ >>> labels = locate_labels("/path/to/position", population="target", frames=[0, 1, 2])
726
1151
  """
727
1152
 
728
1153
  if not position.endswith(os.sep):
@@ -791,7 +1216,7 @@ def fix_missing_labels(position, population='target', prefix='Aligned'):
791
1216
  position += os.sep
792
1217
 
793
1218
  stack = locate_stack(position, prefix=prefix)
794
- template = np.zeros((stack[0].shape[0], stack[0].shape[1]))
1219
+ template = np.zeros((stack[0].shape[0], stack[0].shape[1]),dtype=int)
795
1220
  all_frames = np.arange(len(stack))
796
1221
 
797
1222
  if population.lower() == "target" or population.lower() == "targets":
@@ -913,37 +1338,52 @@ def load_tracking_data(position, prefix="Aligned", population="target"):
913
1338
 
914
1339
 
915
1340
  def auto_load_number_of_frames(stack_path):
916
-
1341
+
917
1342
  """
1343
+ Automatically determine the number of frames in a TIFF image stack.
918
1344
 
919
- Automatically estimate the number of frames in a stack.
1345
+ This function extracts the number of frames (time slices) from the metadata of a TIFF file
1346
+ or infers it from the stack dimensions when metadata is unavailable. It is robust to
1347
+ variations in metadata structure and handles multi-channel images.
920
1348
 
921
1349
  Parameters
922
1350
  ----------
923
1351
  stack_path : str
924
- The file path to the stack.
1352
+ Path to the TIFF image stack file.
925
1353
 
926
1354
  Returns
927
1355
  -------
928
1356
  int or None
929
- The estimated number of frames in the stack. Returns None if the number of frames cannot be determined.
1357
+ The number of frames in the image stack. Returns `None` if the path is `None`
1358
+ or the frame count cannot be determined.
930
1359
 
931
1360
  Notes
932
1361
  -----
933
- This function attempts to estimate the number of frames in a stack by parsing the image description metadata.
934
- It reads the stack file using the TiffFile from the tifffile library.
935
- It searches for metadata fields containing information about the number of slices or frames.
936
- If the number of slices or frames is found, it returns the estimated length of the movie.
937
- If the number of slices or frames cannot be determined, it returns None.
1362
+ - The function attempts to extract the `frames` or `slices` attributes from the
1363
+ TIFF metadata, specifically the `ImageDescription` tag.
1364
+ - If metadata extraction fails, the function reads the image stack and infers
1365
+ the number of frames based on the stack dimensions.
1366
+ - Multi-channel stacks are handled by assuming the number of channels is specified
1367
+ in the metadata under the `channels` attribute.
938
1368
 
939
1369
  Examples
940
1370
  --------
941
- >>> len_movie = auto_load_number_of_frames(stack_path)
942
- # Automatically estimate the number of frames in the stack.
1371
+ Automatically detect the number of frames in a TIFF stack:
943
1372
 
944
- """
1373
+ >>> frames = auto_load_number_of_frames("experiment_stack.tif")
1374
+ Automatically detected stack length: 120...
945
1375
 
946
- # Try to estimate automatically # frames
1376
+ Handle a single-frame TIFF:
1377
+
1378
+ >>> frames = auto_load_number_of_frames("single_frame_stack.tif")
1379
+ Automatically detected stack length: 1...
1380
+
1381
+ Handle invalid or missing paths gracefully:
1382
+
1383
+ >>> frames = auto_load_number_of_frames(None)
1384
+ >>> print(frames)
1385
+ None
1386
+ """
947
1387
 
948
1388
  if stack_path is None:
949
1389
  return None
@@ -1000,6 +1440,47 @@ def auto_load_number_of_frames(stack_path):
1000
1440
 
1001
1441
 
1002
1442
  def parse_isotropic_radii(string):
1443
+
1444
+ """
1445
+ Parse a string representing isotropic radii into a structured list.
1446
+
1447
+ This function extracts integer values and ranges (denoted by square brackets)
1448
+ from a string input and returns them as a list. Single values are stored as integers,
1449
+ while ranges are represented as lists of two integers.
1450
+
1451
+ Parameters
1452
+ ----------
1453
+ string : str
1454
+ The input string containing radii and ranges, separated by commas or spaces.
1455
+ Ranges should be enclosed in square brackets, e.g., `[1 2]`.
1456
+
1457
+ Returns
1458
+ -------
1459
+ list
1460
+ A list of parsed radii where:
1461
+ - Single integers are included as `int`.
1462
+ - Ranges are included as two-element lists `[start, end]`.
1463
+
1464
+ Examples
1465
+ --------
1466
+ Parse a string with single radii and ranges:
1467
+
1468
+ >>> parse_isotropic_radii("1, [2 3], 4")
1469
+ [1, [2, 3], 4]
1470
+
1471
+ Handle inputs with mixed delimiters:
1472
+
1473
+ >>> parse_isotropic_radii("5 [6 7], 8")
1474
+ [5, [6, 7], 8]
1475
+
1476
+ Notes
1477
+ -----
1478
+ - The function splits the input string by commas or spaces.
1479
+ - It identifies ranges using square brackets and assumes that ranges are always
1480
+ two consecutive values.
1481
+ - Non-integer sections of the string are ignored.
1482
+ """
1483
+
1003
1484
  sections = re.split(',| ', string)
1004
1485
  radii = []
1005
1486
  for k, s in enumerate(sections):
@@ -1061,6 +1542,53 @@ def get_tracking_configs_list(return_path=False):
1061
1542
 
1062
1543
  def interpret_tracking_configuration(config):
1063
1544
 
1545
+ """
1546
+ Interpret and resolve the path for a tracking configuration file.
1547
+
1548
+ This function determines the appropriate configuration file path based on the input.
1549
+ If the input is a string representing an existing path or a known configuration name,
1550
+ it resolves to the correct file path. If the input is invalid or `None`, a default
1551
+ configuration is returned.
1552
+
1553
+ Parameters
1554
+ ----------
1555
+ config : str or None
1556
+ The input configuration, which can be:
1557
+ - A string representing the full path to a configuration file.
1558
+ - A short name of a configuration file without the `.json` extension.
1559
+ - `None` to use a default configuration.
1560
+
1561
+ Returns
1562
+ -------
1563
+ str
1564
+ The resolved path to the configuration file.
1565
+
1566
+ Notes
1567
+ -----
1568
+ - If `config` is a string and the specified path exists, it is returned as-is.
1569
+ - If `config` is a name, the function searches in the `tracking_configs` directory
1570
+ within the `celldetective` models folder.
1571
+ - If the file or name is not found, or if `config` is `None`, the function falls
1572
+ back to a default configuration using `cell_config()`.
1573
+
1574
+ Examples
1575
+ --------
1576
+ Resolve a full path:
1577
+
1578
+ >>> interpret_tracking_configuration("/path/to/config.json")
1579
+ '/path/to/config.json'
1580
+
1581
+ Resolve a named configuration:
1582
+
1583
+ >>> interpret_tracking_configuration("default_tracking")
1584
+ '/path/to/celldetective/models/tracking_configs/default_tracking.json'
1585
+
1586
+ Handle `None` to return the default configuration:
1587
+
1588
+ >>> interpret_tracking_configuration(None)
1589
+ '/path/to/default/config.json'
1590
+ """
1591
+
1064
1592
  if isinstance(config, str):
1065
1593
  if os.path.exists(config):
1066
1594
  return config
@@ -1180,6 +1708,61 @@ def get_pair_signal_models_list(return_path=False):
1180
1708
 
1181
1709
  def locate_signal_model(name, path=None, pairs=False):
1182
1710
 
1711
+ """
1712
+ Locate a signal detection model by name, either locally or from Zenodo.
1713
+
1714
+ This function searches for a signal detection model with the specified name in the local
1715
+ `celldetective` directory. If the model is not found locally, it attempts to download
1716
+ the model from Zenodo.
1717
+
1718
+ Parameters
1719
+ ----------
1720
+ name : str
1721
+ The name of the signal detection model to locate.
1722
+ path : str, optional
1723
+ An additional directory path to search for the model. If provided, this directory
1724
+ is also scanned for matching models. Default is `None`.
1725
+ pairs : bool, optional
1726
+ If `True`, searches for paired signal detection models in the `pair_signal_detection`
1727
+ subdirectory. If `False`, searches in the `signal_detection` subdirectory. Default is `False`.
1728
+
1729
+ Returns
1730
+ -------
1731
+ str or None
1732
+ The full path to the located model directory if found, or `None` if the model is not available
1733
+ locally or on Zenodo.
1734
+
1735
+ Notes
1736
+ -----
1737
+ - The function first searches in the `celldetective/models/signal_detection` or
1738
+ `celldetective/models/pair_signal_detection` directory based on the `pairs` argument.
1739
+ - If a `path` is specified, it is searched in addition to the default directories.
1740
+ - If the model is not found locally, the function queries Zenodo for the model. If available,
1741
+ the model is downloaded to the appropriate `celldetective` subdirectory.
1742
+
1743
+ Examples
1744
+ --------
1745
+ Search for a signal detection model locally:
1746
+
1747
+ >>> locate_signal_model("example_model")
1748
+ 'path/to/celldetective/models/signal_detection/example_model/'
1749
+
1750
+ Search for a paired signal detection model:
1751
+
1752
+ >>> locate_signal_model("paired_model", pairs=True)
1753
+ 'path/to/celldetective/models/pair_signal_detection/paired_model/'
1754
+
1755
+ Include an additional search path:
1756
+
1757
+ >>> locate_signal_model("custom_model", path="/additional/models/")
1758
+ '/additional/models/custom_model/'
1759
+
1760
+ Handle a model available only on Zenodo:
1761
+
1762
+ >>> locate_signal_model("remote_model")
1763
+ 'path/to/celldetective/models/signal_detection/remote_model/'
1764
+ """
1765
+
1183
1766
  main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective"])
1184
1767
  modelpath = os.sep.join([main_dir, "models", "signal_detection", os.sep])
1185
1768
  if pairs:
@@ -1206,6 +1789,48 @@ def locate_signal_model(name, path=None, pairs=False):
1206
1789
  return match
1207
1790
 
1208
1791
  def locate_pair_signal_model(name, path=None):
1792
+
1793
+ """
1794
+ Locate a pair signal detection model by name.
1795
+
1796
+ This function searches for a pair signal detection model in the default
1797
+ `celldetective` directory and optionally in an additional user-specified path.
1798
+
1799
+ Parameters
1800
+ ----------
1801
+ name : str
1802
+ The name of the pair signal detection model to locate.
1803
+ path : str, optional
1804
+ An additional directory path to search for the model. If provided, this directory
1805
+ is also scanned for matching models. Default is `None`.
1806
+
1807
+ Returns
1808
+ -------
1809
+ str or None
1810
+ The full path to the located model directory if found, or `None` if no matching
1811
+ model is located.
1812
+
1813
+ Notes
1814
+ -----
1815
+ - The function first searches in the default `celldetective/models/pair_signal_detection`
1816
+ directory.
1817
+ - If a `path` is specified, it is searched in addition to the default directory.
1818
+ - The function prints the search path and model name during execution.
1819
+
1820
+ Examples
1821
+ --------
1822
+ Locate a model in the default directory:
1823
+
1824
+ >>> locate_pair_signal_model("example_model")
1825
+ 'path/to/celldetective/models/pair_signal_detection/example_model/'
1826
+
1827
+ Include an additional search directory:
1828
+
1829
+ >>> locate_pair_signal_model("custom_model", path="/additional/models/")
1830
+ '/additional/models/custom_model/'
1831
+ """
1832
+
1833
+
1209
1834
  main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], "celldetective"])
1210
1835
  modelpath = os.sep.join([main_dir, "models", "pair_signal_detection", os.sep])
1211
1836
  print(f'Looking for {name} in {modelpath}')
@@ -1215,74 +1840,118 @@ def locate_pair_signal_model(name, path=None):
1215
1840
  path += os.sep
1216
1841
  models += glob(path + f'*{os.sep}')
1217
1842
 
1218
- def relabel_segmentation(labels, data, properties, column_labels={'track': "track", 'frame': 'frame', 'y': 'y', 'x': 'x', 'label': 'class_id'}, threads=1):
1843
+ def relabel_segmentation(labels, df, exclude_nans=True, column_labels={'track': "TRACK_ID", 'frame': 'FRAME', 'y': 'POSITION_Y', 'x': 'POSITION_X', 'label': 'class_id'}, threads=1):
1219
1844
 
1220
1845
  """
1846
+ Relabel the segmentation labels with the tracking IDs from the tracks.
1221
1847
 
1222
- Relabel the segmentation labels based on the provided tracking data and properties.
1848
+ The function reassigns the mask value for each cell with the associated `TRACK_ID`, if it exists
1849
+ in the trajectory table (`df`). If no track uses the cell mask, a new track with a single point
1850
+ is generated on the fly (max of `TRACK_ID` values + i, for i=0 to N such cells). It supports
1851
+ multithreaded processing for faster execution on large datasets.
1223
1852
 
1224
1853
  Parameters
1225
1854
  ----------
1226
- labels : ndarray
1227
- The original segmentation labels.
1228
- data : ndarray
1229
- The tracking data containing information about tracks, frames, y-coordinates, and x-coordinates.
1230
- properties : ndarray
1231
- The properties associated with the tracking data.
1855
+ labels : np.ndarray
1856
+ A (TYX) array where each frame contains a 2D segmentation mask. Each unique
1857
+ non-zero integer represents a labeled object.
1858
+ df : pandas.DataFrame
1859
+ A DataFrame containing tracking information with columns
1860
+ specified in `column_labels`. Must include at least frame, track ID, and object ID.
1861
+ exclude_nans : bool, optional
1862
+ Whether to exclude rows in `df` with NaN values in the column specified by
1863
+ `column_labels['label']`. Default is `True`.
1232
1864
  column_labels : dict, optional
1233
- A dictionary specifying the column labels for the tracking data. The default is {'track': "track",
1234
- 'frame': 'frame', 'y': 'y', 'x': 'x', 'label': 'class_id'}.
1865
+ A dictionary specifying the column names in `df`. Default is:
1866
+ - `'track'`: Track ID column name (`"TRACK_ID"`)
1867
+ - `'frame'`: Frame column name (`"FRAME"`)
1868
+ - `'y'`: Y-coordinate column name (`"POSITION_Y"`)
1869
+ - `'x'`: X-coordinate column name (`"POSITION_X"`)
1870
+ - `'label'`: Object ID column name (`"class_id"`)
1871
+ threads : int, optional
1872
+ Number of threads to use for multithreaded processing. Default is `1`.
1235
1873
 
1236
1874
  Returns
1237
1875
  -------
1238
- ndarray
1239
- The relabeled segmentation labels.
1876
+ np.ndarray
1877
+ A new (TYX) array with the same shape as `labels`, where objects are relabeled
1878
+ according to their tracking identity in `df`.
1240
1879
 
1241
1880
  Notes
1242
1881
  -----
1243
- This function relabels the segmentation labels based on the provided tracking data and properties.
1244
- It creates a DataFrame from the tracking data and properties, merges them based on the indices, and sorts them by track and frame.
1245
- Then, it iterates over unique frames in the DataFrame, retrieves the tracks and identities at each frame,
1246
- and updates the corresponding labels with the new track values.
1882
+ - For frames where labeled objects in `labels` do not match any entries in the `df`,
1883
+ new track IDs are generated for the unmatched labels.
1884
+ - The relabeling process maintains synchronization across threads using a shared
1885
+ counter for generating unique track IDs.
1247
1886
 
1248
1887
  Examples
1249
1888
  --------
1250
- >>> relabeled = relabel_segmentation(labels, data, properties, column_labels={'track': "track", 'frame': 'frame',
1251
- ... 'y': 'y', 'x': 'x', 'label': 'class_id'})
1252
- # Relabel the segmentation labels based on the provided tracking data and properties.
1253
-
1254
- """
1889
+ Relabel segmentation using tracking data:
1255
1890
 
1891
+ >>> labels = np.random.randint(0, 5, (10, 100, 100))
1892
+ >>> df = pd.DataFrame({
1893
+ ... "TRACK_ID": [1, 2, 1, 2],
1894
+ ... "FRAME": [0, 0, 1, 1],
1895
+ ... "class_id": [1, 2, 1, 2],
1896
+ ... })
1897
+ >>> new_labels = relabel_segmentation(labels, df, threads=2)
1898
+ Done.
1256
1899
 
1257
- n_threads = threads
1258
- if data.shape[1]==4:
1259
- df = pd.DataFrame(data,columns=[column_labels['track'],column_labels['frame'],column_labels['y'],column_labels['x']])
1260
- else:
1261
- df = pd.DataFrame(data,columns=[column_labels['track'],column_labels['frame'],'z', column_labels['y'],column_labels['x']])
1262
- df = df.drop(columns=['z'])
1900
+ Use custom column labels and exclude rows with NaNs:
1263
1901
 
1264
- df = df.merge(pd.DataFrame(properties),left_index=True, right_index=True)
1902
+ >>> column_labels = {
1903
+ ... 'track': "track_id",
1904
+ ... 'frame': "time",
1905
+ ... 'label': "object_id"
1906
+ ... }
1907
+ >>> new_labels = relabel_segmentation(labels, df, column_labels=column_labels, exclude_nans=True)
1908
+ Done.
1909
+ """
1910
+
1911
+ n_threads = threads
1265
1912
  df = df.sort_values(by=[column_labels['track'],column_labels['frame']])
1266
- df.loc[df['dummy'],column_labels['label']] = np.nan
1913
+ if exclude_nans:
1914
+ df = df.dropna(subset=column_labels['label'])
1267
1915
 
1268
1916
  new_labels = np.zeros_like(labels)
1917
+ shared_data = {"s": 0}
1269
1918
 
1270
1919
  def rewrite_labels(indices):
1271
1920
 
1921
+ all_track_ids = df[column_labels['track']].unique()
1922
+
1272
1923
  for t in tqdm(indices):
1273
1924
 
1274
1925
  f = int(t)
1275
1926
  cells = df.loc[df[column_labels['frame']] == f, [column_labels['track'], column_labels['label']]].to_numpy()
1276
- tracks_at_t = cells[:,0]
1277
- identities = cells[:,1]
1927
+ tracks_at_t = list(cells[:,0])
1928
+ identities = list(cells[:,1])
1929
+
1930
+ labels_at_t = list(np.unique(labels[f]))
1931
+ if 0 in labels_at_t:
1932
+ labels_at_t.remove(0)
1933
+ labels_not_in_df = [lbl for lbl in labels_at_t if lbl not in identities]
1934
+ for lbl in labels_not_in_df:
1935
+ with threading.Lock(): # Synchronize access to `shared_data["s"]`
1936
+ track_id = max(all_track_ids) + shared_data["s"]
1937
+ shared_data["s"] += 1
1938
+ tracks_at_t.append(track_id)
1939
+ identities.append(lbl)
1278
1940
 
1279
1941
  # exclude NaN
1942
+ tracks_at_t = np.array(tracks_at_t)
1943
+ identities = np.array(identities)
1944
+
1280
1945
  tracks_at_t = tracks_at_t[identities == identities]
1281
1946
  identities = identities[identities == identities]
1282
1947
 
1283
1948
  for k in range(len(identities)):
1949
+
1950
+ # need routine to check values from labels not in class_id of this frame and add new track id
1951
+
1284
1952
  loc_i, loc_j = np.where(labels[f] == identities[k])
1285
- new_labels[f, loc_i, loc_j] = round(tracks_at_t[k])
1953
+ track_id = tracks_at_t[k]
1954
+ new_labels[f, loc_i, loc_j] = round(track_id)
1286
1955
 
1287
1956
  # Multithreading
1288
1957
  indices = list(df[column_labels['frame']].unique())
@@ -1296,90 +1965,308 @@ def relabel_segmentation(labels, data, properties, column_labels={'track': "trac
1296
1965
  return new_labels
1297
1966
 
1298
1967
 
1299
- def control_tracking_btrack(position, prefix="Aligned", population="target", relabel=True, flush_memory=True, threads=1):
1968
+ def control_tracks(position, prefix="Aligned", population="target", relabel=True, flush_memory=True, threads=1):
1300
1969
 
1301
1970
  """
1302
- Load the necessary data for visualization of bTrack trajectories in napari.
1971
+ Controls the tracking of cells or objects within a given position by locating the relevant image stack and label data,
1972
+ and then visualizing and managing the tracks in the Napari viewer.
1303
1973
 
1304
1974
  Parameters
1305
1975
  ----------
1306
1976
  position : str
1307
- The path to the position directory.
1308
- prefix : str, optional
1309
- The prefix used to identify the movie file. The default is "Aligned".
1310
- population : str, optional
1311
- The population type to load, either "target" or "effector". The default is "target".
1977
+ The path to the directory containing the position's data. The function will ensure the path uses forward slashes.
1978
+
1979
+ prefix : str, optional, default="Aligned"
1980
+ The prefix of the file names for the image stack and labels. This parameter helps locate the relevant data files.
1981
+
1982
+ population : str, optional, default="target"
1983
+ The population to be tracked, typically either "target" or "effectors". This is used to identify the group of interest for tracking.
1984
+
1985
+ relabel : bool, optional, default=True
1986
+ If True, will relabel the tracks, potentially assigning new track IDs to the detected objects.
1987
+
1988
+ flush_memory : bool, optional, default=True
1989
+ If True, will flush memory after processing to free up resources.
1990
+
1991
+ threads : int, optional, default=1
1992
+ The number of threads to use for processing. This can speed up the task in multi-threaded environments.
1312
1993
 
1313
1994
  Returns
1314
1995
  -------
1315
1996
  None
1316
- This function displays the data in Napari for visualization and analysis.
1317
-
1318
- Examples
1319
- --------
1320
- >>> control_tracking_btrack("path/to/position", population="target")
1321
- # Executes napari for visualization of target trajectories.
1997
+ The function performs visualization and management of tracks in the Napari viewer. It does not return any value.
1322
1998
 
1999
+ Notes
2000
+ -----
2001
+ - This function assumes that the necessary data for tracking (stack and labels) are located in the specified position directory.
2002
+ - The `locate_stack_and_labels` function is used to retrieve the image stack and labels from the specified directory.
2003
+ - The tracks are visualized using the `view_tracks_in_napari` function, which handles the display in the Napari viewer.
2004
+ - The function can be used for tracking biological entities (e.g., cells) and their movement across time frames in an image stack.
2005
+
2006
+ Example
2007
+ -------
2008
+ >>> control_tracks("/path/to/data/position_1", prefix="Aligned", population="target", relabel=True, flush_memory=True, threads=4)
1323
2009
  """
2010
+
2011
+ if not position.endswith(os.sep):
2012
+ position += os.sep
1324
2013
 
1325
- data, properties, graph, labels, stack = load_napari_data(position, prefix=prefix, population=population)
1326
- view_on_napari_btrack(data, properties, graph, labels=labels, stack=stack, relabel=relabel,
2014
+ position = position.replace('\\','/')
2015
+ stack, labels = locate_stack_and_labels(position, prefix=prefix, population=population)
2016
+
2017
+ view_tracks_in_napari(position, population, labels=labels, stack=stack, relabel=relabel,
1327
2018
  flush_memory=flush_memory, threads=threads)
1328
2019
 
1329
2020
 
1330
- def view_on_napari_btrack(data, properties, graph, stack=None, labels=None, relabel=True, flush_memory=True,
1331
- position=None, threads=1):
2021
+ def tracks_to_btrack(df, exclude_nans=False):
2022
+
1332
2023
  """
1333
-
1334
- Visualize btrack data, including stack, labels, points, and tracks, using the napari viewer.
2024
+ Converts a dataframe of tracked objects into the bTrack output format.
2025
+ The function prepares tracking data, properties, and an empty graph structure for further processing.
1335
2026
 
1336
2027
  Parameters
1337
2028
  ----------
1338
- data : ndarray
1339
- The btrack data containing information about tracks.
1340
- properties : ndarray
1341
- The properties associated with the btrack data.
1342
- graph : Graph
1343
- The btrack graph containing information about track connections.
1344
- stack : ndarray, optional
1345
- The stack of images to visualize. The default is None.
1346
- labels : ndarray, optional
1347
- The segmentation labels to visualize. The default is None.
1348
- relabel : bool, optional
1349
- Specify whether to relabel the segmentation labels using the provided data. The default is True.
2029
+ df : pandas.DataFrame
2030
+ A dataframe containing tracking information. The dataframe must have columns for `TRACK_ID`,
2031
+ `FRAME`, `POSITION_Y`, `POSITION_X`, and `class_id` (among others).
2032
+
2033
+ exclude_nans : bool, optional, default=False
2034
+ If True, rows with NaN values in the `class_id` column will be excluded from the dataset.
2035
+ If False, the dataframe will retain all rows, including those with NaN in `class_id`.
1350
2036
 
2037
+ Returns
2038
+ -------
2039
+ data : numpy.ndarray
2040
+ A 2D numpy array containing the tracking data with columns `[TRACK_ID, FRAME, z, POSITION_Y, POSITION_X]`.
2041
+ The `z` column is set to zero for all rows.
2042
+
2043
+ properties : dict
2044
+ A dictionary where keys are property names (e.g., 'FRAME', 'state', 'generation', etc.) and values are numpy arrays
2045
+ containing the corresponding values from the dataframe.
2046
+
2047
+ graph : dict
2048
+ An empty dictionary intended to store graph-related information for the tracking data. It can be extended
2049
+ later to represent relationships between different tracking objects.
2050
+
1351
2051
  Notes
1352
2052
  -----
1353
- This function visualizes btrack data using the napari viewer. It adds the stack, labels, points,
1354
- and tracks to the viewer for visualization. If `relabel` is True and labels are provided, it calls
1355
- the `relabel_segmentation` function to relabel the segmentation labels based on the provided data.
2053
+ - The function assumes that the dataframe contains specific columns: `TRACK_ID`, `FRAME`, `POSITION_Y`, `POSITION_X`,
2054
+ and `class_id`. These columns are used to construct the tracking data and properties.
2055
+ - The `z` coordinate is set to 0 for all tracks since the function does not process 3D data.
2056
+ - This function is useful for transforming tracking data into a format that can be used by tracking graph algorithms.
1356
2057
 
1357
- Examples
1358
- --------
1359
- >>> view_on_napari_btrack(data, properties, graph, stack=stack, labels=labels, relabel=True)
1360
- # Visualize btrack data, including stack, labels, points, and tracks, using the napari viewer.
2058
+ Example
2059
+ -------
2060
+ >>> data, properties, graph = tracks_to_btrack(df, exclude_nans=True)
2061
+ """
2062
+
2063
+ graph = {}
2064
+ if exclude_nans:
2065
+ df.dropna(subset='class_id',inplace=True)
2066
+
2067
+ df["z"] = 0.
2068
+ data = df[["TRACK_ID","FRAME","z","POSITION_Y","POSITION_X"]].to_numpy()
2069
+
2070
+ df['dummy'] = False
2071
+ prop_cols = ['FRAME','state','generation','root','parent','dummy','class_id']
2072
+ properties = {}
2073
+ for col in prop_cols:
2074
+ properties.update({col: df[col].to_numpy()})
2075
+
2076
+ return data, properties, graph
2077
+
2078
+ def tracks_to_napari(df, exclude_nans=False):
2079
+
2080
+ data, properties, graph = tracks_to_btrack(df, exclude_nans=exclude_nans)
2081
+ vertices = data[:, [1,-2,-1]]
2082
+ if data.shape[1]==4:
2083
+ tracks = data
2084
+ else:
2085
+ tracks = data[:,[0,1,3,4]]
2086
+ return vertices, tracks, properties, graph
1361
2087
 
2088
+
2089
+ def view_tracks_in_napari(position, population, stack=None, labels=None, relabel=True, flush_memory=True, threads=1):
2090
+
2091
+ """
2092
+ Updated
1362
2093
  """
1363
2094
 
2095
+ df, df_path = get_position_table(position, population=population, return_path=True)
2096
+ if df is None:
2097
+ print('Please compute trajectories first... Abort...')
2098
+ return None
2099
+ shared_data = {"df": df, "path": df_path, "position": position, "population": population, 'selected_frame': None}
2100
+
1364
2101
  if (labels is not None) * relabel:
1365
2102
  print('Replacing the cell mask labels with the track ID...')
1366
- labels = relabel_segmentation(labels, data, properties, threads=threads)
2103
+ labels = relabel_segmentation(labels, df, exclude_nans=True, threads=threads)
1367
2104
 
1368
- vertices = data[:, [1,-2,-1]]
2105
+ vertices, tracks, properties, graph = tracks_to_napari(df, exclude_nans=True)
1369
2106
 
1370
2107
  viewer = napari.Viewer()
1371
2108
  if stack is not None:
1372
2109
  viewer.add_image(stack, channel_axis=-1, colormap=["gray"] * stack.shape[-1])
1373
2110
  if labels is not None:
1374
- viewer.add_labels(labels.astype(int), name='segmentation', opacity=0.4)
2111
+ labels_layer = viewer.add_labels(labels.astype(int), name='segmentation', opacity=0.4)
1375
2112
  viewer.add_points(vertices, size=4, name='points', opacity=0.3)
1376
- if data.shape[1]==4:
1377
- viewer.add_tracks(data, properties=properties, graph=graph, name='tracks')
1378
- else:
1379
- viewer.add_tracks(data[:,[0,1,3,4]], properties=properties, graph=graph, name='tracks')
2113
+ viewer.add_tracks(tracks, properties=properties, graph=graph, name='tracks')
2114
+
2115
+ def lock_controls(layer, widgets=(), locked=True):
2116
+ qctrl = viewer.window.qt_viewer.controls.widgets[layer]
2117
+ for wdg in widgets:
2118
+ try:
2119
+ getattr(qctrl, wdg).setEnabled(not locked)
2120
+ except:
2121
+ pass
2122
+
2123
+ label_widget_list = ['paint_button', 'erase_button', 'fill_button', 'polygon_button', 'transform_button']
2124
+ lock_controls(viewer.layers['segmentation'], label_widget_list)
2125
+
2126
+ point_widget_list = ['addition_button', 'delete_button', 'select_button', 'transform_button']
2127
+ lock_controls(viewer.layers['points'], point_widget_list)
2128
+
2129
+ track_widget_list = ['transform_button']
2130
+ lock_controls(viewer.layers['tracks'], track_widget_list)
2131
+
2132
+ # Initialize selected frame
2133
+ selected_frame = viewer.dims.current_step[0]
2134
+ shared_data['selected_frame'] = selected_frame
2135
+
2136
+ def export_modifications():
2137
+
2138
+ from celldetective.tracking import write_first_detection_class, clean_trajectories
2139
+ from celldetective.utils import velocity_per_track
2140
+
2141
+ df = shared_data['df']
2142
+ position = shared_data['position']
2143
+ population = shared_data['population']
2144
+ df = velocity_per_track(df, window_size=3, mode='bi')
2145
+ df = write_first_detection_class(df, img_shape=labels[0].shape)
2146
+
2147
+ experiment = extract_experiment_from_position(position)
2148
+ instruction_file = "/".join([experiment,"configs", f"tracking_instructions_{population}.json"])
2149
+ print(f"{instruction_file=}")
2150
+ if os.path.exists(instruction_file):
2151
+ print('Tracking configuration file found...')
2152
+ with open(instruction_file, 'r') as f:
2153
+ instructions = json.load(f)
2154
+ if 'post_processing_options' in instructions:
2155
+ post_processing_options = instructions['post_processing_options']
2156
+ print(f'Applying the following track postprocessing: {post_processing_options}...')
2157
+ df = clean_trajectories(df.copy(),**post_processing_options)
2158
+ unnamed_cols = [c for c in list(df.columns) if c.startswith('Unnamed')]
2159
+ df = df.drop(unnamed_cols, axis=1)
2160
+ print(f"{list(df.columns)=}")
2161
+ df.to_csv(shared_data['path'], index=False)
2162
+ print('Done...')
2163
+
2164
+ @magicgui(call_button='Export the modified\ntracks...')
2165
+ def export_table_widget():
2166
+ return export_modifications()
2167
+
2168
+ def label_changed(event):
2169
+
2170
+ value = viewer.layers['segmentation'].selected_label
2171
+ if value != 0:
2172
+ selected_frame = viewer.dims.current_step[0]
2173
+ shared_data['selected_frame'] = selected_frame
2174
+
2175
+
2176
+ viewer.layers['segmentation'].events.selected_label.connect(label_changed)
2177
+
2178
+ viewer.window.add_dock_widget(export_table_widget, area='right')
2179
+
2180
+ @labels_layer.mouse_double_click_callbacks.append
2181
+ def on_second_click_of_double_click(layer, event):
2182
+
2183
+ df = shared_data['df']
2184
+ position = shared_data['position']
2185
+ population = shared_data['population']
2186
+
2187
+ frame, x, y = event.position
2188
+ try:
2189
+ value_under = viewer.layers['segmentation'].data[int(frame), int(x), int(y)] #labels[0,int(y),int(x)]
2190
+ if value_under==0:
2191
+ return None
2192
+ except:
2193
+ print('Invalid mask value...')
2194
+ return None
2195
+
2196
+ target_track_id = viewer.layers['segmentation'].selected_label
2197
+
2198
+ msgBox = QMessageBox()
2199
+ msgBox.setIcon(QMessageBox.Question)
2200
+ msgBox.setText(f"Do you want to propagate track {target_track_id} to the cell under the mouse, track {value_under}?")
2201
+ msgBox.setWindowTitle("Info")
2202
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
2203
+ returnValue = msgBox.exec()
2204
+ if returnValue == QMessageBox.No:
2205
+ return None
2206
+ else:
2207
+
2208
+ if target_track_id not in df['TRACK_ID'].unique() and target_track_id in np.unique(viewer.layers['segmentation'].data[shared_data['selected_frame']]):
2209
+ # the selected cell in frame -1 is not in the table... we can add it to DataFrame
2210
+ current_labelm1 = viewer.layers['segmentation'].data[shared_data['selected_frame']]
2211
+ original_labelm1 = locate_labels(position, population=population, frames=shared_data['selected_frame'])
2212
+ original_labelm1[current_labelm1!=target_track_id] = 0
2213
+ props = regionprops_table(original_labelm1, intensity_image=None, properties=['centroid', 'label'])
2214
+ props = pd.DataFrame(props)
2215
+ new_cell = props[['centroid-1', 'centroid-0','label']].copy()
2216
+ new_cell.rename(columns={'centroid-1': 'POSITION_X', 'centroid-0': 'POSITION_Y', 'label': 'class_id'},inplace=True)
2217
+ new_cell['FRAME'] = shared_data['selected_frame']
2218
+ new_cell['TRACK_ID'] = target_track_id
2219
+ df = pd.concat([df, new_cell], ignore_index=True)
2220
+
2221
+ if value_under not in df['TRACK_ID'].unique():
2222
+ # the cell to add is not currently part of DataFrame, need to add measurement
2223
+
2224
+ current_label = viewer.layers['segmentation'].data[int(frame)]
2225
+ original_label = locate_labels(position, population=population, frames=int(frame))
2226
+
2227
+ new_datapoint = {'TRACK_ID': value_under, 'FRAME': frame, 'POSITION_X': np.nan, 'POSITION_Y': np.nan, 'class_id': np.nan}
2228
+
2229
+ original_label[current_label!=value_under] = 0
2230
+
2231
+ props = regionprops_table(original_label, intensity_image=None, properties=['centroid', 'label'])
2232
+ props = pd.DataFrame(props)
2233
+
2234
+ new_cell = props[['centroid-1', 'centroid-0','label']].copy()
2235
+ new_cell.rename(columns={'centroid-1': 'POSITION_X', 'centroid-0': 'POSITION_Y', 'label': 'class_id'},inplace=True)
2236
+ new_cell['FRAME'] = int(frame)
2237
+ new_cell['TRACK_ID'] = value_under
2238
+ df = pd.concat([df, new_cell], ignore_index=True)
2239
+
2240
+ relabel = np.amax(df['TRACK_ID'].unique()) + 1
2241
+ for f in viewer.layers['segmentation'].data[int(frame):]:
2242
+ if target_track_id!=0:
2243
+ f[np.where(f==target_track_id)] = relabel
2244
+ f[np.where(f==value_under)] = target_track_id
2245
+
2246
+ if target_track_id!=0:
2247
+ df.loc[(df['FRAME']>=frame)&(df['TRACK_ID']==target_track_id),'TRACK_ID'] = relabel
2248
+ df.loc[(df['FRAME']>=frame)&(df['TRACK_ID']==value_under),'TRACK_ID'] = target_track_id
2249
+ df = df.loc[~(df['TRACK_ID']==0),:]
2250
+ df = df.sort_values(by=['TRACK_ID','FRAME'])
2251
+
2252
+ vertices, tracks, properties, graph = tracks_to_napari(df, exclude_nans=True)
2253
+
2254
+ viewer.layers['tracks'].data = tracks
2255
+ viewer.layers['tracks'].properties = properties
2256
+ viewer.layers['tracks'].graph = graph
2257
+
2258
+ viewer.layers['points'].data = vertices
2259
+
2260
+ viewer.layers['segmentation'].refresh()
2261
+ viewer.layers['tracks'].refresh()
2262
+ viewer.layers['points'].refresh()
2263
+
2264
+ shared_data['df'] = df
2265
+
1380
2266
  viewer.show(block=True)
1381
2267
 
1382
2268
  if flush_memory:
2269
+
1383
2270
  # temporary fix for slight napari memory leak
1384
2271
  for i in range(10000):
1385
2272
  try:
@@ -1394,6 +2281,7 @@ def view_on_napari_btrack(data, properties, graph, stack=None, labels=None, rela
1394
2281
 
1395
2282
 
1396
2283
  def load_napari_data(position, prefix="Aligned", population="target", return_stack=True):
2284
+
1397
2285
  """
1398
2286
  Load the necessary data for visualization in napari.
1399
2287
 
@@ -1417,6 +2305,10 @@ def load_napari_data(position, prefix="Aligned", population="target", return_sta
1417
2305
  # Load the necessary data for visualization of target trajectories.
1418
2306
 
1419
2307
  """
2308
+
2309
+ if not position.endswith(os.sep):
2310
+ position += os.sep
2311
+
1420
2312
  position = position.replace('\\','/')
1421
2313
  if population.lower()=="target" or population.lower()=="targets":
1422
2314
  if os.path.exists(position+os.sep.join(['output','tables','napari_target_trajectories.npy'])):
@@ -1428,6 +2320,7 @@ def load_napari_data(position, prefix="Aligned", population="target", return_sta
1428
2320
  napari_data = np.load(position+os.sep.join(['output', 'tables', 'napari_effector_trajectories.npy']), allow_pickle=True)
1429
2321
  else:
1430
2322
  napari_data = None
2323
+
1431
2324
  if napari_data is not None:
1432
2325
  data = napari_data.item()['data']
1433
2326
  properties = napari_data.item()['properties']
@@ -1444,9 +2337,6 @@ def load_napari_data(position, prefix="Aligned", population="target", return_sta
1444
2337
  return data, properties, graph, labels, stack
1445
2338
 
1446
2339
 
1447
- from skimage.measure import label
1448
-
1449
-
1450
2340
  def auto_correct_masks(masks, bbox_factor = 1.75, min_area=9, fill_labels=False):
1451
2341
 
1452
2342
  """
@@ -1621,7 +2511,7 @@ def control_segmentation_napari(position, prefix='Aligned', population="target",
1621
2511
  squares = np.array(squares)
1622
2512
  squares = squares[test_in_frame]
1623
2513
  nbr_squares = len(squares)
1624
- print(f"Found {nbr_squares} ROIS")
2514
+ print(f"Found {nbr_squares} ROIs...")
1625
2515
  if nbr_squares > 0:
1626
2516
  # deactivate field of view mode
1627
2517
  fov_export = False
@@ -1710,6 +2600,19 @@ def control_segmentation_napari(position, prefix='Aligned', population="target",
1710
2600
  viewer.add_labels(labels.astype(int), name='segmentation', opacity=0.4)
1711
2601
  viewer.window.add_dock_widget(save_widget, area='right')
1712
2602
  viewer.window.add_dock_widget(export_widget, area='right')
2603
+
2604
+ def lock_controls(layer, widgets=(), locked=True):
2605
+ qctrl = viewer.window.qt_viewer.controls.widgets[layer]
2606
+ for wdg in widgets:
2607
+ try:
2608
+ getattr(qctrl, wdg).setEnabled(not locked)
2609
+ except:
2610
+ pass
2611
+
2612
+ label_widget_list = ['polygon_button', 'transform_button']
2613
+ lock_controls(viewer.layers['segmentation'], label_widget_list)
2614
+
2615
+
1713
2616
  viewer.show(block=True)
1714
2617
 
1715
2618
  if flush_memory: