celldetective 1.0.2.post1__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.
Files changed (56) hide show
  1. celldetective/__main__.py +2 -2
  2. celldetective/events.py +2 -44
  3. celldetective/filters.py +4 -5
  4. celldetective/gui/__init__.py +1 -1
  5. celldetective/gui/analyze_block.py +37 -10
  6. celldetective/gui/btrack_options.py +24 -23
  7. celldetective/gui/classifier_widget.py +62 -19
  8. celldetective/gui/configure_new_exp.py +32 -35
  9. celldetective/gui/control_panel.py +115 -81
  10. celldetective/gui/gui_utils.py +674 -396
  11. celldetective/gui/json_readers.py +7 -6
  12. celldetective/gui/layouts.py +755 -0
  13. celldetective/gui/measurement_options.py +168 -487
  14. celldetective/gui/neighborhood_options.py +322 -270
  15. celldetective/gui/plot_measurements.py +1114 -0
  16. celldetective/gui/plot_signals_ui.py +20 -20
  17. celldetective/gui/process_block.py +449 -169
  18. celldetective/gui/retrain_segmentation_model_options.py +27 -26
  19. celldetective/gui/retrain_signal_model_options.py +25 -24
  20. celldetective/gui/seg_model_loader.py +31 -27
  21. celldetective/gui/signal_annotator.py +2326 -2295
  22. celldetective/gui/signal_annotator_options.py +18 -16
  23. celldetective/gui/styles.py +16 -1
  24. celldetective/gui/survival_ui.py +61 -39
  25. celldetective/gui/tableUI.py +60 -23
  26. celldetective/gui/thresholds_gui.py +68 -66
  27. celldetective/gui/viewers.py +596 -0
  28. celldetective/io.py +234 -23
  29. celldetective/measure.py +37 -32
  30. celldetective/neighborhood.py +495 -27
  31. celldetective/preprocessing.py +683 -0
  32. celldetective/scripts/analyze_signals.py +7 -0
  33. celldetective/scripts/measure_cells.py +12 -0
  34. celldetective/scripts/segment_cells.py +5 -0
  35. celldetective/scripts/track_cells.py +11 -0
  36. celldetective/signals.py +221 -98
  37. celldetective/tracking.py +0 -1
  38. celldetective/utils.py +178 -36
  39. celldetective-1.1.0.dist-info/METADATA +305 -0
  40. celldetective-1.1.0.dist-info/RECORD +80 -0
  41. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.0.dist-info}/top_level.txt +1 -0
  42. tests/__init__.py +0 -0
  43. tests/test_events.py +28 -0
  44. tests/test_filters.py +24 -0
  45. tests/test_io.py +70 -0
  46. tests/test_measure.py +141 -0
  47. tests/test_neighborhood.py +70 -0
  48. tests/test_segmentation.py +93 -0
  49. tests/test_signals.py +135 -0
  50. tests/test_tracking.py +164 -0
  51. tests/test_utils.py +71 -0
  52. celldetective-1.0.2.post1.dist-info/METADATA +0 -221
  53. celldetective-1.0.2.post1.dist-info/RECORD +0 -66
  54. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.0.dist-info}/LICENSE +0 -0
  55. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.0.dist-info}/WHEEL +0 -0
  56. {celldetective-1.0.2.post1.dist-info → celldetective-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -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
- valid_counts_intermediate = group.loc[group['FRAME']<=event_time,'intermediate_count_s1_'+neigh_col].to_numpy()
617
- valid_counts_inclusive = group.loc[group['FRAME']<=event_time,'inclusive_count_s1_'+neigh_col].to_numpy()
618
- valid_counts_exclusive = group.loc[group['FRAME']<=event_time,'exclusive_count_s1_'+neigh_col].to_numpy()
619
-
620
- if len(valid_counts_intermediate[valid_counts_intermediate==valid_counts_intermediate])>0:
621
- neigh_table.loc[indices, f'mean_count_intermediate_{neigh_col}_before_event'] = np.nanmean(valid_counts_intermediate)
622
- if len(valid_counts_inclusive[valid_counts_inclusive==valid_counts_inclusive])>0:
623
- neigh_table.loc[indices, f'mean_count_inclusive_{neigh_col}_before_event'] = np.nanmean(valid_counts_inclusive)
624
- if len(valid_counts_exclusive[valid_counts_exclusive==valid_counts_exclusive])>0:
625
- neigh_table.loc[indices, f'mean_count_exclusive_{neigh_col}_before_event'] = np.nanmean(valid_counts_exclusive)
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 or (event_time<0.):
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 len(valid_counts_intermediate[valid_counts_intermediate==valid_counts_intermediate])>0:
692
- neigh_table.loc[indices, f'mean_count_intermediate_{neigh_col}_after_event'] = np.nanmean(valid_counts_intermediate)
693
- if len(valid_counts_inclusive[valid_counts_inclusive==valid_counts_inclusive])>0:
694
- neigh_table.loc[indices, f'mean_count_inclusive_{neigh_col}_after_event'] = np.nanmean(valid_counts_inclusive)
695
- if len(valid_counts_exclusive[valid_counts_exclusive==valid_counts_exclusive])>0:
696
- neigh_table.loc[indices, f'mean_count_exclusive_{neigh_col}_after_event'] = np.nanmean(valid_counts_exclusive)
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