celldetective 1.0.2__py3-none-any.whl → 1.1.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.
- celldetective/__main__.py +2 -2
- celldetective/events.py +2 -44
- celldetective/filters.py +4 -5
- celldetective/gui/__init__.py +1 -1
- celldetective/gui/analyze_block.py +37 -10
- celldetective/gui/btrack_options.py +24 -23
- celldetective/gui/classifier_widget.py +62 -19
- celldetective/gui/configure_new_exp.py +32 -35
- celldetective/gui/control_panel.py +115 -81
- celldetective/gui/gui_utils.py +674 -396
- celldetective/gui/json_readers.py +7 -6
- celldetective/gui/layouts.py +755 -0
- celldetective/gui/measurement_options.py +168 -487
- celldetective/gui/neighborhood_options.py +322 -270
- celldetective/gui/plot_measurements.py +1114 -0
- celldetective/gui/plot_signals_ui.py +20 -20
- celldetective/gui/process_block.py +449 -169
- celldetective/gui/retrain_segmentation_model_options.py +27 -26
- celldetective/gui/retrain_signal_model_options.py +25 -24
- celldetective/gui/seg_model_loader.py +31 -27
- celldetective/gui/signal_annotator.py +2326 -2295
- celldetective/gui/signal_annotator_options.py +18 -16
- celldetective/gui/styles.py +16 -1
- celldetective/gui/survival_ui.py +61 -39
- celldetective/gui/tableUI.py +60 -23
- celldetective/gui/thresholds_gui.py +68 -66
- celldetective/gui/viewers.py +596 -0
- celldetective/io.py +234 -23
- celldetective/measure.py +37 -32
- celldetective/neighborhood.py +495 -27
- celldetective/preprocessing.py +683 -0
- celldetective/scripts/analyze_signals.py +7 -0
- celldetective/scripts/measure_cells.py +12 -0
- celldetective/scripts/segment_cells.py +5 -0
- celldetective/scripts/track_cells.py +11 -0
- celldetective/signals.py +221 -98
- celldetective/tracking.py +0 -1
- celldetective/utils.py +178 -36
- celldetective-1.1.0.dist-info/METADATA +305 -0
- celldetective-1.1.0.dist-info/RECORD +80 -0
- {celldetective-1.0.2.dist-info → celldetective-1.1.0.dist-info}/top_level.txt +1 -0
- tests/__init__.py +0 -0
- tests/test_events.py +28 -0
- tests/test_filters.py +24 -0
- tests/test_io.py +70 -0
- tests/test_measure.py +141 -0
- tests/test_neighborhood.py +70 -0
- tests/test_segmentation.py +93 -0
- tests/test_signals.py +135 -0
- tests/test_tracking.py +164 -0
- tests/test_utils.py +71 -0
- celldetective-1.0.2.dist-info/METADATA +0 -192
- celldetective-1.0.2.dist-info/RECORD +0 -66
- {celldetective-1.0.2.dist-info → celldetective-1.1.0.dist-info}/LICENSE +0 -0
- {celldetective-1.0.2.dist-info → celldetective-1.1.0.dist-info}/WHEEL +0 -0
- {celldetective-1.0.2.dist-info → celldetective-1.1.0.dist-info}/entry_points.txt +0 -0
celldetective/neighborhood.py
CHANGED
|
@@ -2,14 +2,16 @@ import numpy as np
|
|
|
2
2
|
import pandas as pd
|
|
3
3
|
from tqdm import tqdm
|
|
4
4
|
from skimage.measure import regionprops_table
|
|
5
|
+
from skimage.graph import pixel_graph
|
|
5
6
|
from functools import reduce
|
|
6
7
|
from mahotas.features import haralick
|
|
7
8
|
from scipy.ndimage import zoom
|
|
8
9
|
import os
|
|
9
10
|
import subprocess
|
|
10
11
|
from celldetective.utils import rename_intensity_column, create_patch_mask, remove_redundant_features
|
|
11
|
-
from celldetective.io import get_position_table
|
|
12
12
|
from scipy.spatial.distance import cdist
|
|
13
|
+
from celldetective.measure import contour_of_instance_segmentation
|
|
14
|
+
from celldetective.io import locate_labels, get_position_pickle, get_position_table
|
|
13
15
|
import re
|
|
14
16
|
|
|
15
17
|
abs_path = os.sep.join([os.path.split(os.path.dirname(os.path.realpath(__file__)))[0], 'celldetective'])
|
|
@@ -372,6 +374,23 @@ def compute_neighborhood_at_position(pos, distance, population=['targets','effec
|
|
|
372
374
|
df_A, path_A = get_position_table(pos, population=population[0], return_path=True)
|
|
373
375
|
df_B, path_B = get_position_table(pos, population=population[1], return_path=True)
|
|
374
376
|
|
|
377
|
+
df_A_pkl = get_position_pickle(pos, population=population[0], return_path=False)
|
|
378
|
+
df_B_pkl = get_position_pickle(pos, population=population[1], return_path=False)
|
|
379
|
+
|
|
380
|
+
if df_A_pkl is not None:
|
|
381
|
+
pkl_columns = np.array(df_A_pkl.columns)
|
|
382
|
+
neigh_columns = np.array([c.startswith('neighborhood') for c in pkl_columns])
|
|
383
|
+
cols = list(pkl_columns[neigh_columns]) + ['TRACK_ID','FRAME']
|
|
384
|
+
print(f'Recover {cols} from the pickle file...')
|
|
385
|
+
df_A = pd.merge(df_A, df_A_pkl.loc[:,cols], how="outer", on=['TRACK_ID','FRAME'])
|
|
386
|
+
print(df_A.columns)
|
|
387
|
+
if df_B_pkl is not None and df_B is not None:
|
|
388
|
+
pkl_columns = np.array(df_B_pkl.columns)
|
|
389
|
+
neigh_columns = np.array([c.startswith('neighborhood') for c in pkl_columns])
|
|
390
|
+
cols = list(pkl_columns[neigh_columns]) + ['TRACK_ID','FRAME']
|
|
391
|
+
print(f'Recover {cols} from the pickle file...')
|
|
392
|
+
df_B = pd.merge(df_B, df_B_pkl.loc[:,cols], how="outer", on=['TRACK_ID','FRAME'])
|
|
393
|
+
|
|
375
394
|
if clear_neigh:
|
|
376
395
|
unwanted = df_A.columns[df_A.columns.str.contains('neighborhood')]
|
|
377
396
|
df_A = df_A.drop(columns=unwanted)
|
|
@@ -558,7 +577,7 @@ def compute_neighborhood_metrics(neigh_table, neigh_col, metrics=['inclusive','e
|
|
|
558
577
|
|
|
559
578
|
return neigh_table
|
|
560
579
|
|
|
561
|
-
def mean_neighborhood_before_event(neigh_table, neigh_col, event_time_col):
|
|
580
|
+
def mean_neighborhood_before_event(neigh_table, neigh_col, event_time_col, metrics=['inclusive','exclusive','intermediate']):
|
|
562
581
|
|
|
563
582
|
"""
|
|
564
583
|
Computes the mean neighborhood metrics for each cell track before a specified event time.
|
|
@@ -593,11 +612,13 @@ def mean_neighborhood_before_event(neigh_table, neigh_col, event_time_col):
|
|
|
593
612
|
else:
|
|
594
613
|
groupbycols = ['TRACK_ID']
|
|
595
614
|
neigh_table.sort_values(by=groupbycols+['FRAME'],inplace=True)
|
|
615
|
+
suffix = '_before_event'
|
|
596
616
|
|
|
597
617
|
if event_time_col is None:
|
|
598
618
|
print('No event time was provided... Estimating the mean neighborhood over the whole observation time...')
|
|
599
619
|
neigh_table.loc[:,'event_time_temp'] = neigh_table['FRAME'].max()
|
|
600
620
|
event_time_col = 'event_time_temp'
|
|
621
|
+
suffix = ''
|
|
601
622
|
|
|
602
623
|
for tid,group in neigh_table.groupby(groupbycols):
|
|
603
624
|
|
|
@@ -613,22 +634,24 @@ def mean_neighborhood_before_event(neigh_table, neigh_col, event_time_col):
|
|
|
613
634
|
if event_time<0.:
|
|
614
635
|
event_time = group['FRAME'].max()
|
|
615
636
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
if
|
|
625
|
-
|
|
637
|
+
if 'intermediate' in metrics:
|
|
638
|
+
valid_counts_intermediate = group.loc[group['FRAME']<=event_time,'intermediate_count_s1_'+neigh_col].to_numpy()
|
|
639
|
+
if len(valid_counts_intermediate[valid_counts_intermediate==valid_counts_intermediate])>0:
|
|
640
|
+
neigh_table.loc[indices, f'mean_count_intermediate_{neigh_col}{suffix}'] = np.nanmean(valid_counts_intermediate)
|
|
641
|
+
if 'inclusive' in metrics:
|
|
642
|
+
valid_counts_inclusive = group.loc[group['FRAME']<=event_time,'inclusive_count_s1_'+neigh_col].to_numpy()
|
|
643
|
+
if len(valid_counts_inclusive[valid_counts_inclusive==valid_counts_inclusive])>0:
|
|
644
|
+
neigh_table.loc[indices, f'mean_count_inclusive_{neigh_col}{suffix}'] = np.nanmean(valid_counts_inclusive)
|
|
645
|
+
if 'exclusive' in metrics:
|
|
646
|
+
valid_counts_exclusive = group.loc[group['FRAME']<=event_time,'exclusive_count_s1_'+neigh_col].to_numpy()
|
|
647
|
+
if len(valid_counts_exclusive[valid_counts_exclusive==valid_counts_exclusive])>0:
|
|
648
|
+
neigh_table.loc[indices, f'mean_count_exclusive_{neigh_col}{suffix}'] = np.nanmean(valid_counts_exclusive)
|
|
626
649
|
|
|
627
650
|
if event_time_col=='event_time_temp':
|
|
628
651
|
neigh_table = neigh_table.drop(columns='event_time_temp')
|
|
629
652
|
return neigh_table
|
|
630
653
|
|
|
631
|
-
def mean_neighborhood_after_event(neigh_table, neigh_col, event_time_col):
|
|
654
|
+
def mean_neighborhood_after_event(neigh_table, neigh_col, event_time_col, metrics=['inclusive','exclusive','intermediate']):
|
|
632
655
|
|
|
633
656
|
"""
|
|
634
657
|
Computes the mean neighborhood metrics for each cell track after a specified event time.
|
|
@@ -663,10 +686,12 @@ def mean_neighborhood_after_event(neigh_table, neigh_col, event_time_col):
|
|
|
663
686
|
else:
|
|
664
687
|
groupbycols = ['TRACK_ID']
|
|
665
688
|
neigh_table.sort_values(by=groupbycols+['FRAME'],inplace=True)
|
|
689
|
+
suffix = '_after_event'
|
|
666
690
|
|
|
667
691
|
if event_time_col is None:
|
|
668
692
|
neigh_table.loc[:,'event_time_temp'] = None #neigh_table['FRAME'].max()
|
|
669
693
|
event_time_col = 'event_time_temp'
|
|
694
|
+
suffix = ''
|
|
670
695
|
|
|
671
696
|
for tid,group in neigh_table.groupby(groupbycols):
|
|
672
697
|
|
|
@@ -679,26 +704,469 @@ def mean_neighborhood_after_event(neigh_table, neigh_col, event_time_col):
|
|
|
679
704
|
else:
|
|
680
705
|
continue
|
|
681
706
|
|
|
682
|
-
if event_time is None
|
|
683
|
-
neigh_table.loc[indices, f'mean_count_intermediate_{neigh_col}_after_event'] = np.nan
|
|
684
|
-
neigh_table.loc[indices, f'mean_count_inclusive_{neigh_col}_after_event'] = np.nan
|
|
685
|
-
neigh_table.loc[indices, f'mean_count_exclusive_{neigh_col}_after_event'] = np.nan
|
|
686
|
-
else:
|
|
687
|
-
valid_counts_intermediate = group.loc[group['FRAME']>event_time,'intermediate_count_s1_'+neigh_col].to_numpy()
|
|
688
|
-
valid_counts_inclusive = group.loc[group['FRAME']>event_time,'inclusive_count_s1_'+neigh_col].to_numpy()
|
|
689
|
-
valid_counts_exclusive = group.loc[group['FRAME']>event_time,'exclusive_count_s1_'+neigh_col].to_numpy()
|
|
707
|
+
if event_time is not None and (event_time>=0.):
|
|
690
708
|
|
|
691
|
-
if
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if
|
|
696
|
-
|
|
709
|
+
if 'intermediate' in metrics:
|
|
710
|
+
valid_counts_intermediate = group.loc[group['FRAME']>event_time,'intermediate_count_s1_'+neigh_col].to_numpy()
|
|
711
|
+
if len(valid_counts_intermediate[valid_counts_intermediate==valid_counts_intermediate])>0:
|
|
712
|
+
neigh_table.loc[indices, f'mean_count_intermediate_{neigh_col}{suffix}'] = np.nanmean(valid_counts_intermediate)
|
|
713
|
+
if 'inclusive' in metrics:
|
|
714
|
+
valid_counts_inclusive = group.loc[group['FRAME']>event_time,'inclusive_count_s1_'+neigh_col].to_numpy()
|
|
715
|
+
if len(valid_counts_inclusive[valid_counts_inclusive==valid_counts_inclusive])>0:
|
|
716
|
+
neigh_table.loc[indices, f'mean_count_inclusive_{neigh_col}{suffix}'] = np.nanmean(valid_counts_inclusive)
|
|
717
|
+
if 'exclusive' in metrics:
|
|
718
|
+
valid_counts_exclusive = group.loc[group['FRAME']>event_time,'exclusive_count_s1_'+neigh_col].to_numpy()
|
|
719
|
+
if len(valid_counts_exclusive[valid_counts_exclusive==valid_counts_exclusive])>0:
|
|
720
|
+
neigh_table.loc[indices, f'mean_count_exclusive_{neigh_col}{suffix}'] = np.nanmean(valid_counts_exclusive)
|
|
697
721
|
|
|
698
722
|
if event_time_col=='event_time_temp':
|
|
699
723
|
neigh_table = neigh_table.drop(columns='event_time_temp')
|
|
724
|
+
|
|
700
725
|
return neigh_table
|
|
701
726
|
|
|
727
|
+
# New functions for direct cell-cell contact neighborhood
|
|
728
|
+
|
|
729
|
+
def sign(num):
|
|
730
|
+
return -1 if num < 0 else 1
|
|
731
|
+
|
|
732
|
+
def contact_neighborhood(labelsA, labelsB=None, border=3, connectivity=2):
|
|
733
|
+
|
|
734
|
+
labelsA = labelsA.astype(float)
|
|
735
|
+
if labelsB is not None:
|
|
736
|
+
labelsB = labelsB.astype(float)
|
|
737
|
+
|
|
738
|
+
print(f"Border = {border}")
|
|
739
|
+
|
|
740
|
+
if border > 0:
|
|
741
|
+
print(labelsA.shape, border * (-1))
|
|
742
|
+
labelsA_edge = contour_of_instance_segmentation(label=labelsA, distance=border * (-1)).astype(float)
|
|
743
|
+
labelsA[np.where(labelsA_edge>0)] = labelsA_edge[np.where(labelsA_edge>0)]
|
|
744
|
+
if labelsB is not None:
|
|
745
|
+
labelsB_edge = contour_of_instance_segmentation(label=labelsB, distance=border * (-1)).astype(float)
|
|
746
|
+
labelsB[np.where(labelsB_edge>0)] = labelsB_edge[np.where(labelsB_edge>0)]
|
|
747
|
+
|
|
748
|
+
if labelsB is not None:
|
|
749
|
+
labelsA[labelsA!=0] = -labelsA[labelsA!=0]
|
|
750
|
+
labelsAB = merge_labels(labelsA, labelsB)
|
|
751
|
+
labelsBA = merge_labels(labelsB, labelsA)
|
|
752
|
+
label_cases = [labelsAB, labelsBA]
|
|
753
|
+
else:
|
|
754
|
+
label_cases = [labelsA]
|
|
755
|
+
|
|
756
|
+
coocurrences = []
|
|
757
|
+
for lbl in label_cases:
|
|
758
|
+
coocurrences.extend(find_contact_neighbors(lbl, connectivity=connectivity))
|
|
759
|
+
|
|
760
|
+
unique_pairs = np.unique(coocurrences,axis=0)
|
|
761
|
+
|
|
762
|
+
if labelsB is not None:
|
|
763
|
+
neighs = np.unique([tuple(sorted(p)) for p in unique_pairs if p[0]!=p[1] and sign(p[0])!=sign(p[1])],axis=0)
|
|
764
|
+
else:
|
|
765
|
+
neighs = np.unique([tuple(sorted(p)) for p in unique_pairs if p[0]!=p[1]],axis=0)
|
|
766
|
+
|
|
767
|
+
return neighs
|
|
768
|
+
|
|
769
|
+
def merge_labels(labelsA, labelsB):
|
|
770
|
+
|
|
771
|
+
labelsA = labelsA.astype(float)
|
|
772
|
+
labelsB = labelsB.astype(float)
|
|
773
|
+
|
|
774
|
+
labelsAB = labelsA.copy()
|
|
775
|
+
labelsAB[np.where(labelsB!=0)] = labelsB[np.where(labelsB!=0)]
|
|
776
|
+
|
|
777
|
+
return labelsAB
|
|
778
|
+
|
|
779
|
+
def find_contact_neighbors(labels, connectivity=2):
|
|
780
|
+
|
|
781
|
+
assert labels.ndim==2,"Wrong dimension for labels..."
|
|
782
|
+
g, nodes = pixel_graph(labels, mask=labels.astype(bool),connectivity=connectivity)
|
|
783
|
+
g.eliminate_zeros()
|
|
784
|
+
|
|
785
|
+
coo = g.tocoo()
|
|
786
|
+
center_coords = nodes[coo.row]
|
|
787
|
+
neighbor_coords = nodes[coo.col]
|
|
788
|
+
|
|
789
|
+
center_values = labels.ravel()[center_coords]
|
|
790
|
+
neighbor_values = labels.ravel()[neighbor_coords]
|
|
791
|
+
touching_masks = np.column_stack((center_values, neighbor_values))
|
|
792
|
+
|
|
793
|
+
return touching_masks
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def mask_contact_neighborhood(setA, setB, labelsA, labelsB, distance, mode='two-pop', status=None, not_status_option=None, compute_cum_sum=True,
|
|
797
|
+
attention_weight=True, symmetrize=True, include_dead_weight=True,
|
|
798
|
+
column_labels={'track': "TRACK_ID", 'time': 'FRAME', 'x': 'POSITION_X', 'y': 'POSITION_Y', 'mask_id': 'class_id'}):
|
|
799
|
+
|
|
800
|
+
"""
|
|
801
|
+
|
|
802
|
+
Match neighbors in set A and B within a circle of radius d.
|
|
803
|
+
|
|
804
|
+
Parameters
|
|
805
|
+
----------
|
|
806
|
+
setA,setB : pandas DataFrame
|
|
807
|
+
Trajectory or position sets A and B.
|
|
808
|
+
distance : float
|
|
809
|
+
Cut-distance in pixels to match neighboring pairs.
|
|
810
|
+
mode: str
|
|
811
|
+
neighboring mode, between 'two-pop' (e.g. target-effector) and 'self' (target-target or effector-effector).
|
|
812
|
+
status: None or status
|
|
813
|
+
name to look for cells to ignore (because they are dead). By default all cells are kept.
|
|
814
|
+
compute_cum_sum: bool,
|
|
815
|
+
compute cumulated time of presence of neighbours (only if trajectories available for both sets)
|
|
816
|
+
attention_weight: bool,
|
|
817
|
+
compute the attention weight (how much a cell of set B is shared across cells of set A)
|
|
818
|
+
symmetrize: bool,
|
|
819
|
+
write in set B the neighborhood of set A
|
|
820
|
+
include_dead_weight: bool
|
|
821
|
+
do not count dead cells when establishing attention weight
|
|
822
|
+
"""
|
|
823
|
+
|
|
824
|
+
# Check live_status option
|
|
825
|
+
if setA is not None and setB is not None:
|
|
826
|
+
setA, setB, status = set_live_status(setA, setB, status, not_status_option)
|
|
827
|
+
else:
|
|
828
|
+
return None,None
|
|
829
|
+
|
|
830
|
+
# Check distance option
|
|
831
|
+
if not isinstance(distance, list):
|
|
832
|
+
distance = [distance]
|
|
833
|
+
|
|
834
|
+
for d in distance:
|
|
835
|
+
# loop over each provided distance
|
|
836
|
+
|
|
837
|
+
if mode=='two-pop':
|
|
838
|
+
neigh_col = f'neighborhood_2_contact_{d}_px'
|
|
839
|
+
elif mode=='self':
|
|
840
|
+
neigh_col = f'neighborhood_self_contact_{d}_px'
|
|
841
|
+
|
|
842
|
+
cl = []
|
|
843
|
+
for s in [setA,setB]:
|
|
844
|
+
|
|
845
|
+
# Check whether data can be tracked
|
|
846
|
+
temp_column_labels = column_labels.copy()
|
|
847
|
+
|
|
848
|
+
if not 'TRACK_ID' in list(s.columns):
|
|
849
|
+
temp_column_labels.update({'track': 'ID'})
|
|
850
|
+
compute_cum_sum = False # if no tracking data then cum_sum is not relevant
|
|
851
|
+
cl.append(temp_column_labels)
|
|
852
|
+
|
|
853
|
+
# Remove nan tracks (cells that do not belong to a track)
|
|
854
|
+
s[neigh_col] = np.nan
|
|
855
|
+
s[neigh_col] = s[neigh_col].astype(object)
|
|
856
|
+
s.dropna(subset=[cl[-1]['track']],inplace=True)
|
|
857
|
+
|
|
858
|
+
# Loop over each available timestep
|
|
859
|
+
timeline = np.unique(np.concatenate([setA[cl[0]['time']].to_numpy(), setB[cl[1]['time']].to_numpy()])).astype(int)
|
|
860
|
+
for t in tqdm(timeline):
|
|
861
|
+
|
|
862
|
+
index_A = list(setA.loc[setA[cl[0]['time']]==t].index)
|
|
863
|
+
coordinates_A = setA.loc[setA[cl[0]['time']]==t,[cl[0]['x'], cl[0]['y']]].to_numpy()
|
|
864
|
+
ids_A = setA.loc[setA[cl[0]['time']]==t,cl[0]['track']].to_numpy()
|
|
865
|
+
mask_ids_A = setA.loc[setA[cl[0]['time']]==t,cl[0]['mask_id']].to_numpy()
|
|
866
|
+
status_A = setA.loc[setA[cl[0]['time']]==t,status[0]].to_numpy()
|
|
867
|
+
|
|
868
|
+
index_B = list(setB.loc[setB[cl[1]['time']]==t].index)
|
|
869
|
+
coordinates_B = setB.loc[setB[cl[1]['time']]==t,[cl[1]['x'], cl[1]['y']]].to_numpy()
|
|
870
|
+
ids_B = setB.loc[setB[cl[1]['time']]==t,cl[1]['track']].to_numpy()
|
|
871
|
+
mask_ids_B = setB.loc[setB[cl[1]['time']]==t,cl[1]['mask_id']].to_numpy()
|
|
872
|
+
status_B = setB.loc[setB[cl[1]['time']]==t,status[1]].to_numpy()
|
|
873
|
+
|
|
874
|
+
print(f"Frame {t}")
|
|
875
|
+
print(f"{mask_ids_A=}",f"{mask_ids_B}")
|
|
876
|
+
|
|
877
|
+
if len(ids_A) > 0 and len(ids_B) > 0:
|
|
878
|
+
|
|
879
|
+
# compute distance matrix
|
|
880
|
+
dist_map = cdist(coordinates_A, coordinates_B, metric="euclidean")
|
|
881
|
+
|
|
882
|
+
# Do the mask contact computation
|
|
883
|
+
if labelsB is not None:
|
|
884
|
+
lblB = labelsB[t]
|
|
885
|
+
else:
|
|
886
|
+
lblB = labelsB
|
|
887
|
+
|
|
888
|
+
print(f"Distance {d} for contact as border")
|
|
889
|
+
contact_pairs = contact_neighborhood(labelsA[t], labelsB=lblB, border=d, connectivity=2)
|
|
890
|
+
|
|
891
|
+
print(t, f"{np.unique(labelsA[t])=}")
|
|
892
|
+
print(f"Frame {t}: found the following contact pairs: {contact_pairs}...")
|
|
893
|
+
# Put infinite distance to all non-contact pairs (something like this)
|
|
894
|
+
plot_map=False
|
|
895
|
+
|
|
896
|
+
if len(contact_pairs)>0:
|
|
897
|
+
mask = np.ones_like(dist_map).astype(bool)
|
|
898
|
+
|
|
899
|
+
indices_to_keep = []
|
|
900
|
+
for cp in contact_pairs:
|
|
901
|
+
|
|
902
|
+
if np.any(cp<0):
|
|
903
|
+
if cp[0]<0:
|
|
904
|
+
mask_A = cp[1]
|
|
905
|
+
mask_B = np.abs(cp[0])
|
|
906
|
+
else:
|
|
907
|
+
mask_A = cp[0]
|
|
908
|
+
mask_B = np.abs(cp[1])
|
|
909
|
+
else:
|
|
910
|
+
mask_A = cp[0]
|
|
911
|
+
mask_B = cp[1]
|
|
912
|
+
|
|
913
|
+
try:
|
|
914
|
+
|
|
915
|
+
idx_A = np.where(mask_ids_A==int(mask_A))[0][0]
|
|
916
|
+
idx_B = np.where(mask_ids_B==int(mask_B))[0][0]
|
|
917
|
+
print(idx_A, idx_B)
|
|
918
|
+
indices_to_keep.append([idx_A,idx_B])
|
|
919
|
+
except:
|
|
920
|
+
pass
|
|
921
|
+
|
|
922
|
+
print(f'Indices to keep: {indices_to_keep}...')
|
|
923
|
+
if len(indices_to_keep)>0:
|
|
924
|
+
indices_to_keep = np.array(indices_to_keep)
|
|
925
|
+
mask[indices_to_keep[:,0],indices_to_keep[:,1]] = False
|
|
926
|
+
if mode=='self':
|
|
927
|
+
mask[indices_to_keep[:,1],indices_to_keep[:,0]] = False
|
|
928
|
+
dist_map[mask] = 1.0E06
|
|
929
|
+
plot_map=True
|
|
930
|
+
else:
|
|
931
|
+
dist_map[:,:] = 1.0E06
|
|
932
|
+
|
|
933
|
+
# PROCEED all the same?? --> I guess so
|
|
934
|
+
# if plot_map:
|
|
935
|
+
# import matplotlib.pyplot as plt
|
|
936
|
+
# print(indices_to_keep)
|
|
937
|
+
# plt.imshow(dist_map)
|
|
938
|
+
# plt.pause(5)
|
|
939
|
+
# plt.close()
|
|
940
|
+
|
|
941
|
+
d_filter = 1.0E05
|
|
942
|
+
if attention_weight:
|
|
943
|
+
weights, closest_A = compute_attention_weight(dist_map, d_filter, status_A, ids_A, axis=1, include_dead_weight=include_dead_weight)
|
|
944
|
+
|
|
945
|
+
# Target centric
|
|
946
|
+
for k in range(dist_map.shape[0]):
|
|
947
|
+
|
|
948
|
+
col = dist_map[k,:]
|
|
949
|
+
col[col==0.] = 1.0E06
|
|
950
|
+
|
|
951
|
+
neighs_B = np.array([ids_B[i] for i in np.where((col<=d_filter))[0]])
|
|
952
|
+
status_neigh_B = np.array([status_B[i] for i in np.where((col<=d_filter))[0]])
|
|
953
|
+
dist_B = [round(col[i],2) for i in np.where((col<=d_filter))[0]]
|
|
954
|
+
if len(dist_B)>0:
|
|
955
|
+
closest_B_cell = neighs_B[np.argmin(dist_B)]
|
|
956
|
+
|
|
957
|
+
if symmetrize and attention_weight:
|
|
958
|
+
n_neighs = float(len(neighs_B))
|
|
959
|
+
if not include_dead_weight:
|
|
960
|
+
n_neighs_alive = len(np.where(status_neigh_B==1)[0])
|
|
961
|
+
neigh_count = n_neighs_alive
|
|
962
|
+
else:
|
|
963
|
+
neigh_count = n_neighs
|
|
964
|
+
if neigh_count>0:
|
|
965
|
+
weight_A = 1./neigh_count
|
|
966
|
+
else:
|
|
967
|
+
weight_A = np.nan
|
|
968
|
+
|
|
969
|
+
if not include_dead_weight and status_A[k]==0:
|
|
970
|
+
weight_A = 0
|
|
971
|
+
|
|
972
|
+
neighs = []
|
|
973
|
+
setA.at[index_A[k], neigh_col] = []
|
|
974
|
+
for n in range(len(neighs_B)):
|
|
975
|
+
|
|
976
|
+
# index in setB
|
|
977
|
+
n_index = np.where(ids_B==neighs_B[n])[0][0]
|
|
978
|
+
# Assess if neigh B is closest to A
|
|
979
|
+
if attention_weight:
|
|
980
|
+
if closest_A[n_index]==ids_A[k]:
|
|
981
|
+
closest = True
|
|
982
|
+
else:
|
|
983
|
+
closest = False
|
|
984
|
+
|
|
985
|
+
if symmetrize:
|
|
986
|
+
# Load neighborhood previous data
|
|
987
|
+
sym_neigh = setB.loc[index_B[n_index], neigh_col]
|
|
988
|
+
if neighs_B[n]==closest_B_cell:
|
|
989
|
+
closest_b=True
|
|
990
|
+
else:
|
|
991
|
+
closest_b=False
|
|
992
|
+
if isinstance(sym_neigh, list):
|
|
993
|
+
sym_neigh.append({'id': ids_A[k], 'distance': dist_B[n], 'status': status_A[k]})
|
|
994
|
+
else:
|
|
995
|
+
sym_neigh = [{'id': ids_A[k], 'distance': dist_B[n],'status': status_A[k]}]
|
|
996
|
+
if attention_weight:
|
|
997
|
+
sym_neigh[-1].update({'weight': weight_A, 'closest': closest_b})
|
|
998
|
+
|
|
999
|
+
# Write the minimum info about neighborhing cell B
|
|
1000
|
+
neigh_dico = {'id': neighs_B[n], 'distance': dist_B[n], 'status': status_neigh_B[n]}
|
|
1001
|
+
if attention_weight:
|
|
1002
|
+
neigh_dico.update({'weight': weights[n_index], 'closest': closest})
|
|
1003
|
+
|
|
1004
|
+
if compute_cum_sum:
|
|
1005
|
+
# Compute the integrated presence of the neighboring cell B
|
|
1006
|
+
assert cl[1]['track'] == 'TRACK_ID','The set B does not seem to contain tracked data. The cumulative time will be meaningless.'
|
|
1007
|
+
past_neighs = [[ll['id'] for ll in l] if len(l)>0 else [None] for l in setA.loc[(setA[cl[0]['track']]==ids_A[k])&(setA[cl[0]['time']]<=t), neigh_col].to_numpy()]
|
|
1008
|
+
past_neighs = [item for sublist in past_neighs for item in sublist]
|
|
1009
|
+
|
|
1010
|
+
if attention_weight:
|
|
1011
|
+
past_weights = [[ll['weight'] for ll in l] if len(l)>0 else [None] for l in setA.loc[(setA[cl[0]['track']]==ids_A[k])&(setA[cl[0]['time']]<=t), neigh_col].to_numpy()]
|
|
1012
|
+
past_weights = [item for sublist in past_weights for item in sublist]
|
|
1013
|
+
|
|
1014
|
+
cum_sum = len(np.where(past_neighs==neighs_B[n])[0])
|
|
1015
|
+
neigh_dico.update({'cumulated_presence': cum_sum+1})
|
|
1016
|
+
|
|
1017
|
+
if attention_weight:
|
|
1018
|
+
cum_sum_weighted = np.sum([w if l==neighs_B[n] else 0 for l,w in zip(past_neighs, past_weights)])
|
|
1019
|
+
neigh_dico.update({'cumulated_presence_weighted': cum_sum_weighted + weights[n_index]})
|
|
1020
|
+
|
|
1021
|
+
if symmetrize:
|
|
1022
|
+
setB.at[index_B[n_index], neigh_col] = sym_neigh
|
|
1023
|
+
|
|
1024
|
+
neighs.append(neigh_dico)
|
|
1025
|
+
|
|
1026
|
+
setA.at[index_A[k], neigh_col] = neighs
|
|
1027
|
+
|
|
1028
|
+
return setA, setB
|
|
1029
|
+
|
|
1030
|
+
def compute_contact_neighborhood_at_position(pos, distance, population=['targets','effectors'], theta_dist=None, img_shape=(2048,2048), return_tables=False, clear_neigh=False, event_time_col=None,
|
|
1031
|
+
neighborhood_kwargs={'mode': 'two-pop','status': None, 'not_status_option': None,'include_dead_weight': True,"compute_cum_sum": False,"attention_weight": True, 'symmetrize': True}):
|
|
1032
|
+
|
|
1033
|
+
"""
|
|
1034
|
+
Computes neighborhood metrics for specified cell populations within a given position, based on distance criteria and additional parameters.
|
|
1035
|
+
|
|
1036
|
+
This function assesses the neighborhood interactions between two specified cell populations (or within a single population) at a given position.
|
|
1037
|
+
It computes various neighborhood metrics based on specified distances, considering the entire image or excluding edge regions.
|
|
1038
|
+
The results are optionally cleared of previous neighborhood calculations and can be returned as updated tables.
|
|
1039
|
+
|
|
1040
|
+
Parameters
|
|
1041
|
+
----------
|
|
1042
|
+
pos : str
|
|
1043
|
+
The path to the position directory where the analysis is to be performed.
|
|
1044
|
+
distance : float or list of float
|
|
1045
|
+
The distance(s) in pixels to define neighborhoods.
|
|
1046
|
+
population : list of str, optional
|
|
1047
|
+
Names of the cell populations to analyze. If a single population is provided, it is used for both populations in the analysis (default is ['targets', 'effectors']).
|
|
1048
|
+
theta_dist : float or list of float, optional
|
|
1049
|
+
Edge threshold(s) in pixels to exclude cells close to the image boundaries from the analysis. If not provided, defaults to 90% of each specified distance.
|
|
1050
|
+
img_shape : tuple of int, optional
|
|
1051
|
+
The dimensions (height, width) of the images in pixels (default is (2048, 2048)).
|
|
1052
|
+
return_tables : bool, optional
|
|
1053
|
+
If True, returns the updated data tables for both populations (default is False).
|
|
1054
|
+
clear_neigh : bool, optional
|
|
1055
|
+
If True, clears existing neighborhood columns from the data tables before computing new metrics (default is False).
|
|
1056
|
+
event_time_col : str, optional
|
|
1057
|
+
The column name indicating the event time for each cell, required if mean neighborhood metrics are to be computed before events.
|
|
1058
|
+
neighborhood_kwargs : dict, optional
|
|
1059
|
+
Additional keyword arguments for neighborhood computation, including mode, status options, and metrics (default includes mode 'two-pop', and symmetrization).
|
|
1060
|
+
|
|
1061
|
+
Returns
|
|
1062
|
+
-------
|
|
1063
|
+
pandas.DataFrame or (pandas.DataFrame, pandas.DataFrame)
|
|
1064
|
+
If `return_tables` is True, returns the updated data tables for the specified populations. If only one population is analyzed, both returned data frames will be identical.
|
|
1065
|
+
|
|
1066
|
+
Raises
|
|
1067
|
+
------
|
|
1068
|
+
AssertionError
|
|
1069
|
+
If the specified position path does not exist or if the number of distances and edge thresholds do not match.
|
|
1070
|
+
|
|
1071
|
+
"""
|
|
1072
|
+
|
|
1073
|
+
pos = pos.replace('\\','/')
|
|
1074
|
+
pos = rf"{pos}"
|
|
1075
|
+
assert os.path.exists(pos),f'Position {pos} is not a valid path.'
|
|
1076
|
+
|
|
1077
|
+
if isinstance(population, str):
|
|
1078
|
+
population = [population, population]
|
|
1079
|
+
|
|
1080
|
+
if not isinstance(distance, list):
|
|
1081
|
+
distance = [distance]
|
|
1082
|
+
if not theta_dist is None and not isinstance(theta_dist, list):
|
|
1083
|
+
theta_dist = [theta_dist]
|
|
1084
|
+
|
|
1085
|
+
if theta_dist is None:
|
|
1086
|
+
theta_dist = [0 for d in distance] #0.9*d
|
|
1087
|
+
assert len(theta_dist)==len(distance),'Incompatible number of distances and number of edge thresholds.'
|
|
1088
|
+
|
|
1089
|
+
if population[0]==population[1]:
|
|
1090
|
+
neighborhood_kwargs.update({'mode': 'self'})
|
|
1091
|
+
if population[1]!=population[0]:
|
|
1092
|
+
neighborhood_kwargs.update({'mode': 'two-pop'})
|
|
1093
|
+
|
|
1094
|
+
df_A, path_A = get_position_table(pos, population=population[0], return_path=True)
|
|
1095
|
+
df_B, path_B = get_position_table(pos, population=population[1], return_path=True)
|
|
1096
|
+
|
|
1097
|
+
df_A_pkl = get_position_pickle(pos, population=population[0], return_path=False)
|
|
1098
|
+
df_B_pkl = get_position_pickle(pos, population=population[1], return_path=False)
|
|
1099
|
+
|
|
1100
|
+
if df_A_pkl is not None:
|
|
1101
|
+
pkl_columns = np.array(df_A_pkl.columns)
|
|
1102
|
+
neigh_columns = np.array([c.startswith('neighborhood') for c in pkl_columns])
|
|
1103
|
+
cols = list(pkl_columns[neigh_columns]) + ['TRACK_ID','FRAME']
|
|
1104
|
+
print(f'Recover {cols} from the pickle file...')
|
|
1105
|
+
df_A = pd.merge(df_A, df_A_pkl.loc[:,cols], how="outer", on=['TRACK_ID','FRAME'])
|
|
1106
|
+
print(df_A.columns)
|
|
1107
|
+
if df_B_pkl is not None and df_B is not None:
|
|
1108
|
+
pkl_columns = np.array(df_B_pkl.columns)
|
|
1109
|
+
neigh_columns = np.array([c.startswith('neighborhood') for c in pkl_columns])
|
|
1110
|
+
cols = list(pkl_columns[neigh_columns]) + ['TRACK_ID','FRAME']
|
|
1111
|
+
print(f'Recover {cols} from the pickle file...')
|
|
1112
|
+
df_B = pd.merge(df_B, df_B_pkl.loc[:,cols], how="outer", on=['TRACK_ID','FRAME'])
|
|
1113
|
+
|
|
1114
|
+
labelsA = locate_labels(pos, population=population[0])
|
|
1115
|
+
if population[1]==population[0]:
|
|
1116
|
+
labelsB = None
|
|
1117
|
+
else:
|
|
1118
|
+
labelsB = locate_labels(pos, population=population[1])
|
|
1119
|
+
|
|
1120
|
+
if clear_neigh:
|
|
1121
|
+
unwanted = df_A.columns[df_A.columns.str.contains('neighborhood')]
|
|
1122
|
+
df_A = df_A.drop(columns=unwanted)
|
|
1123
|
+
unwanted = df_B.columns[df_B.columns.str.contains('neighborhood')]
|
|
1124
|
+
df_B = df_B.drop(columns=unwanted)
|
|
1125
|
+
|
|
1126
|
+
print(f"Distance: {distance} for mask contact")
|
|
1127
|
+
df_A, df_B = mask_contact_neighborhood(df_A, df_B, labelsA, labelsB, distance,**neighborhood_kwargs)
|
|
1128
|
+
if df_A is None or df_B is None:
|
|
1129
|
+
return None
|
|
1130
|
+
|
|
1131
|
+
for td,d in zip(theta_dist, distance):
|
|
1132
|
+
|
|
1133
|
+
if neighborhood_kwargs['mode']=='two-pop':
|
|
1134
|
+
neigh_col = f'neighborhood_2_contact_{d}_px'
|
|
1135
|
+
elif neighborhood_kwargs['mode']=='self':
|
|
1136
|
+
neigh_col = f'neighborhood_self_contact_{d}_px'
|
|
1137
|
+
|
|
1138
|
+
df_A.loc[df_A['class_id'].isnull(),neigh_col] = np.nan
|
|
1139
|
+
|
|
1140
|
+
# edge_filter_A = (df_A['POSITION_X'] > td)&(df_A['POSITION_Y'] > td)&(df_A['POSITION_Y'] < (img_shape[0] - td))&(df_A['POSITION_X'] < (img_shape[1] - td))
|
|
1141
|
+
# edge_filter_B = (df_B['POSITION_X'] > td)&(df_B['POSITION_Y'] > td)&(df_B['POSITION_Y'] < (img_shape[0] - td))&(df_B['POSITION_X'] < (img_shape[1] - td))
|
|
1142
|
+
# df_A.loc[~edge_filter_A, neigh_col] = np.nan
|
|
1143
|
+
# df_B.loc[~edge_filter_B, neigh_col] = np.nan
|
|
1144
|
+
|
|
1145
|
+
df_A = compute_neighborhood_metrics(df_A, neigh_col, metrics=['inclusive','intermediate'], decompose_by_status=True)
|
|
1146
|
+
if neighborhood_kwargs['symmetrize']:
|
|
1147
|
+
df_B = compute_neighborhood_metrics(df_B, neigh_col, metrics=['inclusive','intermediate'], decompose_by_status=True)
|
|
1148
|
+
|
|
1149
|
+
df_A = mean_neighborhood_before_event(df_A, neigh_col, event_time_col, metrics=['inclusive','intermediate'])
|
|
1150
|
+
if event_time_col is not None:
|
|
1151
|
+
df_A = mean_neighborhood_after_event(df_A, neigh_col, event_time_col, metrics=['inclusive','intermediate'])
|
|
1152
|
+
|
|
1153
|
+
df_A.to_pickle(path_A.replace('.csv','.pkl'))
|
|
1154
|
+
if not population[0]==population[1]:
|
|
1155
|
+
df_B.to_pickle(path_B.replace('.csv','.pkl'))
|
|
1156
|
+
|
|
1157
|
+
unwanted = df_A.columns[df_A.columns.str.startswith('neighborhood_')]
|
|
1158
|
+
df_A2 = df_A.drop(columns=unwanted)
|
|
1159
|
+
df_A2.to_csv(path_A, index=False)
|
|
1160
|
+
|
|
1161
|
+
if not population[0]==population[1]:
|
|
1162
|
+
unwanted = df_B.columns[df_B.columns.str.startswith('neighborhood_')]
|
|
1163
|
+
df_B_csv = df_B.drop(unwanted, axis=1, inplace=False)
|
|
1164
|
+
df_B_csv.to_csv(path_B,index=False)
|
|
1165
|
+
|
|
1166
|
+
if return_tables:
|
|
1167
|
+
return df_A, df_B
|
|
1168
|
+
|
|
1169
|
+
|
|
702
1170
|
|
|
703
1171
|
# def mask_intersection_neighborhood(setA, labelsA, setB, labelsB, threshold_iou=0.5, viewpoint='B'):
|
|
704
1172
|
# # do whatever to match objects in A and B
|