celldetective 1.1.0__py3-none-any.whl → 1.1.1.post1__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 (34) hide show
  1. celldetective/__main__.py +5 -19
  2. celldetective/extra_properties.py +63 -53
  3. celldetective/filters.py +39 -11
  4. celldetective/gui/classifier_widget.py +56 -7
  5. celldetective/gui/control_panel.py +5 -0
  6. celldetective/gui/layouts.py +3 -2
  7. celldetective/gui/measurement_options.py +13 -109
  8. celldetective/gui/plot_signals_ui.py +1 -0
  9. celldetective/gui/process_block.py +1 -1
  10. celldetective/gui/survival_ui.py +7 -1
  11. celldetective/gui/tableUI.py +294 -28
  12. celldetective/gui/thresholds_gui.py +51 -10
  13. celldetective/gui/viewers.py +169 -22
  14. celldetective/io.py +41 -17
  15. celldetective/measure.py +13 -238
  16. celldetective/models/segmentation_effectors/primNK_cfse/config_input.json +29 -0
  17. celldetective/models/segmentation_effectors/primNK_cfse/cp-cfse-transfer +0 -0
  18. celldetective/models/segmentation_effectors/primNK_cfse/training_instructions.json +37 -0
  19. celldetective/neighborhood.py +4 -1
  20. celldetective/preprocessing.py +483 -143
  21. celldetective/scripts/segment_cells.py +26 -7
  22. celldetective/scripts/train_segmentation_model.py +35 -34
  23. celldetective/segmentation.py +29 -20
  24. celldetective/signals.py +13 -231
  25. celldetective/tracking.py +2 -1
  26. celldetective/utils.py +440 -26
  27. {celldetective-1.1.0.dist-info → celldetective-1.1.1.post1.dist-info}/METADATA +1 -1
  28. {celldetective-1.1.0.dist-info → celldetective-1.1.1.post1.dist-info}/RECORD +34 -30
  29. {celldetective-1.1.0.dist-info → celldetective-1.1.1.post1.dist-info}/WHEEL +1 -1
  30. tests/test_preprocessing.py +37 -0
  31. tests/test_utils.py +48 -1
  32. {celldetective-1.1.0.dist-info → celldetective-1.1.1.post1.dist-info}/LICENSE +0 -0
  33. {celldetective-1.1.0.dist-info → celldetective-1.1.1.post1.dist-info}/entry_points.txt +0 -0
  34. {celldetective-1.1.0.dist-info → celldetective-1.1.1.post1.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,7 @@ import json
9
9
  from stardist.models import StarDist2D
10
10
  from cellpose.models import CellposeModel
11
11
  from celldetective.io import locate_segmentation_model, auto_load_number_of_frames, load_frames
12
- 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
12
+ from celldetective.utils import interpolate_nan, _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
13
13
  from pathlib import Path, PurePath
14
14
  from glob import glob
15
15
  from shutil import rmtree
@@ -22,6 +22,9 @@ from art import tprint
22
22
  from scipy.ndimage import zoom
23
23
  import threading
24
24
 
25
+ import matplotlib.pyplot as plt
26
+ import time
27
+
25
28
  tprint("Segment")
26
29
 
27
30
  parser = argparse.ArgumentParser(description="Segment a movie in position with the selected model",
@@ -118,6 +121,7 @@ if model_type=='cellpose':
118
121
  flow_threshold = input_config['flow_threshold']
119
122
 
120
123
  scale = _estimate_scale_factor(spatial_calibration, required_spatial_calibration)
124
+ print(f"Scale = {scale}...")
121
125
 
122
126
  nbr_channels = _extract_nbr_channels_from_config(config)
123
127
  print(f'Number of channels in the input movie: {nbr_channels}')
@@ -137,6 +141,7 @@ with open(pos+f'log_{mode}.json', 'a') as f:
137
141
 
138
142
  # Loop over all frames and segment
139
143
  def segment_index(indices):
144
+ global scale
140
145
 
141
146
  if model_type=='stardist':
142
147
  model = StarDist2D(None, name=modelname, basedir=Path(model_complete_path).parent)
@@ -153,18 +158,32 @@ def segment_index(indices):
153
158
  device = torch.device("cuda")
154
159
 
155
160
  model = CellposeModel(gpu=use_gpu, device=device, pretrained_model=model_complete_path+modelname, model_type=None, nchan=len(required_channels)) #diam_mean=30.0,
156
- model.diam_mean = 30.0
161
+ if scale is None:
162
+ scale_model = model.diam_mean / model.diam_labels
163
+ else:
164
+ scale_model = scale * model.diam_mean / model.diam_labels
165
+ print(f"Diam mean: {model.diam_mean}; Diam labels: {model.diam_labels}; Final rescaling: {scale_model}...")
157
166
  print(f'Cellpose model {modelname} successfully loaded.')
158
167
 
159
168
  for t in tqdm(indices,desc="frame"):
160
169
 
161
170
  # Load channels at time t
162
- f = load_frames(img_num_channels[:,t], file, scale=scale, normalize_input=False)
163
- f = normalize_per_channel([f], normalization_percentile_mode=normalization_percentile, normalization_values=normalization_values,
164
- normalization_clipping=normalization_clip)
165
- f = f[0]
171
+ values = []
172
+ percentiles = []
173
+ for k in range(len(normalization_percentile)):
174
+ if normalization_percentile[k]:
175
+ percentiles.append(normalization_values[k])
176
+ values.append(None)
177
+ else:
178
+ percentiles.append(None)
179
+ values.append(normalization_values[k])
180
+
181
+ f = load_frames(img_num_channels[:,t], file, scale=scale_model, normalize_input=True, normalize_kwargs={"percentiles": percentiles, 'values': values, 'clip': normalization_clip})
182
+ f = np.moveaxis([interpolate_nan(f[:,:,c].copy()) for c in range(f.shape[-1])],0,-1)
183
+
166
184
  if np.any(img_num_channels[:,t]==-1):
167
185
  f[:,:,np.where(img_num_channels[:,t]==-1)[0]] = 0.
186
+
168
187
 
169
188
  if model_type=="stardist":
170
189
  Y_pred, details = model.predict_instances(f, n_tiles=model._guess_n_tiles(f), show_tile_progress=False, verbose=False)
@@ -177,7 +196,7 @@ def segment_index(indices):
177
196
  Y_pred = Y_pred.astype(np.uint16)
178
197
 
179
198
  if scale is not None:
180
- Y_pred = zoom(Y_pred, [1./scale,1./scale],order=0)
199
+ Y_pred = zoom(Y_pred, [1./scale_model,1./scale_model],order=0)
181
200
 
182
201
  template = load_frames(0,file,scale=1,normalize_input=False)
183
202
  if Y_pred.shape != template.shape[:2]:
@@ -11,28 +11,15 @@ from tqdm import tqdm
11
11
  import numpy as np
12
12
  import random
13
13
 
14
- from celldetective.utils import load_image_dataset, normalize_per_channel, augmenter
14
+ from celldetective.utils import load_image_dataset, normalize_per_channel, augmenter, interpolate_nan
15
+ from celldetective.io import normalize_multichannel
15
16
  from stardist import fill_label_holes
16
17
  from art import tprint
17
18
  import matplotlib.pyplot as plt
18
19
 
19
- def interpolate_nan(array_like):
20
- array = array_like.copy()
21
-
22
- isnan_array = ~np.isnan(array)
23
-
24
- xp = isnan_array.ravel().nonzero()[0]
25
-
26
- fp = array[~np.isnan(array)]
27
- x = np.isnan(array).ravel().nonzero()[0]
28
-
29
- array[np.isnan(array)] = np.interp(x, xp, fp)
30
-
31
- return array
32
20
 
33
21
  tprint("Train")
34
22
 
35
-
36
23
  parser = argparse.ArgumentParser(description="Train a signal model from instructions.",
37
24
  formatter_class=argparse.ArgumentDefaultsHelpFormatter)
38
25
  parser.add_argument('-c',"--config", required=True,help="Training instructions")
@@ -78,25 +65,39 @@ X,Y = load_image_dataset(datasets, target_channels, train_spatial_calibration=sp
78
65
  print('Dataset loaded...')
79
66
 
80
67
  # Normalize images
81
- X = normalize_per_channel(X,
82
- normalization_percentile_mode=normalization_percentile,
83
- normalization_values=normalization_values,
84
- normalization_clipping=normalization_clip
85
- )
86
-
87
- for x in X:
88
- plt.imshow(x[:,:,0])
89
- plt.xlim(0,1004)
90
- plt.ylim(0,1002)
91
- plt.colorbar()
92
- plt.pause(2)
93
- plt.close()
94
- print(x.shape)
95
- interp = interpolate_nan(x)
96
- print(interp.shape)
97
- print(np.any(np.isnan(x).flatten()))
98
- print(np.any(np.isnan(interp).flatten()))
99
-
68
+ # X = normalize_per_channel(X,
69
+ # normalization_percentile_mode=normalization_percentile,
70
+ # normalization_values=normalization_values,
71
+ # normalization_clipping=normalization_clip
72
+ # )
73
+
74
+ values = []
75
+ percentiles = []
76
+ for k in range(len(normalization_percentile)):
77
+ if normalization_percentile[k]:
78
+ percentiles.append(normalization_values[k])
79
+ values.append(None)
80
+ else:
81
+ percentiles.append(None)
82
+ values.append(normalization_values[k])
83
+
84
+ X = [normalize_multichannel(x, **{"percentiles": percentiles, 'values': values, 'clip': normalization_clip}) for x in X]
85
+
86
+ for k in range(len(X)):
87
+ x = X[k].copy()
88
+ x_interp = np.moveaxis([interpolate_nan(x[:,:,c].copy()) for c in range(x.shape[-1])],0,-1)
89
+ X[k] = x_interp
90
+
91
+ # for x in X[:10]:
92
+ # plt.imshow(x[:,:,0])
93
+ # plt.colorbar()
94
+ # plt.pause(2)
95
+ # plt.close()
96
+
97
+ # plt.imshow(x[:,:,1])
98
+ # plt.colorbar()
99
+ # plt.pause(2)
100
+ # plt.close()
100
101
 
101
102
  Y = [fill_label_holes(y) for y in tqdm(Y)]
102
103
 
@@ -13,7 +13,8 @@ from cellpose.models import CellposeModel
13
13
  from skimage.transform import resize
14
14
  from celldetective.io import _view_on_napari, locate_labels, locate_stack, _view_on_napari
15
15
  from celldetective.filters import * #rework this to give a name
16
- from celldetective.utils import rename_intensity_column
16
+ from celldetective.utils import rename_intensity_column, mask_edges, estimate_unreliable_edge
17
+ from stardist import fill_label_holes
17
18
  import scipy.ndimage as ndi
18
19
  from skimage.segmentation import watershed
19
20
  from skimage.feature import peak_local_max
@@ -115,6 +116,10 @@ def segment(stack, model_name, channels=None, spatial_calibration=None, view_on_
115
116
 
116
117
  elif model_type=='cellpose':
117
118
  model = CellposeModel(gpu=use_gpu, pretrained_model=model_path+model_path.split('/')[-2], diam_mean=30.0)
119
+ if scale is None:
120
+ scale_model = model.diam_mean / model.diam_labels
121
+ else:
122
+ scale_model = scale * model.diam_mean / model.diam_labels
118
123
 
119
124
  labels = []
120
125
  if (time_flat_normalization)*normalize:
@@ -132,7 +137,7 @@ def segment(stack, model_name, channels=None, spatial_calibration=None, view_on_
132
137
  frame = normalize_multichannel(frame, values=normalization_values)
133
138
 
134
139
  if scale is not None:
135
- frame = ndi.zoom(frame, [scale,scale,1], order=3)
140
+ frame = [ndi.zoom(frame[:,:,c].copy(), [scale_model,scale_model], order=3, prefilter=False) for c in range(frame.shape[-1])]
136
141
 
137
142
  if model_type=="stardist":
138
143
 
@@ -141,16 +146,11 @@ def segment(stack, model_name, channels=None, spatial_calibration=None, view_on_
141
146
 
142
147
  elif model_type=="cellpose":
143
148
 
144
- if stack.ndim==3:
145
- channels_cp = [[0,0]]
146
- else:
147
- channels_cp = [[0,1]]
148
-
149
- Y_pred, _, _ = model.eval([frame], diameter = diameter, flow_threshold=flow_threshold, channels=channels_cp, normalize=normalize)
150
- Y_pred = Y_pred[0].astype(np.uint16)
149
+ Y_pred, _, _ = model.eval(frame, diameter = diameter, cellprob_threshold=cellprob_threshold, flow_threshold=flow_threshold, channels=None, normalize=False)
150
+ Y_pred = Y_pred.astype(np.uint16)
151
151
 
152
152
  if scale is not None:
153
- Y_pred = ndi.zoom(Y_pred, [1./scale,1./scale],order=0)
153
+ Y_pred = ndi.zoom(Y_pred, [1./scale_model,1./scale_model],order=0)
154
154
 
155
155
 
156
156
  if Y_pred.shape != stack[0].shape[:2]:
@@ -228,7 +228,7 @@ def segment_from_thresholds(stack, target_channel=0, thresholds=None, view_on_na
228
228
  return masks
229
229
 
230
230
  def segment_frame_from_thresholds(frame, target_channel=0, thresholds=None, equalize_reference=None,
231
- filters=None, marker_min_distance=30, marker_footprint_size=20, marker_footprint=None, feature_queries=None, channel_names=None):
231
+ filters=None, marker_min_distance=30, marker_footprint_size=20, marker_footprint=None, feature_queries=None, channel_names=None, do_watershed=True):
232
232
 
233
233
  """
234
234
  Segments objects within a single frame based on intensity thresholds and optional image processing steps.
@@ -273,9 +273,15 @@ def segment_frame_from_thresholds(frame, target_channel=0, thresholds=None, equa
273
273
  img = match_histograms(img, equalize_reference)
274
274
  img_mc = frame.copy()
275
275
  img = filter_image(img, filters=filters)
276
- binary_image = threshold_image(img, thresholds[0], thresholds[1])
277
- coords,distance = identify_markers_from_binary(binary_image, marker_min_distance, footprint_size=marker_footprint_size, footprint=marker_footprint, return_edt=True)
278
- instance_seg = apply_watershed(binary_image, coords, distance)
276
+ edge = estimate_unreliable_edge(filters)
277
+ binary_image = threshold_image(img, thresholds[0], thresholds[1], fill_holes=True, edge_exclusion=edge)
278
+
279
+ if do_watershed:
280
+ coords,distance = identify_markers_from_binary(binary_image, marker_min_distance, footprint_size=marker_footprint_size, footprint=marker_footprint, return_edt=True)
281
+ instance_seg = apply_watershed(binary_image, coords, distance)
282
+ else:
283
+ instance_seg, _ = ndi.label(binary_image.astype(int).copy())
284
+
279
285
  instance_seg = filter_on_property(instance_seg, intensity_image=img_mc, queries=feature_queries, channel_names=channel_names)
280
286
 
281
287
  return instance_seg
@@ -357,7 +363,7 @@ def filter_on_property(labels, intensity_image=None, queries=None, channel_names
357
363
  return labels
358
364
 
359
365
 
360
- def apply_watershed(binary_image, coords, distance):
366
+ def apply_watershed(binary_image, coords, distance, fill_holes=True):
361
367
 
362
368
  """
363
369
  Applies the watershed algorithm to segment objects in a binary image using given markers and distance map.
@@ -404,7 +410,8 @@ def apply_watershed(binary_image, coords, distance):
404
410
  mask[tuple(coords.T)] = True
405
411
  markers, _ = ndi.label(mask)
406
412
  labels = watershed(-distance, markers, mask=binary_image)
407
-
413
+ if fill_holes:
414
+ labels = fill_label_holes(labels)
408
415
  return labels
409
416
 
410
417
  def identify_markers_from_binary(binary_image, min_distance, footprint_size=20, footprint=None, return_edt=False):
@@ -457,7 +464,7 @@ def identify_markers_from_binary(binary_image, min_distance, footprint_size=20,
457
464
  return coords
458
465
 
459
466
 
460
- def threshold_image(img, min_threshold, max_threshold, foreground_value=255., fill_holes=True):
467
+ def threshold_image(img, min_threshold, max_threshold, foreground_value=255., fill_holes=True, edge_exclusion=None):
461
468
 
462
469
  """
463
470
 
@@ -496,10 +503,12 @@ def threshold_image(img, min_threshold, max_threshold, foreground_value=255., fi
496
503
 
497
504
  """
498
505
 
499
-
500
- binary = (img>=min_threshold)*(img<=max_threshold) * foreground_value
506
+ binary = np.zeros_like(img).astype(bool)
507
+ binary[img==img] = (img[img==img]>=min_threshold)*(img[img==img]<=max_threshold) * foreground_value
508
+ if isinstance(edge_exclusion, (int,np.int_)):
509
+ binary = mask_edges(binary, edge_exclusion)
501
510
  if fill_holes:
502
- binary = ndi.binary_fill_holes(binary)
511
+ binary = ndi.binary_fill_holes(binary.astype(int))
503
512
  return binary
504
513
 
505
514
  def filter_image(img, filters=None):
celldetective/signals.py CHANGED
@@ -2258,231 +2258,6 @@ def train_signal_model(config):
2258
2258
  cmd = f'python "{script_path}" --config "{config}"'
2259
2259
  subprocess.call(cmd, shell=True)
2260
2260
 
2261
- def derivative(x, timeline, window, mode='bi'):
2262
-
2263
- """
2264
- Compute the derivative of a given array of values with respect to time using a specified numerical differentiation method.
2265
-
2266
- Parameters
2267
- ----------
2268
- x : array_like
2269
- The input array of values.
2270
- timeline : array_like
2271
- The array representing the time points corresponding to the input values.
2272
- window : int
2273
- The size of the window used for numerical differentiation. Must be a positive odd integer.
2274
- mode : {'bi', 'forward', 'backward'}, optional
2275
- The numerical differentiation method to be used:
2276
- - 'bi' (default): Bidirectional differentiation using a symmetric window.
2277
- - 'forward': Forward differentiation using a one-sided window.
2278
- - 'backward': Backward differentiation using a one-sided window.
2279
-
2280
- Returns
2281
- -------
2282
- dxdt : ndarray
2283
- The computed derivative values of the input array with respect to time.
2284
-
2285
- Raises
2286
- ------
2287
- AssertionError
2288
- If the window size is not an odd integer and mode is 'bi'.
2289
-
2290
- Notes
2291
- -----
2292
- - For 'bi' mode, the window size must be an odd number.
2293
- - For 'forward' mode, the derivative at the edge points may not be accurate due to the one-sided window.
2294
- - For 'backward' mode, the derivative at the first few points may not be accurate due to the one-sided window.
2295
-
2296
- Examples
2297
- --------
2298
- >>> import numpy as np
2299
- >>> x = np.array([1, 2, 4, 7, 11])
2300
- >>> timeline = np.array([0, 1, 2, 3, 4])
2301
- >>> window = 3
2302
- >>> derivative(x, timeline, window, mode='bi')
2303
- array([3., 3., 3.])
2304
-
2305
- >>> derivative(x, timeline, window, mode='forward')
2306
- array([1., 2., 3.])
2307
-
2308
- >>> derivative(x, timeline, window, mode='backward')
2309
- array([3., 3., 3., 3.])
2310
- """
2311
-
2312
- # modes = bi, forward, backward
2313
- dxdt = np.zeros(len(x))
2314
- dxdt[:] = np.nan
2315
-
2316
- if mode=='bi':
2317
- assert window%2==1,'Please set an odd window for the bidirectional mode'
2318
- lower_bound = window//2
2319
- upper_bound = len(x) - window//2 - 1
2320
- elif mode=='forward':
2321
- lower_bound = 0
2322
- upper_bound = len(x) - window
2323
- elif mode=='backward':
2324
- lower_bound = window
2325
- upper_bound = len(x)
2326
-
2327
- for t in range(lower_bound,upper_bound):
2328
- if mode=='bi':
2329
- dxdt[t] = (x[t+window//2+1] - x[t-window//2]) / (timeline[t+window//2+1] - timeline[t-window//2])
2330
- elif mode=='forward':
2331
- dxdt[t] = (x[t+window] - x[t]) / (timeline[t+window] - timeline[t])
2332
- elif mode=='backward':
2333
- dxdt[t] = (x[t] - x[t-window]) / (timeline[t] - timeline[t-window])
2334
- return dxdt
2335
-
2336
- def velocity(x,y,timeline,window,mode='bi'):
2337
-
2338
- """
2339
- Compute the velocity vector of a given 2D trajectory represented by arrays of x and y coordinates
2340
- with respect to time using a specified numerical differentiation method.
2341
-
2342
- Parameters
2343
- ----------
2344
- x : array_like
2345
- The array of x-coordinates of the trajectory.
2346
- y : array_like
2347
- The array of y-coordinates of the trajectory.
2348
- timeline : array_like
2349
- The array representing the time points corresponding to the x and y coordinates.
2350
- window : int
2351
- The size of the window used for numerical differentiation. Must be a positive odd integer.
2352
- mode : {'bi', 'forward', 'backward'}, optional
2353
- The numerical differentiation method to be used:
2354
- - 'bi' (default): Bidirectional differentiation using a symmetric window.
2355
- - 'forward': Forward differentiation using a one-sided window.
2356
- - 'backward': Backward differentiation using a one-sided window.
2357
-
2358
- Returns
2359
- -------
2360
- v : ndarray
2361
- The computed velocity vector of the 2D trajectory with respect to time.
2362
- The first column represents the x-component of velocity, and the second column represents the y-component.
2363
-
2364
- Raises
2365
- ------
2366
- AssertionError
2367
- If the window size is not an odd integer and mode is 'bi'.
2368
-
2369
- Notes
2370
- -----
2371
- - For 'bi' mode, the window size must be an odd number.
2372
- - For 'forward' mode, the velocity at the edge points may not be accurate due to the one-sided window.
2373
- - For 'backward' mode, the velocity at the first few points may not be accurate due to the one-sided window.
2374
-
2375
- Examples
2376
- --------
2377
- >>> import numpy as np
2378
- >>> x = np.array([1, 2, 4, 7, 11])
2379
- >>> y = np.array([0, 3, 5, 8, 10])
2380
- >>> timeline = np.array([0, 1, 2, 3, 4])
2381
- >>> window = 3
2382
- >>> velocity(x, y, timeline, window, mode='bi')
2383
- array([[3., 3.],
2384
- [3., 3.]])
2385
-
2386
- >>> velocity(x, y, timeline, window, mode='forward')
2387
- array([[2., 2.],
2388
- [3., 3.]])
2389
-
2390
- >>> velocity(x, y, timeline, window, mode='backward')
2391
- array([[3., 3.],
2392
- [3., 3.]])
2393
- """
2394
-
2395
- v = np.zeros((len(x),2))
2396
- v[:,:] = np.nan
2397
-
2398
- v[:,0] = derivative(x, timeline, window, mode=mode)
2399
- v[:,1] = derivative(y, timeline, window, mode=mode)
2400
-
2401
- return v
2402
-
2403
- def magnitude_velocity(v_matrix):
2404
-
2405
- """
2406
- Compute the magnitude of velocity vectors given a matrix representing 2D velocity vectors.
2407
-
2408
- Parameters
2409
- ----------
2410
- v_matrix : array_like
2411
- The matrix where each row represents a 2D velocity vector with the first column
2412
- being the x-component and the second column being the y-component.
2413
-
2414
- Returns
2415
- -------
2416
- magnitude : ndarray
2417
- The computed magnitudes of the input velocity vectors.
2418
-
2419
- Notes
2420
- -----
2421
- - If a velocity vector has NaN components, the corresponding magnitude will be NaN.
2422
- - The function handles NaN values in the input matrix gracefully.
2423
-
2424
- Examples
2425
- --------
2426
- >>> import numpy as np
2427
- >>> v_matrix = np.array([[3, 4],
2428
- ... [2, 2],
2429
- ... [3, 3]])
2430
- >>> magnitude_velocity(v_matrix)
2431
- array([5., 2.82842712, 4.24264069])
2432
-
2433
- >>> v_matrix_with_nan = np.array([[3, 4],
2434
- ... [np.nan, 2],
2435
- ... [3, np.nan]])
2436
- >>> magnitude_velocity(v_matrix_with_nan)
2437
- array([5., nan, nan])
2438
- """
2439
-
2440
- magnitude = np.zeros(len(v_matrix))
2441
- magnitude[:] = np.nan
2442
- for i in range(len(v_matrix)):
2443
- if v_matrix[i,0]==v_matrix[i,0]:
2444
- magnitude[i] = np.sqrt(v_matrix[i,0]**2 + v_matrix[i,1]**2)
2445
- return magnitude
2446
-
2447
- def orientation(v_matrix):
2448
-
2449
- """
2450
- Compute the orientation angles (in radians) of 2D velocity vectors given a matrix representing velocity vectors.
2451
-
2452
- Parameters
2453
- ----------
2454
- v_matrix : array_like
2455
- The matrix where each row represents a 2D velocity vector with the first column
2456
- being the x-component and the second column being the y-component.
2457
-
2458
- Returns
2459
- -------
2460
- orientation_array : ndarray
2461
- The computed orientation angles of the input velocity vectors in radians.
2462
- If a velocity vector has NaN components, the corresponding orientation angle will be NaN.
2463
-
2464
- Examples
2465
- --------
2466
- >>> import numpy as np
2467
- >>> v_matrix = np.array([[3, 4],
2468
- ... [2, 2],
2469
- ... [-3, -3]])
2470
- >>> orientation(v_matrix)
2471
- array([0.92729522, 0.78539816, -2.35619449])
2472
-
2473
- >>> v_matrix_with_nan = np.array([[3, 4],
2474
- ... [np.nan, 2],
2475
- ... [3, np.nan]])
2476
- >>> orientation(v_matrix_with_nan)
2477
- array([0.92729522, nan, nan])
2478
- """
2479
-
2480
- orientation_array = np.zeros(len(v_matrix))
2481
- for t in range(len(orientation_array)):
2482
- if v_matrix[t,0]==v_matrix[t,0]:
2483
- orientation_array[t] = np.arctan2(v_matrix[t,0],v_matrix[t,1])
2484
- return orientation_array
2485
-
2486
2261
  def T_MSD(x,y,dt):
2487
2262
 
2488
2263
  """
@@ -2859,14 +2634,14 @@ def columnwise_mean(matrix, min_nbr_values = 1):
2859
2634
 
2860
2635
  for k in range(matrix.shape[1]):
2861
2636
  values = matrix[:,k]
2862
- values = values[values!=0]
2637
+ values = values[values==values]
2863
2638
  if len(values[values==values])>min_nbr_values:
2864
2639
  mean_line[k] = np.nanmean(values)
2865
2640
  mean_line_std[k] = np.nanstd(values)
2866
2641
  return mean_line, mean_line_std
2867
2642
 
2868
2643
 
2869
- def mean_signal(df, signal_name, class_col, time_col=None, class_value=[0], return_matrix=False, forced_max_duration=None, min_nbr_values=2):
2644
+ def mean_signal(df, signal_name, class_col, time_col=None, class_value=[0], return_matrix=False, forced_max_duration=None, min_nbr_values=2,conflict_mode='mean'):
2870
2645
 
2871
2646
  """
2872
2647
  Calculate the mean and standard deviation of a specified signal for tracks of a given class in the input DataFrame.
@@ -2912,12 +2687,13 @@ def mean_signal(df, signal_name, class_col, time_col=None, class_value=[0], retu
2912
2687
  else:
2913
2688
  max_duration = forced_max_duration
2914
2689
  n_tracks = len(df.groupby(['position','TRACK_ID']))
2915
- signal_matrix = np.zeros((n_tracks,max_duration*2 + 1))
2690
+ signal_matrix = np.zeros((n_tracks,int(max_duration)*2 + 1))
2916
2691
  signal_matrix[:,:] = np.nan
2917
2692
 
2693
+ df = df.sort_values(by=['position','TRACK_ID','FRAME'])
2694
+
2918
2695
  trackid=0
2919
2696
  for track,track_group in df.loc[df[class_col].isin(class_value)].groupby(['position','TRACK_ID']):
2920
- track_group = track_group.sort_values(by='FRAME')
2921
2697
  cclass = track_group[class_col].to_numpy()[0]
2922
2698
  if cclass != 0:
2923
2699
  ref_time = 0
@@ -2926,8 +2702,14 @@ def mean_signal(df, signal_name, class_col, time_col=None, class_value=[0], retu
2926
2702
  ref_time = floor(track_group[time_col].to_numpy()[0])
2927
2703
  except:
2928
2704
  continue
2929
- signal = track_group[signal_name].to_numpy()
2930
- timeline = track_group['FRAME'].to_numpy().astype(int)
2705
+ if conflict_mode=='mean':
2706
+ signal = track_group.groupby('FRAME')[signal_name].mean().to_numpy()
2707
+ elif conflict_mode=='first':
2708
+ signal = track_group.groupby('FRAME')[signal_name].first().to_numpy()
2709
+ else:
2710
+ signal = track_group[signal_name].to_numpy()
2711
+
2712
+ timeline = track_group['FRAME'].unique().astype(int)
2931
2713
  timeline_shifted = timeline - ref_time + max_duration
2932
2714
  signal_matrix[trackid,timeline_shifted] = signal
2933
2715
  trackid+=1
celldetective/tracking.py CHANGED
@@ -7,7 +7,7 @@ from btrack.io.utils import localizations_to_objects
7
7
  from btrack import BayesianTracker
8
8
 
9
9
  from celldetective.measure import measure_features
10
- from celldetective.utils import rename_intensity_column
10
+ from celldetective.utils import rename_intensity_column, velocity_per_track
11
11
  from celldetective.io import view_on_napari_btrack, interpret_tracking_configuration
12
12
 
13
13
  from btrack.datasets import cell_config
@@ -150,6 +150,7 @@ def track(labels, configuration=None, stack=None, spatial_calibration=1, feature
150
150
  df[columns] = df_temp
151
151
 
152
152
  df = df.sort_values(by=[column_labels['track'],column_labels['time']])
153
+ df = velocity_per_track(df, window_size=3, mode='bi')
153
154
 
154
155
  if channel_names is not None:
155
156
  df = rename_intensity_column(df, channel_names)