celldetective 1.0.2.post1__py3-none-any.whl → 1.1.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 (63) hide show
  1. celldetective/__main__.py +7 -21
  2. celldetective/events.py +2 -44
  3. celldetective/extra_properties.py +62 -52
  4. celldetective/filters.py +4 -5
  5. celldetective/gui/__init__.py +1 -1
  6. celldetective/gui/analyze_block.py +37 -10
  7. celldetective/gui/btrack_options.py +24 -23
  8. celldetective/gui/classifier_widget.py +62 -19
  9. celldetective/gui/configure_new_exp.py +32 -35
  10. celldetective/gui/control_panel.py +120 -81
  11. celldetective/gui/gui_utils.py +674 -396
  12. celldetective/gui/json_readers.py +7 -6
  13. celldetective/gui/layouts.py +756 -0
  14. celldetective/gui/measurement_options.py +98 -513
  15. celldetective/gui/neighborhood_options.py +322 -270
  16. celldetective/gui/plot_measurements.py +1114 -0
  17. celldetective/gui/plot_signals_ui.py +21 -20
  18. celldetective/gui/process_block.py +449 -169
  19. celldetective/gui/retrain_segmentation_model_options.py +27 -26
  20. celldetective/gui/retrain_signal_model_options.py +25 -24
  21. celldetective/gui/seg_model_loader.py +31 -27
  22. celldetective/gui/signal_annotator.py +2326 -2295
  23. celldetective/gui/signal_annotator_options.py +18 -16
  24. celldetective/gui/styles.py +16 -1
  25. celldetective/gui/survival_ui.py +67 -39
  26. celldetective/gui/tableUI.py +337 -48
  27. celldetective/gui/thresholds_gui.py +75 -71
  28. celldetective/gui/viewers.py +743 -0
  29. celldetective/io.py +247 -27
  30. celldetective/measure.py +43 -263
  31. celldetective/models/segmentation_effectors/primNK_cfse/config_input.json +29 -0
  32. celldetective/models/segmentation_effectors/primNK_cfse/cp-cfse-transfer +0 -0
  33. celldetective/models/segmentation_effectors/primNK_cfse/training_instructions.json +37 -0
  34. celldetective/neighborhood.py +498 -27
  35. celldetective/preprocessing.py +1023 -0
  36. celldetective/scripts/analyze_signals.py +7 -0
  37. celldetective/scripts/measure_cells.py +12 -0
  38. celldetective/scripts/segment_cells.py +20 -4
  39. celldetective/scripts/track_cells.py +11 -0
  40. celldetective/scripts/train_segmentation_model.py +35 -34
  41. celldetective/segmentation.py +14 -9
  42. celldetective/signals.py +234 -329
  43. celldetective/tracking.py +2 -2
  44. celldetective/utils.py +602 -49
  45. celldetective-1.1.1.dist-info/METADATA +305 -0
  46. celldetective-1.1.1.dist-info/RECORD +84 -0
  47. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/top_level.txt +1 -0
  48. tests/__init__.py +0 -0
  49. tests/test_events.py +28 -0
  50. tests/test_filters.py +24 -0
  51. tests/test_io.py +70 -0
  52. tests/test_measure.py +141 -0
  53. tests/test_neighborhood.py +70 -0
  54. tests/test_preprocessing.py +37 -0
  55. tests/test_segmentation.py +93 -0
  56. tests/test_signals.py +135 -0
  57. tests/test_tracking.py +164 -0
  58. tests/test_utils.py +118 -0
  59. celldetective-1.0.2.post1.dist-info/METADATA +0 -221
  60. celldetective-1.0.2.post1.dist-info/RECORD +0 -66
  61. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/LICENSE +0 -0
  62. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/WHEEL +0 -0
  63. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.1.dist-info}/entry_points.txt +0 -0
celldetective/io.py CHANGED
@@ -19,7 +19,12 @@ from celldetective.utils import ConfigSectionMap, extract_experiment_channels, _
19
19
  import json
20
20
  import threading
21
21
  from skimage.measure import regionprops_table
22
-
22
+ 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
23
+ import matplotlib.pyplot as plt
24
+ from celldetective.filters import std_filter, median_filter, gauss_filter
25
+ from stardist import fill_label_holes
26
+ from celldetective.utils import interpolate_nan
27
+ from scipy.interpolate import griddata
23
28
 
24
29
  def get_experiment_wells(experiment):
25
30
 
@@ -131,15 +136,58 @@ def get_experiment_pharmaceutical_agents(experiment, dtype=str):
131
136
  return np.array([dtype(c) for c in pharmaceutical_agents])
132
137
 
133
138
 
134
- def _interpret_wells_and_positions(experiment, well_option, position_option):
139
+ def interpret_wells_and_positions(experiment, well_option, position_option):
135
140
 
136
- wells = get_experiment_wells(experiment)
137
- nbr_of_wells = len(wells)
141
+ """
142
+ Interpret well and position options for a given experiment.
143
+
144
+ This function takes an experiment and well/position options to return the selected
145
+ wells and positions. It supports selection of all wells or specific wells/positions
146
+ as specified. The well numbering starts from 0 (i.e., Well 0 is W1 and so on).
147
+
148
+ Parameters
149
+ ----------
150
+ experiment : object
151
+ The experiment object containing well information.
152
+ well_option : str, int, or list of int
153
+ The well selection option:
154
+ - '*' : Select all wells.
155
+ - int : Select a specific well by its index.
156
+ - list of int : Select multiple wells by their indices.
157
+ position_option : str, int, or list of int
158
+ The position selection option:
159
+ - '*' : Select all positions (returns None).
160
+ - int : Select a specific position by its index.
161
+ - list of int : Select multiple positions by their indices.
162
+
163
+ Returns
164
+ -------
165
+ well_indices : numpy.ndarray or list of int
166
+ The indices of the selected wells.
167
+ position_indices : numpy.ndarray or list of int or None
168
+ The indices of the selected positions. Returns None if all positions are selected.
169
+
170
+ Examples
171
+ --------
172
+ >>> experiment = ... # Some experiment object
173
+ >>> interpret_wells_and_positions(experiment, '*', '*')
174
+ (array([0, 1, 2, ..., n-1]), None)
175
+
176
+ >>> interpret_wells_and_positions(experiment, 2, '*')
177
+ ([2], None)
138
178
 
179
+ >>> interpret_wells_and_positions(experiment, [1, 3, 5], 2)
180
+ ([1, 3, 5], array([2]))
181
+
182
+ """
183
+
184
+ wells = get_experiment_wells(experiment)
185
+ nbr_of_wells = len(wells)
186
+
139
187
  if well_option=='*':
140
- well_indices = np.arange(len(wells))
141
- elif isinstance(well_option, int):
142
- well_indices = np.array([well_option], dtype=int)
188
+ well_indices = np.arange(nbr_of_wells)
189
+ elif isinstance(well_option, int) or isinstance(well_option, np.int_):
190
+ well_indices = [int(well_option)]
143
191
  elif isinstance(well_option, list):
144
192
  well_indices = well_option
145
193
 
@@ -149,11 +197,43 @@ def _interpret_wells_and_positions(experiment, well_option, position_option):
149
197
  position_indices = np.array([position_option], dtype=int)
150
198
  elif isinstance(position_option, list):
151
199
  position_indices = position_option
152
-
200
+
153
201
  return well_indices, position_indices
154
202
 
155
203
  def extract_well_name_and_number(well):
156
204
 
205
+ """
206
+ Extract the well name and number from a given well path.
207
+
208
+ This function takes a well path string, splits it by the OS-specific path separator,
209
+ and extracts the well name and number. The well name is the last component of the path,
210
+ and the well number is derived by removing the 'W' prefix and converting the remaining
211
+ part to an integer.
212
+
213
+ Parameters
214
+ ----------
215
+ well : str
216
+ The well path string, where the well name is the last component.
217
+
218
+ Returns
219
+ -------
220
+ well_name : str
221
+ The name of the well, extracted from the last component of the path.
222
+ well_number : int
223
+ The well number, obtained by stripping the 'W' prefix from the well name
224
+ and converting the remainder to an integer.
225
+
226
+ Examples
227
+ --------
228
+ >>> well_path = "path/to/W23"
229
+ >>> extract_well_name_and_number(well_path)
230
+ ('W23', 23)
231
+
232
+ >>> well_path = "another/path/W1"
233
+ >>> extract_well_name_and_number(well_path)
234
+ ('W1', 1)
235
+ """
236
+
157
237
  split_well_path = well.split(os.sep)
158
238
  split_well_path = list(filter(None, split_well_path))
159
239
  well_name = split_well_path[-1]
@@ -163,6 +243,34 @@ def extract_well_name_and_number(well):
163
243
 
164
244
  def extract_position_name(pos):
165
245
 
246
+ """
247
+ Extract the position name from a given position path.
248
+
249
+ This function takes a position path string, splits it by the OS-specific path separator,
250
+ filters out any empty components, and extracts the position name, which is the last
251
+ component of the path.
252
+
253
+ Parameters
254
+ ----------
255
+ pos : str
256
+ The position path string, where the position name is the last component.
257
+
258
+ Returns
259
+ -------
260
+ pos_name : str
261
+ The name of the position, extracted from the last component of the path.
262
+
263
+ Examples
264
+ --------
265
+ >>> pos_path = "path/to/position1"
266
+ >>> extract_position_name(pos_path)
267
+ 'position1'
268
+
269
+ >>> pos_path = "another/path/positionA"
270
+ >>> extract_position_name(pos_path)
271
+ 'positionA'
272
+ """
273
+
166
274
  split_pos_path = pos.split(os.sep)
167
275
  split_pos_path = list(filter(None, split_pos_path))
168
276
  pos_name = split_pos_path[-1]
@@ -222,18 +330,109 @@ def get_position_table(pos, population, return_path=False):
222
330
  else:
223
331
  return df_pos
224
332
 
333
+ def get_position_pickle(pos, population, return_path=False):
334
+
335
+ """
336
+ Retrieves the data table for a specified population at a given position, optionally returning the table's file path.
337
+
338
+ This function locates and loads a CSV data table associated with a specific population (e.g., 'targets', 'cells')
339
+ from a specified position directory. The position directory should contain an 'output/tables' subdirectory where
340
+ the CSV file named 'trajectories_{population}.csv' is expected to be found. If the file exists, it is loaded into
341
+ a pandas DataFrame; otherwise, None is returned.
342
+
343
+ Parameters
344
+ ----------
345
+ pos : str
346
+ The path to the position directory from which to load the data table.
347
+ population : str
348
+ The name of the population for which the data table is to be retrieved. This name is used to construct the
349
+ file name of the CSV file to be loaded.
350
+ return_path : bool, optional
351
+ If True, returns a tuple containing the loaded data table (or None) and the path to the CSV file. If False,
352
+ only the loaded data table (or None) is returned (default is False).
353
+
354
+ Returns
355
+ -------
356
+ pandas.DataFrame or None, or (pandas.DataFrame or None, str)
357
+ If return_path is False, returns the loaded data table as a pandas DataFrame, or None if the table file does
358
+ not exist. If return_path is True, returns a tuple where the first element is the data table (or None) and the
359
+ second element is the path to the CSV file.
360
+
361
+ Examples
362
+ --------
363
+ >>> df_pos = get_position_table('/path/to/position', 'targets')
364
+ # This will load the 'trajectories_targets.csv' table from the specified position directory into a pandas DataFrame.
365
+
366
+ >>> df_pos, table_path = get_position_table('/path/to/position', 'targets', return_path=True)
367
+ # This will load the 'trajectories_targets.csv' table and also return the path to the CSV file.
368
+
369
+ """
370
+
371
+ if not pos.endswith(os.sep):
372
+ table = os.sep.join([pos,'output','tables',f'trajectories_{population}.pkl'])
373
+ else:
374
+ table = pos + os.sep.join(['output','tables',f'trajectories_{population}.pkl'])
375
+
376
+ if os.path.exists(table):
377
+ df_pos = np.load(table, allow_pickle=True)
378
+ else:
379
+ df_pos = None
380
+
381
+ if return_path:
382
+ return df_pos, table
383
+ else:
384
+ return df_pos
385
+
386
+
225
387
  def get_position_movie_path(pos, prefix=''):
226
388
 
389
+ """
390
+ Get the path of the movie file for a given position.
391
+
392
+ This function constructs the path to a movie file within a given position directory.
393
+ It searches for TIFF files that match the specified prefix. If multiple matching files
394
+ are found, the first one is returned.
395
+
396
+ Parameters
397
+ ----------
398
+ pos : str
399
+ The directory path for the position.
400
+ prefix : str, optional
401
+ The prefix to filter movie files. Defaults to an empty string.
402
+
403
+ Returns
404
+ -------
405
+ stack_path : str or None
406
+ The path to the first matching movie file, or None if no matching file is found.
407
+
408
+ Examples
409
+ --------
410
+ >>> pos_path = "path/to/position1"
411
+ >>> get_position_movie_path(pos_path, prefix='experiment_')
412
+ 'path/to/position1/movie/experiment_001.tif'
413
+
414
+ >>> pos_path = "another/path/positionA"
415
+ >>> get_position_movie_path(pos_path)
416
+ 'another/path/positionA/movie/001.tif'
417
+
418
+ >>> pos_path = "nonexistent/path"
419
+ >>> get_position_movie_path(pos_path)
420
+ None
421
+ """
422
+
423
+
227
424
  if not pos.endswith(os.sep):
228
425
  pos+=os.sep
229
426
  movies = glob(pos+os.sep.join(['movie',prefix+'*.tif']))
230
427
  if len(movies)>0:
231
428
  stack_path = movies[0]
232
429
  else:
233
- stack_path = np.nan
430
+ stack_path = None
234
431
 
235
432
  return stack_path
236
-
433
+
434
+
435
+
237
436
  def load_experiment_tables(experiment, population='targets', well_option='*',position_option='*', return_pos_info=False):
238
437
 
239
438
 
@@ -303,18 +502,19 @@ def load_experiment_tables(experiment, population='targets', well_option='*',pos
303
502
  pharmaceutical_agents = get_experiment_pharmaceutical_agents(experiment)
304
503
  well_labels = _extract_labels_from_config(config,len(wells))
305
504
 
306
- well_indices, position_indices = _interpret_wells_and_positions(experiment, well_option, position_option)
505
+ well_indices, position_indices = interpret_wells_and_positions(experiment, well_option, position_option)
307
506
 
308
507
  df = []
309
508
  df_pos_info = []
310
509
  real_well_index = 0
311
510
 
312
- for widx, well_path in enumerate(tqdm(wells[well_indices])):
511
+ for k, well_path in enumerate(tqdm(wells[well_indices])):
313
512
 
314
513
  any_table = False # assume no table
315
514
 
316
- well_index = widx
317
515
  well_name, well_number = extract_well_name_and_number(well_path)
516
+ widx = well_indices[k]
517
+
318
518
  well_alias = well_labels[widx]
319
519
 
320
520
  well_concentration = concentrations[widx]
@@ -322,7 +522,7 @@ def load_experiment_tables(experiment, population='targets', well_option='*',pos
322
522
  well_cell_type = cell_types[widx]
323
523
  well_pharmaceutical_agent = pharmaceutical_agents[widx]
324
524
 
325
- positions = np.array(natsorted(glob(well_path+'*'+os.sep)),dtype=str)
525
+ positions = get_positions_in_well(well_path)
326
526
  if position_indices is not None:
327
527
  try:
328
528
  positions = positions[position_indices]
@@ -413,6 +613,9 @@ def locate_stack(position, prefix='Aligned'):
413
613
 
414
614
  """
415
615
 
616
+ if not position.endswith(os.sep):
617
+ position+=os.sep
618
+
416
619
  stack_path = glob(position+os.sep.join(['movie', f'{prefix}*.tif']))
417
620
  assert len(stack_path)>0,f"No movie with prefix {prefix} found..."
418
621
  stack = imread(stack_path[0].replace('\\','/'))
@@ -457,7 +660,9 @@ def locate_labels(position, population='target'):
457
660
 
458
661
  """
459
662
 
460
-
663
+ if not position.endswith(os.sep):
664
+ position+=os.sep
665
+
461
666
  if population.lower()=="target" or population.lower()=="targets":
462
667
  label_path = natsorted(glob(position+os.sep.join(["labels_targets", "*.tif"])))
463
668
  elif population.lower()=="effector" or population.lower()=="effectors":
@@ -758,12 +963,16 @@ def get_signal_models_list(return_path=False):
758
963
  return available_models, modelpath
759
964
 
760
965
 
761
- def locate_signal_model(name):
966
+ def locate_signal_model(name, path=None):
762
967
 
763
968
  main_dir = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0],"celldetective"])
764
969
  modelpath = os.sep.join([main_dir, "models", "signal_detection", os.sep])
765
970
  print(f'Looking for {name} in {modelpath}')
766
971
  models = glob(modelpath+f'*{os.sep}')
972
+ if path is not None:
973
+ if not path.endswith(os.sep):
974
+ path += os.sep
975
+ models += glob(path+f'*{os.sep}')
767
976
 
768
977
  match=None
769
978
  for m in models:
@@ -1002,7 +1211,7 @@ def view_on_napari_btrack(data,properties,graph,stack=None,labels=None,relabel=T
1002
1211
  del labels
1003
1212
  gc.collect()
1004
1213
 
1005
- def load_napari_data(position, prefix="Aligned", population="target"):
1214
+ def load_napari_data(position, prefix="Aligned", population="target", return_stack=True):
1006
1215
 
1007
1216
  """
1008
1217
  Load the necessary data for visualization in napari.
@@ -1035,9 +1244,11 @@ def load_napari_data(position, prefix="Aligned", population="target"):
1035
1244
  data = napari_data.item()['data']
1036
1245
  properties = napari_data.item()['properties']
1037
1246
  graph = napari_data.item()['graph']
1038
-
1039
- stack,labels = locate_stack_and_labels(position, prefix=prefix, population=population)
1040
-
1247
+ if return_stack:
1248
+ stack,labels = locate_stack_and_labels(position, prefix=prefix, population=population)
1249
+ else:
1250
+ labels=locate_labels(position,population=population)
1251
+ stack = None
1041
1252
  return data,properties,graph,labels,stack
1042
1253
 
1043
1254
  from skimage.measure import label
@@ -1738,6 +1949,8 @@ def normalize_multichannel(multichannel_frame, percentiles=None,
1738
1949
 
1739
1950
  """
1740
1951
 
1952
+
1953
+
1741
1954
  mf = multichannel_frame.copy().astype(float)
1742
1955
  assert mf.ndim==3,f'Wrong shape for the multichannel frame: {mf.shape}.'
1743
1956
  if percentiles is None:
@@ -1749,12 +1962,13 @@ def normalize_multichannel(multichannel_frame, percentiles=None,
1749
1962
  values = [values]*mf.shape[-1]
1750
1963
  assert len(values)==mf.shape[-1],'Mismatch between the normalization values provided and the number of channels.'
1751
1964
 
1965
+ mf_new = []
1752
1966
  for c in range(mf.shape[-1]):
1753
1967
  if values is not None:
1754
1968
  v = values[c]
1755
1969
  else:
1756
1970
  v = None
1757
- mf[:,:,c] = normalize(mf[:,:,c].copy(),
1971
+ norm = normalize(mf[:,:,c].copy(),
1758
1972
  percentiles=percentiles[c],
1759
1973
  values=v,
1760
1974
  ignore_gray_value=ignore_gray_value,
@@ -1762,7 +1976,9 @@ def normalize_multichannel(multichannel_frame, percentiles=None,
1762
1976
  amplification=amplification,
1763
1977
  dtype=dtype,
1764
1978
  )
1765
- return mf
1979
+ mf_new.append(norm)
1980
+
1981
+ return np.moveaxis(mf_new,0,-1)
1766
1982
 
1767
1983
  def load_frames(img_nums, stack_path, scale=None, normalize_input=True, dtype=float, normalize_kwargs={"percentiles": (0.,99.99)}):
1768
1984
 
@@ -1818,7 +2034,7 @@ def load_frames(img_nums, stack_path, scale=None, normalize_input=True, dtype=fl
1818
2034
  """
1819
2035
 
1820
2036
  try:
1821
- frames = skio.imread(stack_path, img_num=img_nums, plugin="tifffile")
2037
+ frames = skio.imread(stack_path, key=img_nums, plugin="tifffile")
1822
2038
  except Exception as e:
1823
2039
  print(f'Error in loading the frame {img_nums} {e}. Please check that the experiment channel information is consistent with the movie being read.')
1824
2040
  return None
@@ -1827,13 +2043,17 @@ def load_frames(img_nums, stack_path, scale=None, normalize_input=True, dtype=fl
1827
2043
  # Systematically move channel axis to the end
1828
2044
  channel_axis = np.argmin(frames.shape)
1829
2045
  frames = np.moveaxis(frames, channel_axis, -1)
2046
+
1830
2047
  if frames.ndim==2:
1831
- frames = frames[:,:,np.newaxis]
2048
+ frames = frames[:,:,np.newaxis].astype(float)
2049
+
1832
2050
  if normalize_input:
1833
2051
  frames = normalize_multichannel(frames, **normalize_kwargs)
2052
+
1834
2053
  if scale is not None:
1835
- frames = zoom(frames, [scale,scale,1], order=3)
1836
-
2054
+ frames = [zoom(frames[:,:,c].copy(), [scale,scale], order=3, prefilter=False) for c in range(frames.shape[-1])]
2055
+ frames = np.moveaxis(frames,0,-1)
2056
+
1837
2057
  # add a fake pixel to prevent auto normalization errors on images that are uniform
1838
2058
  # to revisit
1839
2059
  for k in range(frames.shape[2]):
@@ -1955,7 +2175,7 @@ def get_positions_in_well(well):
1955
2175
  well = well[:-1]
1956
2176
 
1957
2177
  w_numeric = os.path.split(well)[-1].replace('W','')
1958
- positions = glob(os.sep.join([well,f'{w_numeric}*{os.sep}']))
2178
+ positions = natsorted(glob(os.sep.join([well,f'{w_numeric}*{os.sep}'])))
1959
2179
 
1960
2180
  return np.array(positions,dtype=str)
1961
2181