spacr 0.4.60__py3-none-any.whl → 0.5.0__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.
- spacr/__init__.py +2 -2
- spacr/core.py +0 -1
- spacr/gui.py +0 -1
- spacr/gui_utils.py +5 -2
- spacr/io.py +168 -178
- spacr/resources/MEDIAR/__pycache__/SetupDict.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/__pycache__/evaluate.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/__pycache__/generate_mapping.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/__pycache__/main.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/Baseline/__pycache__/Predictor.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/Baseline/__pycache__/Trainer.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/Baseline/__pycache__/__init__.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/Baseline/__pycache__/utils.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/MEDIAR/__pycache__/EnsemblePredictor.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/MEDIAR/__pycache__/Predictor.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/MEDIAR/__pycache__/Trainer.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/MEDIAR/__pycache__/__init__.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/MEDIAR/__pycache__/utils.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/__pycache__/BasePredictor.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/__pycache__/BaseTrainer.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/__pycache__/__init__.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/core/__pycache__/utils.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/__pycache__/__init__.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/__pycache__/measures.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/__pycache__/utils.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/data_utils/__pycache__/__init__.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/data_utils/__pycache__/datasetter.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/data_utils/__pycache__/transforms.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/data_utils/__pycache__/utils.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/data_utils/custom/__pycache__/CellAware.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/data_utils/custom/__pycache__/LoadImage.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/data_utils/custom/__pycache__/NormalizeImage.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/data_utils/custom/__pycache__/__init__.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/models/__pycache__/MEDIARFormer.cpython-39.pyc +0 -0
- spacr/resources/MEDIAR/train_tools/models/__pycache__/__init__.cpython-39.pyc +0 -0
- spacr/settings.py +26 -8
- spacr/submodules.py +10 -13
- spacr/timelapse.py +172 -4
- spacr/utils.py +26 -41
- {spacr-0.4.60.dist-info → spacr-0.5.0.dist-info}/METADATA +4 -2
- {spacr-0.4.60.dist-info → spacr-0.5.0.dist-info}/RECORD +46 -17
- spacr/stats.py +0 -221
- /spacr/{cellpose.py → spacr_cellpose.py} +0 -0
- {spacr-0.4.60.dist-info → spacr-0.5.0.dist-info}/LICENSE +0 -0
- {spacr-0.4.60.dist-info → spacr-0.5.0.dist-info}/WHEEL +0 -0
- {spacr-0.4.60.dist-info → spacr-0.5.0.dist-info}/entry_points.txt +0 -0
- {spacr-0.4.60.dist-info → spacr-0.5.0.dist-info}/top_level.txt +0 -0
spacr/settings.py
CHANGED
@@ -95,14 +95,12 @@ def set_default_settings_preprocess_generate_masks(settings={}):
|
|
95
95
|
|
96
96
|
# Misc settings
|
97
97
|
settings.setdefault('all_to_mip', False)
|
98
|
-
settings.setdefault('pick_slice', False)
|
99
|
-
settings.setdefault('skip_mode', '01')
|
100
98
|
settings.setdefault('upscale', False)
|
101
99
|
settings.setdefault('upscale_factor', 2.0)
|
102
100
|
settings.setdefault('adjust_cells', False)
|
103
101
|
return settings
|
104
102
|
|
105
|
-
def
|
103
|
+
def set_default_settings_preprocess_img_data_v1(settings):
|
106
104
|
|
107
105
|
metadata_type = settings.setdefault('metadata_type', 'cellvoyager')
|
108
106
|
custom_regex = settings.setdefault('custom_regex', None)
|
@@ -127,6 +125,27 @@ def set_default_settings_preprocess_img_data(settings):
|
|
127
125
|
|
128
126
|
return settings, metadata_type, custom_regex, nr, plot, batch_size, timelapse, lower_percentile, randomize, all_to_mip, pick_slice, skip_mode, cmap, figuresize, normalize, save_dtype, test_mode, test_images, random_test
|
129
127
|
|
128
|
+
def set_default_settings_preprocess_img_data(settings):
|
129
|
+
|
130
|
+
settings.setdefault('metadata_type', 'cellvoyager')
|
131
|
+
settings.setdefault('custom_regex', None)
|
132
|
+
settings.setdefault('nr', 1)
|
133
|
+
settings.setdefault('plot', True)
|
134
|
+
settings.setdefault('batch_size', 50)
|
135
|
+
settings.setdefault('timelapse', False)
|
136
|
+
settings.setdefault('lower_percentile', 2)
|
137
|
+
settings.setdefault('randomize', True)
|
138
|
+
settings.setdefault('all_to_mip', False)
|
139
|
+
settings.setdefault('cmap', 'inferno')
|
140
|
+
settings.setdefault('figuresize', 10)
|
141
|
+
settings.setdefault('normalize', True)
|
142
|
+
settings.setdefault('save_dtype', 'uint16')
|
143
|
+
settings.setdefault('test_mode', False)
|
144
|
+
settings.setdefault('test_images', 10)
|
145
|
+
settings.setdefault('random_test', True)
|
146
|
+
settings.setdefault('fps', 2)
|
147
|
+
return settings
|
148
|
+
|
130
149
|
def _get_object_settings(object_type, settings):
|
131
150
|
|
132
151
|
from .utils import _get_diam
|
@@ -278,7 +297,7 @@ def get_measure_crop_settings(settings={}):
|
|
278
297
|
|
279
298
|
# Timelapsed settings
|
280
299
|
settings.setdefault('timelapse', False)
|
281
|
-
settings.setdefault('timelapse_objects', 'cell')
|
300
|
+
settings.setdefault('timelapse_objects', ['cell'])
|
282
301
|
|
283
302
|
# Operational settings
|
284
303
|
settings.setdefault('plot',False)
|
@@ -715,7 +734,7 @@ expected_types = {
|
|
715
734
|
#"timelapse_frame_limits": (list, type(None)), # This can be a list of lists
|
716
735
|
"timelapse_remove_transient": bool,
|
717
736
|
"timelapse_mode": str,
|
718
|
-
"timelapse_objects": list,
|
737
|
+
"timelapse_objects": (list, type(None)),
|
719
738
|
"fps": int,
|
720
739
|
"remove_background": bool,
|
721
740
|
"lower_percentile": (int, float),
|
@@ -1005,7 +1024,7 @@ categories = {"Paths":[ "src", "grna", "barcodes", "custom_model_path", "dataset
|
|
1005
1024
|
"Plot": ["split_axis_lims", "x_lim","log_x","log_y", "plot_control", "plot_nr", "examples_to_plot", "normalize_plots", "cmap", "figuresize", "plot_cluster_grids", "img_zoom", "row_limit", "color_by", "plot_images", "smooth_lines", "plot_points", "plot_outlines", "black_background", "plot_by_cluster", "heatmap_feature","grouping","min_max","cmap","save_figure"],
|
1006
1025
|
"Timelapse": ["timelapse", "fps", "timelapse_displacement", "timelapse_memory", "timelapse_frame_limits", "timelapse_remove_transient", "timelapse_mode", "timelapse_objects", "compartments"],
|
1007
1026
|
"Advanced": ["merge_edge_pathogen_cells", "test_images", "random_test", "test_nr", "test", "test_split", "normalize", "target_unique_count","threshold_multiplier", "threshold_method", "min_n","shuffle", "target_intensity_min", "cells_per_well", "nuclei_limit", "pathogen_limit", "background", "backgrounds", "schedule", "test_size","exclude","n_repeats","top_features", "model_type_ml", "model_type","minimum_cell_count","n_estimators","preprocess", "remove_background", "normalize", "lower_percentile", "merge_pathogens", "batch_size", "filter", "save", "masks", "verbose", "randomize", "n_jobs"],
|
1008
|
-
"Beta": ["all_to_mip", "
|
1027
|
+
"Beta": ["all_to_mip", "upscale", "upscale_factor", "consolidate"]
|
1009
1028
|
}
|
1010
1029
|
|
1011
1030
|
|
@@ -1033,7 +1052,7 @@ def check_settings(vars_dict, expected_types, q=None):
|
|
1033
1052
|
expected_type = expected_types.get(key, str)
|
1034
1053
|
|
1035
1054
|
try:
|
1036
|
-
if key in ["cell_plate_metadata", "timelapse_frame_limits", "png_size", "png_dims", "pathogen_plate_metadata", "treatment_plate_metadata", "class_metadata", "crop_mode"]:
|
1055
|
+
if key in ["cell_plate_metadata", "timelapse_frame_limits", "png_size", "png_dims", "pathogen_plate_metadata", "treatment_plate_metadata", "timelapse_objects", "class_metadata", "crop_mode"]:
|
1037
1056
|
if value is None:
|
1038
1057
|
parsed_value = None
|
1039
1058
|
else:
|
@@ -1274,7 +1293,6 @@ def generate_fields(variables, scrollable_frame):
|
|
1274
1293
|
"pc": "(str) - Positive control identifier.",
|
1275
1294
|
"pc_loc": "(str) - Location of the positive control in the images.",
|
1276
1295
|
"percentiles": "(list) - Percentiles to use for normalizing the images.",
|
1277
|
-
"pick_slice": "(bool) - Whether to pick a single slice from the z-stack images. If False, the maximum intensity projection will be used.",
|
1278
1296
|
"pin_memory": "(bool) - Whether to pin memory for the data loader.",
|
1279
1297
|
"plate": "(str) - Plate identifier for the experiment.",
|
1280
1298
|
"plate_dict": "(dict) - Dictionary of plate metadata.",
|
spacr/submodules.py
CHANGED
@@ -28,10 +28,7 @@ from skimage.measure import regionprops, label as sklabel
|
|
28
28
|
import matplotlib.pyplot as plt
|
29
29
|
from natsort import natsorted
|
30
30
|
|
31
|
-
import torch
|
32
31
|
from torch.utils.data import Dataset
|
33
|
-
from spacr.settings import get_train_cellpose_default_settings
|
34
|
-
from spacr.utils import save_settings, invert_image
|
35
32
|
|
36
33
|
class CellposeLazyDataset(Dataset):
|
37
34
|
def __init__(self, image_files, label_files, settings, randomize=True, augment=False):
|
@@ -91,8 +88,8 @@ class CellposeLazyDataset(Dataset):
|
|
91
88
|
|
92
89
|
def train_cellpose(settings):
|
93
90
|
|
94
|
-
from
|
95
|
-
from
|
91
|
+
from .settings import get_train_cellpose_default_settings
|
92
|
+
from .utils import save_settings
|
96
93
|
|
97
94
|
settings = get_train_cellpose_default_settings(settings)
|
98
95
|
img_src = os.path.join(settings['src'], 'train', 'images')
|
@@ -161,11 +158,11 @@ def train_cellpose(settings):
|
|
161
158
|
|
162
159
|
def test_cellpose_model(settings):
|
163
160
|
|
164
|
-
from
|
161
|
+
from .utils import save_settings, print_progress
|
165
162
|
from .settings import get_default_test_cellpose_model_settings
|
166
163
|
|
167
164
|
def plot_cellpose_resilts(i, j, results_dir, img, lbl, pred, flow):
|
168
|
-
from
|
165
|
+
from . plot import generate_mask_random_cmap
|
169
166
|
fig, axs = plt.subplots(1, 5, figsize=(16, 4), gridspec_kw={'wspace': 0.1, 'hspace': 0.1})
|
170
167
|
cmap_lbl = generate_mask_random_cmap(lbl)
|
171
168
|
cmap_pred = generate_mask_random_cmap(pred)
|
@@ -348,7 +345,7 @@ def test_cellpose_model(settings):
|
|
348
345
|
def apply_cellpose_model(settings):
|
349
346
|
|
350
347
|
from .settings import get_default_apply_cellpose_model_settings
|
351
|
-
from
|
348
|
+
from .utils import save_settings, print_progress
|
352
349
|
|
353
350
|
def plot_cellpose_result(i, j, results_dir, img, pred, flow):
|
354
351
|
|
@@ -466,7 +463,7 @@ def apply_cellpose_model(settings):
|
|
466
463
|
print("Saved object count and average area to summary.csv")
|
467
464
|
|
468
465
|
def plot_cellpose_batch(images, labels):
|
469
|
-
from
|
466
|
+
from .plot import generate_mask_random_cmap
|
470
467
|
|
471
468
|
cmap_lbl = generate_mask_random_cmap(labels)
|
472
469
|
batch_size = len(images)
|
@@ -481,8 +478,8 @@ def plot_cellpose_batch(images, labels):
|
|
481
478
|
plt.show()
|
482
479
|
|
483
480
|
def analyze_percent_positive(settings):
|
484
|
-
from
|
485
|
-
from
|
481
|
+
from .io import _read_and_merge_data
|
482
|
+
from .utils import save_settings
|
486
483
|
from .settings import default_settings_analyze_percent_positive
|
487
484
|
|
488
485
|
settings = default_settings_analyze_percent_positive(settings)
|
@@ -681,7 +678,7 @@ def analyze_recruitment(settings):
|
|
681
678
|
|
682
679
|
def analyze_plaques(settings):
|
683
680
|
|
684
|
-
from .
|
681
|
+
from .spacr_cellpose import identify_masks_finetune
|
685
682
|
from .settings import get_analyze_plaque_settings
|
686
683
|
from .utils import save_settings, download_models
|
687
684
|
from spacr import __file__ as spacr_path
|
@@ -1020,7 +1017,7 @@ def interperate_vision_model(settings={}):
|
|
1020
1017
|
else:
|
1021
1018
|
return None
|
1022
1019
|
|
1023
|
-
from
|
1020
|
+
from .plot import spacrGraph
|
1024
1021
|
|
1025
1022
|
df[name] = df['feature'].apply(lambda x: find_feature_class(x, feature_groups))
|
1026
1023
|
|
spacr/timelapse.py
CHANGED
@@ -7,9 +7,9 @@ from IPython.display import display
|
|
7
7
|
from IPython.display import Image as ipyimage
|
8
8
|
import trackpy as tp
|
9
9
|
from btrack import datasets as btrack_datasets
|
10
|
-
from skimage.measure import regionprops
|
10
|
+
from skimage.measure import regionprops, regionprops_table
|
11
11
|
from scipy.signal import find_peaks
|
12
|
-
from scipy.optimize import curve_fit
|
12
|
+
from scipy.optimize import curve_fit, linear_sum_assignment
|
13
13
|
from scipy.integrate import trapz
|
14
14
|
import matplotlib.pyplot as plt
|
15
15
|
|
@@ -255,7 +255,7 @@ def _relabel_masks_based_on_tracks(masks, tracks, mode='btrack'):
|
|
255
255
|
|
256
256
|
return relabeled_masks
|
257
257
|
|
258
|
-
def
|
258
|
+
def _prepare_for_tracking_v1(mask_array):
|
259
259
|
"""
|
260
260
|
Prepare the mask array for object tracking.
|
261
261
|
|
@@ -286,6 +286,87 @@ def _prepare_for_tracking(mask_array):
|
|
286
286
|
})
|
287
287
|
return pd.DataFrame(frames)
|
288
288
|
|
289
|
+
def _prepare_for_tracking(mask_array):
|
290
|
+
frames = []
|
291
|
+
for t, frame in enumerate(mask_array):
|
292
|
+
props = regionprops_table(
|
293
|
+
frame,
|
294
|
+
properties=('label', 'centroid-0', 'centroid-1', 'area',
|
295
|
+
'bbox-0', 'bbox-1', 'bbox-2', 'bbox-3',
|
296
|
+
'eccentricity')
|
297
|
+
)
|
298
|
+
df = pd.DataFrame(props)
|
299
|
+
df = df.rename(columns={
|
300
|
+
'centroid-0': 'y', 'centroid-1': 'x', 'area': 'mass',
|
301
|
+
'label': 'original_label'
|
302
|
+
})
|
303
|
+
df['frame'] = t
|
304
|
+
frames.append(df[['frame','y','x','mass','original_label',
|
305
|
+
'bbox-0','bbox-1','bbox-2','bbox-3','eccentricity']])
|
306
|
+
return pd.concat(frames, ignore_index=True)
|
307
|
+
|
308
|
+
|
309
|
+
def _track_by_iou(masks, iou_threshold=0.1):
|
310
|
+
"""
|
311
|
+
Build a track table by linking masks frame→frame via IoU.
|
312
|
+
Returns a DataFrame with columns [frame, original_label, track_id].
|
313
|
+
"""
|
314
|
+
n_frames = masks.shape[0]
|
315
|
+
# 1) initialize: every label in frame 0 starts its own track
|
316
|
+
labels0 = np.unique(masks[0])[1:]
|
317
|
+
next_track = 1
|
318
|
+
track_map = {} # (frame,label) -> track_id
|
319
|
+
for L in labels0:
|
320
|
+
track_map[(0, L)] = next_track
|
321
|
+
next_track += 1
|
322
|
+
|
323
|
+
# 2) iterate through frames
|
324
|
+
for t in range(1, n_frames):
|
325
|
+
prev, curr = masks[t-1], masks[t]
|
326
|
+
matches = link_by_iou(prev, curr, iou_threshold=iou_threshold)
|
327
|
+
used_curr = set()
|
328
|
+
# a) assign matched labels to existing tracks
|
329
|
+
for L_prev, L_curr in matches:
|
330
|
+
tid = track_map[(t-1, L_prev)]
|
331
|
+
track_map[(t, L_curr)] = tid
|
332
|
+
used_curr.add(L_curr)
|
333
|
+
# b) any label in curr not matched → new track
|
334
|
+
for L in np.unique(curr)[1:]:
|
335
|
+
if L not in used_curr:
|
336
|
+
track_map[(t, L)] = next_track
|
337
|
+
next_track += 1
|
338
|
+
|
339
|
+
# 3) flatten into DataFrame
|
340
|
+
records = []
|
341
|
+
for (frame, label), tid in track_map.items():
|
342
|
+
records.append({'frame': frame, 'original_label': label, 'track_id': tid})
|
343
|
+
return pd.DataFrame(records)
|
344
|
+
|
345
|
+
def link_by_iou(mask_prev, mask_next, iou_threshold=0.1):
|
346
|
+
# Get labels
|
347
|
+
labels_prev = np.unique(mask_prev)[1:]
|
348
|
+
labels_next = np.unique(mask_next)[1:]
|
349
|
+
# Precompute masks as boolean
|
350
|
+
bool_prev = {L: mask_prev==L for L in labels_prev}
|
351
|
+
bool_next = {L: mask_next==L for L in labels_next}
|
352
|
+
# Cost matrix = 1 - IoU
|
353
|
+
cost = np.ones((len(labels_prev), len(labels_next)), dtype=float)
|
354
|
+
for i, L1 in enumerate(labels_prev):
|
355
|
+
m1 = bool_prev[L1]
|
356
|
+
for j, L2 in enumerate(labels_next):
|
357
|
+
m2 = bool_next[L2]
|
358
|
+
inter = np.logical_and(m1, m2).sum()
|
359
|
+
union = np.logical_or(m1, m2).sum()
|
360
|
+
if union > 0:
|
361
|
+
cost[i, j] = 1 - inter/union
|
362
|
+
# Solve assignment
|
363
|
+
row_ind, col_ind = linear_sum_assignment(cost)
|
364
|
+
matches = []
|
365
|
+
for i, j in zip(row_ind, col_ind):
|
366
|
+
if cost[i,j] <= 1 - iou_threshold:
|
367
|
+
matches.append((labels_prev[i], labels_next[j]))
|
368
|
+
return matches
|
369
|
+
|
289
370
|
def _find_optimal_search_range(features, initial_search_range=500, increment=10, max_attempts=49, memory=3):
|
290
371
|
"""
|
291
372
|
Find the optimal search range for linking features.
|
@@ -336,7 +417,94 @@ def _remove_objects_from_first_frame(masks, percentage=10):
|
|
336
417
|
masks[0][first_frame == label] = 0
|
337
418
|
return masks
|
338
419
|
|
339
|
-
def
|
420
|
+
def _track_by_iou(masks, iou_threshold=0.1):
|
421
|
+
"""
|
422
|
+
Build a track table by linking masks frame→frame via IoU.
|
423
|
+
Returns a DataFrame with columns [frame, original_label, track_id].
|
424
|
+
"""
|
425
|
+
n_frames = masks.shape[0]
|
426
|
+
# 1) initialize: every label in frame 0 starts its own track
|
427
|
+
labels0 = np.unique(masks[0])[1:]
|
428
|
+
next_track = 1
|
429
|
+
track_map = {} # (frame,label) -> track_id
|
430
|
+
for L in labels0:
|
431
|
+
track_map[(0, L)] = next_track
|
432
|
+
next_track += 1
|
433
|
+
|
434
|
+
# 2) iterate through frames
|
435
|
+
for t in range(1, n_frames):
|
436
|
+
prev, curr = masks[t-1], masks[t]
|
437
|
+
matches = link_by_iou(prev, curr, iou_threshold=iou_threshold)
|
438
|
+
used_curr = set()
|
439
|
+
# a) assign matched labels to existing tracks
|
440
|
+
for L_prev, L_curr in matches:
|
441
|
+
tid = track_map[(t-1, L_prev)]
|
442
|
+
track_map[(t, L_curr)] = tid
|
443
|
+
used_curr.add(L_curr)
|
444
|
+
# b) any label in curr not matched → new track
|
445
|
+
for L in np.unique(curr)[1:]:
|
446
|
+
if L not in used_curr:
|
447
|
+
track_map[(t, L)] = next_track
|
448
|
+
next_track += 1
|
449
|
+
|
450
|
+
# 3) flatten into DataFrame
|
451
|
+
records = []
|
452
|
+
for (frame, label), tid in track_map.items():
|
453
|
+
records.append({'frame': frame, 'original_label': label, 'track_id': tid})
|
454
|
+
return pd.DataFrame(records)
|
455
|
+
|
456
|
+
|
457
|
+
def _facilitate_trackin_with_adaptive_removal(masks, search_range=None, max_attempts=5, memory=3, min_mass=50, track_by_iou=False):
|
458
|
+
"""
|
459
|
+
Facilitates object tracking with deterministic initial filtering and
|
460
|
+
trackpy’s constant-velocity prediction.
|
461
|
+
|
462
|
+
Args:
|
463
|
+
masks (np.ndarray): integer‐labeled masks (frames × H × W).
|
464
|
+
search_range (int|None): max displacement; if None, auto‐computed.
|
465
|
+
max_attempts (int): how many times to retry with smaller search_range.
|
466
|
+
memory (int): trackpy memory parameter.
|
467
|
+
min_mass (float): drop any object in frame 0 with area < min_mass.
|
468
|
+
|
469
|
+
Returns:
|
470
|
+
masks, features_df, tracks_df
|
471
|
+
|
472
|
+
Raises:
|
473
|
+
RuntimeError if linking fails after max_attempts.
|
474
|
+
"""
|
475
|
+
# 1) initial features & filter frame 0 by area
|
476
|
+
features = _prepare_for_tracking(masks)
|
477
|
+
f0 = features[features['frame'] == 0]
|
478
|
+
valid = f0.loc[f0['mass'] >= min_mass, 'original_label'].unique()
|
479
|
+
masks[0] = np.where(np.isin(masks[0], valid), masks[0], 0)
|
480
|
+
|
481
|
+
# 2) recompute features on filtered masks
|
482
|
+
features = _prepare_for_tracking(masks)
|
483
|
+
|
484
|
+
# 3) default search_range = 2×sqrt(99th‑pct area)
|
485
|
+
if search_range is None:
|
486
|
+
a99 = f0['mass'].quantile(0.99)
|
487
|
+
search_range = max(1, int(2 * np.sqrt(a99)))
|
488
|
+
|
489
|
+
# 4) attempt linking, shrinking search_range on failure
|
490
|
+
for attempt in range(1, max_attempts + 1):
|
491
|
+
try:
|
492
|
+
if track_by_iou:
|
493
|
+
tracks_df = _track_by_iou(masks, iou_threshold=0.1)
|
494
|
+
else:
|
495
|
+
tracks_df = tp.link_df(features,search_range=search_range, memory=memory, predict=True)
|
496
|
+
print(f"Linked on attempt {attempt} with search_range={search_range}")
|
497
|
+
return masks, features, tracks_df
|
498
|
+
|
499
|
+
except Exception as e:
|
500
|
+
search_range = max(1, int(search_range * 0.8))
|
501
|
+
print(f"Attempt {attempt} failed ({e}); reducing search_range to {search_range}")
|
502
|
+
|
503
|
+
raise RuntimeError(
|
504
|
+
f"Failed to track after {max_attempts} attempts; last search_range={search_range}"
|
505
|
+
)
|
506
|
+
|
507
|
+
def _facilitate_trackin_with_adaptive_removal_v1(masks, search_range=500, max_attempts=100, memory=3):
|
340
508
|
"""
|
341
509
|
Facilitates object tracking with adaptive removal.
|
342
510
|
|
spacr/utils.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import os, re, sqlite3, torch, torchvision, random, string, shutil, cv2, tarfile, glob, psutil, platform, gzip, subprocess, time, requests, ast, traceback
|
2
|
-
|
3
2
|
import numpy as np
|
4
3
|
import pandas as pd
|
5
4
|
from cellpose import models as cp_models
|
@@ -554,7 +553,7 @@ def _get_cellpose_batch_size():
|
|
554
553
|
except Exception as e:
|
555
554
|
return 8
|
556
555
|
|
557
|
-
def _extract_filename_metadata(filenames, src, regular_expression, metadata_type='cellvoyager'
|
556
|
+
def _extract_filename_metadata(filenames, src, regular_expression, metadata_type='cellvoyager'):
|
558
557
|
|
559
558
|
images_by_key = defaultdict(list)
|
560
559
|
|
@@ -568,33 +567,38 @@ def _extract_filename_metadata(filenames, src, regular_expression, metadata_type
|
|
568
567
|
plate = os.path.basename(src)
|
569
568
|
|
570
569
|
well = match.group('wellID')
|
571
|
-
field = match.group('fieldID')
|
572
|
-
channel = match.group('chanID')
|
573
|
-
mode = None
|
574
|
-
|
575
570
|
if well[0].isdigit():
|
576
571
|
well = str(_safe_int_convert(well))
|
572
|
+
|
573
|
+
field = match.group('fieldID')
|
577
574
|
if field[0].isdigit():
|
578
575
|
field = str(_safe_int_convert(field))
|
576
|
+
|
577
|
+
channel = match.group('chanID')
|
579
578
|
if channel[0].isdigit():
|
580
579
|
channel = str(_safe_int_convert(channel))
|
581
|
-
|
580
|
+
|
581
|
+
if 'timeID' in match.groupdict():
|
582
|
+
timeID = match.group('timeID')
|
583
|
+
if timeID[0].isdigit():
|
584
|
+
timeID = str(_safe_int_convert(timeID))
|
585
|
+
else:
|
586
|
+
timeID = None
|
587
|
+
|
588
|
+
if 'sliceID' in match.groupdict():
|
589
|
+
sliceID = match.group('sliceID')
|
590
|
+
if sliceID[0].isdigit():
|
591
|
+
sliceID = str(_safe_int_convert(sliceID))
|
592
|
+
else:
|
593
|
+
sliceID = None
|
594
|
+
|
582
595
|
if metadata_type =='cq1':
|
583
596
|
orig_wellID = wellID
|
584
597
|
wellID = _convert_cq1_well_id(wellID)
|
585
598
|
print(f'Converted Well ID: {orig_wellID} to {wellID}', end='\r', flush=True)
|
586
599
|
|
587
|
-
|
588
|
-
|
589
|
-
mode = match.group('AID')
|
590
|
-
except IndexError:
|
591
|
-
sliceid = '00'
|
592
|
-
|
593
|
-
if mode == skip_mode:
|
594
|
-
continue
|
595
|
-
|
596
|
-
key = (plate, well, field, channel, mode)
|
597
|
-
file_path = os.path.join(src, filename) # Store the full path
|
600
|
+
key = (plate, well, field, channel, timeID, sliceID)
|
601
|
+
file_path = os.path.join(src, filename)
|
598
602
|
images_by_key[key].append(file_path)
|
599
603
|
|
600
604
|
except IndexError:
|
@@ -3279,15 +3283,6 @@ class SaliencyMapGenerator:
|
|
3279
3283
|
return fig
|
3280
3284
|
|
3281
3285
|
def percentile_normalize(self, img, lower_percentile=2, upper_percentile=98):
|
3282
|
-
"""
|
3283
|
-
Normalize each channel of the image to the given percentiles.
|
3284
|
-
Args:
|
3285
|
-
img: Input image as numpy array with shape (H, W, C)
|
3286
|
-
lower_percentile: Lower percentile for normalization (default 2)
|
3287
|
-
upper_percentile: Upper percentile for normalization (default 98)
|
3288
|
-
Returns:
|
3289
|
-
img: Normalized image
|
3290
|
-
"""
|
3291
3286
|
img_normalized = np.zeros_like(img)
|
3292
3287
|
|
3293
3288
|
for c in range(img.shape[2]): # Iterate over each channel
|
@@ -3297,7 +3292,6 @@ class SaliencyMapGenerator:
|
|
3297
3292
|
|
3298
3293
|
return img_normalized
|
3299
3294
|
|
3300
|
-
|
3301
3295
|
class GradCAMGenerator:
|
3302
3296
|
def __init__(self, model, target_layer, cam_type='gradcam'):
|
3303
3297
|
self.model = model
|
@@ -3402,15 +3396,6 @@ class GradCAMGenerator:
|
|
3402
3396
|
return fig
|
3403
3397
|
|
3404
3398
|
def percentile_normalize(self, img, lower_percentile=2, upper_percentile=98):
|
3405
|
-
"""
|
3406
|
-
Normalize each channel of the image to the given percentiles.
|
3407
|
-
Args:
|
3408
|
-
img: Input image as numpy array with shape (H, W, C)
|
3409
|
-
lower_percentile: Lower percentile for normalization (default 2)
|
3410
|
-
upper_percentile: Upper percentile for normalization (default 98)
|
3411
|
-
Returns:
|
3412
|
-
img: Normalized image
|
3413
|
-
"""
|
3414
3399
|
img_normalized = np.zeros_like(img)
|
3415
3400
|
|
3416
3401
|
for c in range(img.shape[2]): # Iterate over each channel
|
@@ -4698,10 +4683,10 @@ def get_ml_results_paths(src, model_type='xgboost', channel_of_interest=1):
|
|
4698
4683
|
elif isinstance(channel_of_interest, int):
|
4699
4684
|
feature_string = f"channel_{channel_of_interest}"
|
4700
4685
|
|
4701
|
-
elif channel_of_interest
|
4686
|
+
elif channel_of_interest == 'morphology':
|
4702
4687
|
feature_string = 'morphology'
|
4703
4688
|
|
4704
|
-
elif channel_of_interest
|
4689
|
+
elif channel_of_interest == None:
|
4705
4690
|
feature_string = 'all_features'
|
4706
4691
|
else:
|
4707
4692
|
raise ValueError(f"Unsupported channel_of_interest: {channel_of_interest}. Supported values are 'int', 'list', 'None', or 'morphology'.")
|
@@ -5115,9 +5100,9 @@ def correct_metadata_column_names(df):
|
|
5115
5100
|
|
5116
5101
|
def control_filelist(folder, mode='columnID', values=['01','02']):
|
5117
5102
|
files = os.listdir(folder)
|
5118
|
-
if mode
|
5103
|
+
if mode == 'columnID':
|
5119
5104
|
filtered_files = [file for file in files if file.split('_')[1][1:] in values]
|
5120
|
-
if mode
|
5105
|
+
if mode == 'rowID':
|
5121
5106
|
filtered_files = [file for file in files if file.split('_')[1][:1] in values]
|
5122
5107
|
return filtered_files
|
5123
5108
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: spacr
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.0
|
4
4
|
Summary: Spatial phenotype analysis of crisp screens (SpaCr)
|
5
5
|
Home-page: https://github.com/EinarOlafsson/spacr
|
6
6
|
Author: Einar Birnir Olafsson
|
@@ -41,6 +41,8 @@ Requires-Dist: pillow<11.0,>=10.2.0
|
|
41
41
|
Requires-Dist: tifffile>=2023.4.12
|
42
42
|
Requires-Dist: nd2reader<4.0,>=3.3.0
|
43
43
|
Requires-Dist: czifile
|
44
|
+
Requires-Dist: pylibCZIrw<6.0,>=5.0.0
|
45
|
+
Requires-Dist: aicspylibczi
|
44
46
|
Requires-Dist: readlif
|
45
47
|
Requires-Dist: imageio<3.0,>=2.34.0
|
46
48
|
Requires-Dist: pingouin<1.0,>=0.5.5
|
@@ -79,7 +81,7 @@ Provides-Extra: headless
|
|
79
81
|
Requires-Dist: opencv-python-headless; extra == "headless"
|
80
82
|
|
81
83
|
.. |Documentation Status| image:: https://readthedocs.org/projects/spacr/badge/?version=latest
|
82
|
-
:target: https://
|
84
|
+
:target: https://einarolafsson.github.io/spacr
|
83
85
|
.. |PyPI version| image:: https://badge.fury.io/py/spacr.svg
|
84
86
|
:target: https://badge.fury.io/py/spacr
|
85
87
|
.. |Python version| image:: https://img.shields.io/pypi/pyversions/spacr
|