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.
- celldetective/__main__.py +5 -19
- celldetective/extra_properties.py +63 -53
- celldetective/filters.py +39 -11
- celldetective/gui/classifier_widget.py +56 -7
- celldetective/gui/control_panel.py +5 -0
- celldetective/gui/layouts.py +3 -2
- celldetective/gui/measurement_options.py +13 -109
- celldetective/gui/plot_signals_ui.py +1 -0
- celldetective/gui/process_block.py +1 -1
- celldetective/gui/survival_ui.py +7 -1
- celldetective/gui/tableUI.py +294 -28
- celldetective/gui/thresholds_gui.py +51 -10
- celldetective/gui/viewers.py +169 -22
- celldetective/io.py +41 -17
- celldetective/measure.py +13 -238
- celldetective/models/segmentation_effectors/primNK_cfse/config_input.json +29 -0
- celldetective/models/segmentation_effectors/primNK_cfse/cp-cfse-transfer +0 -0
- celldetective/models/segmentation_effectors/primNK_cfse/training_instructions.json +37 -0
- celldetective/neighborhood.py +4 -1
- celldetective/preprocessing.py +483 -143
- celldetective/scripts/segment_cells.py +26 -7
- celldetective/scripts/train_segmentation_model.py +35 -34
- celldetective/segmentation.py +29 -20
- celldetective/signals.py +13 -231
- celldetective/tracking.py +2 -1
- celldetective/utils.py +440 -26
- {celldetective-1.1.0.dist-info → celldetective-1.1.1.post1.dist-info}/METADATA +1 -1
- {celldetective-1.1.0.dist-info → celldetective-1.1.1.post1.dist-info}/RECORD +34 -30
- {celldetective-1.1.0.dist-info → celldetective-1.1.1.post1.dist-info}/WHEEL +1 -1
- tests/test_preprocessing.py +37 -0
- tests/test_utils.py +48 -1
- {celldetective-1.1.0.dist-info → celldetective-1.1.1.post1.dist-info}/LICENSE +0 -0
- {celldetective-1.1.0.dist-info → celldetective-1.1.1.post1.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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./
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
celldetective/segmentation.py
CHANGED
|
@@ -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, [
|
|
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
|
-
|
|
145
|
-
|
|
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./
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
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
|
-
|
|
2930
|
-
|
|
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)
|