epicure.tracking

Tracking options and handles track informations.

This class offers options to perform tracking within EpiCure. It also handles the tracking information, wether the tracking was performed locally or imported.

It contains the graph that controls the mother-daughter relationship of the cells. The format of the graph is a dictionnary of daughter->mother (key->value) pairs. This is to be compatible with the Laptrack output format and napari Tracks layer.

The track_data table contains the information of all cell center position at each frame. The Tracks layer is slow to update so it is updated only when clicking on Update tracks in the viewer, but the object track_data is keeping the updated information.

   1"""
   2    **Tracking options and handles track informations.**
   3
   4    This class offers options to perform tracking within EpiCure.
   5    It also handles the tracking information, wether the tracking was performed locally or imported.
   6
   7    It contains the `graph` that controls the mother-daughter relationship of the cells. The format of the graph is a dictionnary of daughter->mother (key->value) pairs. This is to be compatible with the Laptrack output format and napari Tracks layer.
   8
   9    The `track_data` table contains the information of all cell center position at each frame. The `Tracks` layer is slow to update so it is updated only when clicking on `Update tracks` in the viewer, but the object `track_data` is keeping the updated information.
  10"""
  11
  12from qtpy.QtWidgets import QVBoxLayout, QWidget # type: ignore
  13from epicure.laptrack_centroids import LaptrackCentroids
  14import epicure.Utils as ut
  15laptrack_over = False
  16try:    
  17    from epicure.laptrack_overlaps import LaptrackOverlaps
  18    laptrack_over = True
  19except ImportError:
  20    print("Laptrack overlap not available in your laptrack version. Only the centroid option will be proposed. Update laptrack to 0.16 to have it")
  21    pass
  22import laptrack
  23if ut.version_above(laptrack, "0.16"):
  24    try:    
  25        from laptrack.data_conversion import split_merge_df_to_napari_graph as to_napari_graph# type: ignore
  26    except ImportError:
  27        from laptrack.data_conversion import convert_split_merge_df_to_napari_graph as to_napari_graph # type: ignore
  28else:
  29    try:    
  30        from laptrack.data_conversion import convert_split_merge_df_to_napari_graph as to_napari_graph # type: ignore
  31    except ImportError:
  32        from laptrack.data_conversion import split_merge_df_to_napari_graph as to_napari_graph # type: ignore
  33from napari.utils import progress # type: ignore
  34from skimage.transform import warp
  35from skimage.registration import optical_flow_ilk
  36import pandas as pd
  37import numpy as np
  38import scipy.ndimage as ndi
  39import epicure.epiwidgets as wid
  40from joblib import Parallel, delayed
  41
  42class Tracking(QWidget):
  43    """
  44        Handles tracking of cells, track operations with the Tracks layer
  45    """
  46    def __init__(self, napari_viewer, epic):
  47        super().__init__()
  48        self.viewer = napari_viewer
  49        self.epicure = epic
  50        self.graph = None      ## init 
  51        self.tracklayer = None      ## track layer with information (centroids, labels, tree..)
  52        self.track_data = None ## keep the updated data, and update the layer only from time to time (slow to do)
  53        self.tracklayer_name = "Tracks"  ## name of the layer containing tracks
  54        self.nframes = self.epicure.nframes
  55        self.properties = ["label", "centroid"]
  56
  57        layout = QVBoxLayout()
  58        
  59        ## Add update track button 
  60        self.track_update = wid.add_button( "Update tracks display", self.update_track_layer, "Update the Track layer with the changements made since the last update" )
  61        layout.addWidget(self.track_update)
  62        
  63        ## Correct track button 
  64        #track_reset = wid.add_button( "Correct track data", self.reset_tracks, "Correct the track data after some track was lost" )
  65        #layout.addWidget(track_reset)
  66
  67        ## Method specific
  68        track_method, self.track_choice = wid.list_line( "Tracking method", "Choose the tracking method to use and display its parameter", func=None )
  69        layout.addWidget(self.track_choice)
  70        
  71        self.track_choice.addItem("Laptrack-Centroids")
  72        self.create_laptrack_centroids()
  73        layout.addWidget(self.gLapCentroids)
  74
  75        if laptrack_over: 
  76            self.track_choice.addItem("Laptrack-Overlaps")
  77            self.create_laptrack_overlap()
  78            layout.addWidget(self.gLapOverlap)
  79        else:
  80            self.min_iou = None
  81            self.split_cost = None
  82            self.merg_cost = None
  83
  84        drift_layout, self.drift_correction, self.drift_radius = wid.check_value( check="With drift correction", checked=False, value=str(50), descr="Taking into account local drift in tracking calculations") 
  85        layout.addLayout( drift_layout )
  86        
  87        self.track_go = wid.add_button( "Track", self.do_tracking, "Launch the tracking with the current parameter. Can take time" )
  88        layout.addWidget(self.track_go)
  89        self.setLayout(layout)
  90
  91        ## General tracking options
  92        frame_line, self.frame_range, self.range_group = wid.checkgroup_help( "Track only some frames", False, "Option to track only a given range of frames", None ) 
  93        self.frame_range.clicked.connect( self.show_frame_range )
  94        range_layout = QVBoxLayout()
  95        ntrack, self.start_frame = wid.ranged_value_line( "Track from frame:", 0, self.nframes-1, 1, 0, "Set first frame to begin tracking" )
  96        range_layout.addLayout(ntrack)
  97        
  98        entrack, self.end_frame = wid.ranged_value_line( "Until frame:", 1, self.nframes-1, 1, self.nframes-1, "Set the last frame unitl which to track" )
  99        range_layout.addLayout(entrack)
 100        self.start_frame.valueChanged.connect( self.changed_start )
 101        self.end_frame.valueChanged.connect( self.changed_end )
 102        
 103        self.range_group.setLayout( range_layout )
 104        layout.addWidget( self.frame_range )
 105        layout.addWidget( self.range_group )
 106        
 107        self.show_frame_range()
 108        self.show_trackoptions()
 109        self.track_choice.currentIndexChanged.connect(self.show_trackoptions)
 110        
 111
 112    def show_frame_range( self ):
 113        """ Show/Hide frame range options """
 114        self.range_group.setVisible( self.frame_range.isChecked() )
 115        
 116    #### settings
 117
 118    def get_current_settings( self ):
 119        """ Get current settings to save as preferences """
 120        settings = {}
 121        settings["Track method"] = self.track_choice.currentText() 
 122        settings["Add feat"] = self.check_penalties.isChecked()
 123        settings["Max distance"] = self.max_dist.text()
 124        settings["Splitting cost"] = self.splitting_cost.text()
 125        settings["Merging cutoff"] = self.merging_cost.text()
 126        settings["Min IOU"] = self.min_iou.text()
 127        settings["Over split"] = self.split_cost.text()
 128        settings["Over merge"] = self.merg_cost.text()
 129        return settings
 130
 131    def apply_settings( self, settings ):
 132        """ Set the parameters/current display from the prefered settings """
 133        for setty, val in settings.items():
 134            if setty == "Track method":
 135                self.track_choice.setCurrentText( val )
 136            if setty == "Add feat":
 137                self.check_penalties.setChecked( val )
 138            if setty == "Max distance":
 139                self.max_dist.setText( val )
 140            if setty == "Splitting cost":
 141                self.splitting_cost.setText( val )
 142            if setty == "Merging cutoff":
 143                self.merging_cost.setText( val )
 144            if laptrack_over:
 145                if setty == "Min IOU":
 146                    self.min_iou.setText( val )
 147                if setty == "Over split":
 148                    self.split_cost.setText( val )
 149                if setty == "Over merge":
 150                    self.merg_cost.setText( val )
 151            
 152    ##########################################
 153    #### Tracks layer and function
 154
 155    def reset( self ):
 156        """ Reset Tracks layer and data """
 157        self.graph = None
 158        self.track_data = None
 159        ut.remove_layer( self.viewer, "Tracks" )
 160
 161    def init_tracks(self, track_table=None, track_prop=None ):
 162        """ Add a track layer with the new tracks """
 163        if track_table is None:
 164            track_table, track_prop = self.create_tracks()
 165        #print(track_table)
 166        
 167        ## plot tracks
 168        if len(track_table) > 0:
 169            self.clear_graph()
 170            self.viewer.add_tracks(
 171                track_table,
 172                graph=self.graph, 
 173                name=self.tracklayer_name,
 174                properties = track_prop,
 175                scale = self.viewer.layers["Segmentation"].scale,
 176                )
 177            self.viewer.layers[self.tracklayer_name].visible=True
 178            self.viewer.layers[self.tracklayer_name].color_by="track_id"
 179            ut.set_active_layer(self.viewer, "Segmentation")
 180            self.tracklayer = self.viewer.layers[self.tracklayer_name]
 181            self.track_data = self.tracklayer.data
 182            #self.track.display_id = True
 183            self.color_tracks_as_labels()
 184
 185    def color_tracks_as_labels(self):
 186        """ Color the tracks the same as the label layer """
 187        ## must color it manually by getting the Label layer colors for each track_id
 188        cols = np.zeros((len(self.tracklayer.data[:,0]),4))
 189        for i, tr in enumerate(self.tracklayer.data[:,0]):
 190            cols[i] = (self.epicure.seglayer.get_color(tr))
 191        self.tracklayer._track_colors = cols
 192        self.tracklayer.events.color_by()
 193    
 194    def color_tracks_by_lineage(self):
 195        """ Color the tracks by their lineage (daughters same colors as parents) """
 196        ## must color it manually by getting the Label layer colors for each track_id
 197        cols = np.zeros((len(self.tracklayer.data[:,0]),4))
 198        for i, tr in enumerate(self.tracklayer.data[:,0]):
 199            ## find the parent cell,n going up the tree until no more parent
 200            while tr in self.graph.keys():
 201                tr = self.graph_parent( tr )
 202            cols[i] = (self.epicure.seglayer.get_color(tr))
 203        self.tracklayer._track_colors = cols
 204        self.tracklayer.events.color_by()
 205
 206    def graph_parent( self, ind ):
 207        """ Get the value of the parent from the graph """
 208        if ind not in self.graph.keys():
 209            return None
 210        if isinstance(self.graph[ind], list):
 211            return self.graph[ind][0]
 212        return self.graph[ind]
 213
 214    def replace_tracks(self, track_df):
 215        """ Replace all tracks based on the dataframe """
 216        if not self.undrifted and self.drift_correction.isChecked():
 217            ## recalculate the label centroids as it was corrected for drift
 218            track_table, track_prop = self.create_tracks()
 219        else:
 220            track_table, track_prop = self.build_tracks( track_df )
 221        self.tracklayer.data = track_table
 222        self.track_data = self.tracklayer.data
 223        self.tracklayer.properties = track_prop
 224        self.tracklayer.refresh()
 225        self.color_tracks_as_labels()
 226
 227    def reset_tracks(self):
 228        """ Reset tracks and reload them from labels """
 229        ut.remove_layer(self.viewer, "Tracks")
 230        self.init_tracks()
 231
 232    def update_track_layer(self):
 233        """ Update the track layer (slow) """
 234        self.viewer.window._status_bar._toggle_activity_dock(True)
 235        progress_bar = progress(total=1)
 236        progress_bar.set_description( "Updating track layer" )
 237        self.tracklayer.data = self.track_data
 238        progress_bar.close()
 239        self.color_tracks_as_labels()
 240        self.viewer.window._status_bar._toggle_activity_dock(False)
 241
 242    def measure_intensity_features( self, feat, intimg=None, frames=None ):
 243        """ Measure mean value of a feature in a track """
 244        if ( intimg is not None ):
 245            if frames is None:
 246                tracks = self.get_track_list()
 247                seg = self.epicure.seg
 248                iimg = intimg
 249            else:
 250                tracks = self.get_tracks_list_frames( frames )
 251                seg = self.epicure.seg[frames]
 252                iimg = intimg[frames]
 253        if feat == "intensity_mean":
 254            mean_intensities = ndi.mean( iimg, seg, tracks )
 255            return tracks, mean_intensities
 256        if feat == "intensity_sum":
 257            sum_intensities = ndi.sum( iimg, seg, tracks )
 258            return tracks, sum_intensities
 259        if feat == "intensity_max":
 260            sum_intensities = ndi.maximum( iimg, seg, tracks )
 261            return tracks, sum_intensities
 262        if feat == "intensity_min":
 263            sum_intensities = ndi.minimum( iimg, seg, tracks )
 264            return tracks, sum_intensities
 265        if feat == "intensity_median":
 266            sum_intensities = ndi.median( iimg, seg, tracks )
 267            return tracks, sum_intensities
 268        print( "Mean feature on track not implemented" )
 269        return None
 270
 271    def measure_track_features( self, track_id, scaling=False ):
 272        """ Measure features (length, total displacement...) of given track """
 273        features = {}
 274        track = self.get_track_data( track_id )
 275        if track.shape[0] == 0:
 276            return features
 277        track = track[track[:,1].argsort()]
 278        start = int(np.min(track[:,1]))
 279        end = int(np.max(track[:,1]))
 280        temp_unit = ""
 281        vel_unit = ""
 282        disp_unit = ""
 283        temp_scale = 1
 284        vel_scale = 1
 285        disp_scale = 1
 286        if scaling:
 287            temp_unit = "_"+self.epicure.epi_metadata["UnitT"]
 288            vel_unit = "_"+self.epicure.epi_metadata["UnitXY"]+"/"+self.epicure.epi_metadata["UnitT"]
 289            disp_unit = "_"+self.epicure.epi_metadata["UnitXY"]
 290            temp_scale = self.epicure.epi_metadata["ScaleT"]
 291            vel_scale = self.epicure.epi_metadata["ScaleXY"]/self.epicure.epi_metadata["ScaleT"]
 292            disp_scale = self.epicure.epi_metadata["ScaleXY"]
 293        features["Label"] = track_id
 294        features["TrackDuration"+temp_unit] = (end - start + 1)*temp_scale
 295        features["TrackStart"+temp_unit] = start * temp_scale
 296        features["TrackEnd"+temp_unit] = end * temp_scale
 297        features["NbGaps"] = end - start + 1 - len(track)
 298        if (end-start) == 0:
 299            ## only one frame
 300            features["TotalDisplacement"+disp_unit] = None
 301            features["NetDisplacement"+disp_unit] = None
 302            features["Straightness"] = None
 303            features["MeanVelocity"+vel_unit] = None
 304        else:
 305            features["TotalDisplacement"+disp_unit] = ut.total_distance( track[:,2:4] ) * disp_scale
 306            features["NetDisplacement"+disp_unit] = ut.net_distance( track[:,2:4] ) * disp_scale
 307            features["MeanVelocity"+vel_unit] = np.mean( ut.velocities( track[:,1:4] ) ) * vel_scale 
 308            if features["TotalDisplacement"+disp_unit] > 0:
 309                features["Straightness"] = features["NetDisplacement"+disp_unit]/features["TotalDisplacement"+disp_unit]
 310            else:
 311                features["Straightness"] = None
 312        return features
 313
 314    def measure_speed( self, track_id ):
 315        """ Returns the velocities of the track """
 316        track = self.get_track_data( track_id )
 317        if track.shape[0] == 0:
 318            return None 
 319        track = track[track[:,1].argsort()]
 320        return ut.velocities( track[:,1:4] )
 321
 322    def measure_features( self, track_id, features ):
 323        """ Measure features along all the track """
 324        mask = self.epicure.get_mask( track_id )
 325        res = {}
 326        for feat in features:
 327            res[feat] = []
 328        for frame in mask:
 329            props = ut.labels_properties( frame )
 330            if len(props) > 0:
 331                if "Area" in features:
 332                    res["Area"].append( props[0].area )
 333                if "Hull" in features:
 334                    res["Hull"].append( props[0].area_convex )
 335                if "Elongation" in features:
 336                    res["Elongation"].append( props[0].axis_major_length )
 337                if "Eccentricity" in features:
 338                    res["Eccentricity"].append( props[0].eccentricity )
 339                if "Perimeter" in features:
 340                    res["Perimeter"].append( props[0].perimeter )
 341                if "Solidity" in features:
 342                    res["Solidity"].append( props[0].solidity )
 343        return res
 344
 345    def measure_specific_feature( self, track_id, featureName ):
 346        """ Measure some specific feature """
 347        if featureName == "Similarity":
 348            import skimage.metrics as imetrics
 349            movie = self.epicure.get_label_movie( track_id, extend=1.5 )
 350            sim_scores = []
 351            for i in range(0, len(movie)-1):
 352                score = imetrics.normalized_mutual_information( movie[i], movie[i+1] ) 
 353                sim_scores.append(score)
 354            return sim_scores
 355
 356    def measure_labels(self, segimg):
 357        """ Get the dataframe of the labels in the segmented image """
 358        resdf = None
 359        for iframe, frame in progress(enumerate(segimg)):
 360            frame_table = ut.labels_to_table( frame, iframe )
 361            if resdf is None:
 362                resdf = pd.DataFrame(frame_table)
 363            else:
 364                resdf = pd.concat([resdf, pd.DataFrame(frame_table)])
 365        return resdf
 366
 367    def add_track_frame(self, label, frame, centroid, tree=None, group=None):
 368        """ Add one frame to the track """
 369        new_frame = np.array([label, frame, centroid[0], centroid[1]])
 370        self.track_data = np.vstack((self.track_data, new_frame))
 371            
 372    def get_track_list(self):
 373        """ Get list of unique track_ids """
 374        return np.unique( self.track_data[:,0] )
 375    
 376    def get_tracks_list_frames( self, frames ):
 377        """ Return list of tracks present on list of frames """
 378        return np.unique( self.track_data[ np.isin( self.track_data[:,1], frames), 0] ) 
 379    
 380    def get_tracks_on_frame( self, tframe ):
 381        """ Return list of tracks present on given frame """
 382        return np.unique( self.track_data[ self.track_data[:,1]==tframe, 0] ) 
 383
 384    def has_track(self, label):
 385        """ Test if track label is present """
 386        return label in self.track_data[:,0]
 387    
 388    def has_tracks(self, labels):
 389        """ Test if track labels are present """
 390        return np.isin( labels, self.track_data[:,0] )
 391    
 392    def nb_points(self):
 393        """ Number of points in the tracks """
 394        return self.track_data.shape[0]
 395
 396    def nb_tracks(self):
 397        """ Return number of tracks """
 398        #return self.track._manager.__len__()
 399        return len(self.get_track_list())
 400
 401    def gaped_track(self, track_id):
 402        """ Check if there is a gap (missing frame) in a track """
 403        indexes = self.get_track_indexes(track_id)
 404        if len(indexes) <= 0:
 405            return False
 406        track_frames = self.track_data[indexes,1]
 407        return ((np.max(track_frames)-np.min(track_frames)+1) > len(track_frames) )
 408
 409    def gap_frames(self, track_id):
 410        """ Returns the frame(s) at which the gap(s) are """
 411        track_frames = self.get_track_column( track_id, "frame" )
 412        gaps = []
 413        if len( track_frames ) > 0:
 414            min_frame = int( np.min(track_frames) )
 415            max_frame = int( np.max(track_frames) )
 416            gaps = np.setdiff1d( np.arange(min_frame+1, max_frame), track_frames ).tolist()
 417            if len(gaps) > 0:
 418                gaps.sort()
 419        return gaps
 420            
 421    def check_gap(self, tracks=None, verbose=None):
 422        """ Check if there is a track with a gap, flag it if yes """
 423        if tracks is None:
 424            tracks = self.get_track_list()
 425        gaped = []
 426        for track in tracks:
 427            if self.gaped_track( track ):
 428                gaped.append(track)
 429        if verbose is None:
 430            verbose = self.epicure.verbose
 431        if verbose > 0 and len(gaped)>0:
 432            ut.show_warning("Gap in track(s) "+str(gaped)+"\n"
 433            +"Consider doing sanity_check in Editing onglet to fix it")
 434        return gaped
 435
 436    def get_track_indexes(self, track_id):
 437        """ Get indexes of track_id tracks position in the arrays """
 438        if isinstance( track_id,  int ):
 439            return (np.flatnonzero( self.track_data[:,0] == track_id ) )
 440        return (np.flatnonzero( np.isin( self.track_data[:,0], track_id ) ) )
 441    
 442    def get_track_indexes_onframes( self, track_id, frames ):
 443        """ Get indexes of track_id tracks position in the arrays """
 444        if isinstance( frames, int ):
 445            frames = [frames]
 446        if isinstance( track_id,  int ):
 447            return (np.flatnonzero( (self.track_data[:,0] == track_id) * np.isin( self.track_data[:,1], frames) ) )
 448        return (np.flatnonzero( np.isin( self.track_data[:,0], track_id ) * np.isin( self.track_data[:,1], frames) ) )
 449
 450    def get_track_indexes_from_frame(self, track_id, frame):
 451        """ Get indexes of track_id tracks position in the arrays from the given frame """
 452        if type(track_id) == int:
 453            return (np.argwhere( (self.track_data[:,0] == track_id)*(self.track_data[:,1]>= frame) )).flatten()
 454        return (np.argwhere( np.isin( self.track_data[:,0], track_id )*(self.track_data[:,1]>= frame) )).flatten()
 455
 456    def get_index(self, track_id, frame ):
 457        """ Get index of track_id at given frame """
 458        if np.isscalar(track_id):
 459            track_id = [track_id]
 460        return np.argwhere( (np.isin(self.track_data[:,0], track_id))*(self.track_data[:,1] == frame) )
 461
 462    def get_small_tracks(self, max_length=1):
 463        """ Get tracks smaller than the given threshold """
 464        labels = []
 465        lengths = []
 466        positions = []
 467        for lab in self.get_track_list():
 468            indexes = self.get_track_indexes(lab)
 469            length = len(indexes)
 470            if length <= max_length:
 471                pos = self.mean_position( indexes, only_first=False )
 472                labels.append(lab)
 473                lengths.append(length)
 474                positions.append(pos)
 475        return labels, lengths, positions
 476
 477    def get_track_data(self, track_id):
 478        """ Return the data of track track_id """
 479        indexes = self.get_track_indexes( track_id )
 480        track = self.track_data[indexes,]
 481        return track
 482    
 483    def get_track_column( self, track_id, column ):
 484        """ Return the chosen column (frame, x, y, label) of track track_id """
 485        indexes = self.get_track_indexes( track_id )
 486        if column == "frame":
 487            return self.track_data[indexes, 1]
 488        if column == "label":
 489            return self.track_data[indexes, 0]
 490        if column == "pos":
 491            return self.track_data[indexes, 2:4]
 492        if column == "fullpos":
 493            return self.track_data[indexes, 1:4]
 494        track = self.track_data[indexes]
 495        return track
 496
 497    def get_frame_data( self, track_id, ind ):
 498        """ Get ind-th data of track track_id """
 499        track = self.get_track_data( track_id )
 500        return track[ind]
 501    
 502    def get_middle_position( self, track_id, framea, frameb ):
 503        """ Get track position in middle of frame a and frame b """
 504        inda = self.get_index( track_id, framea ) 
 505        indb = self.get_index( track_id, frameb )
 506        return self.mean_position( np.ravel( np.vstack((inda, indb)) ), only_first=False )
 507
 508    def get_position( self, track_id, frame ):
 509        """ Get position of the track at given frame """
 510        ind = self.get_index( track_id, frame )
 511        ind = ind.flatten()[0] ## ensure it's single element
 512        x,y = self.track_data[ind,2], self.track_data[ind,3]
 513        return [int(x), int(y)]
 514
 515    def get_full_position( self, track_id, frame ):
 516        """ Get position of the track at given frame, with the frame itself """
 517        ind = self.get_index( track_id, frame )
 518        ind = ind.flatten()[0] ## ensure it's single element
 519        x,y = self.track_data[ind,2], self.track_data[ind,3]
 520        return [frame,x,y]
 521
 522    def mean_position(self, indexes, only_first=False):
 523        """ Mean positions of tracks at indexes """
 524        if len(indexes) <= 0:
 525            return None
 526        track = self.track_data[indexes,]
 527        ## keep only the first frame of the tracks
 528        if only_first:
 529            min_frame = np.min(track[:,1])
 530            track = track[track[:,1]==min_frame,]
 531        return ( int(np.mean(track[:,1])), int(np.mean(track[:,2])), int(np.mean(track[:,3])) )
 532
 533    def get_first_frame(self, track_id):
 534        """ Returns first frame where track_id is present """
 535        track = self.get_track_data( track_id )
 536        if len(track) <= 0:
 537            return None
 538        return int( np.min(track[:,1]) )
 539
 540    def is_in_frame( self, track_id, frame ):
 541        """ Returns if track_id is present at given frame """
 542        track = self.get_track_data( track_id )
 543        if len(track) > 0:
 544            return frame in track[:,1]
 545        return False
 546    
 547    def get_last_frame(self, track_id):
 548        """ Returns last frame where track_id is present """
 549        track = self.get_track_data( track_id )
 550        if len(track) > 0:
 551            return int(np.max(track[:,1]))
 552        return None
 553    
 554    def get_extreme_frames(self, track_id):
 555        """ Returns the first and last frames where track_id is present """
 556        track = self.get_track_data( track_id )
 557        if track.shape[0] > 0:
 558            return (int(np.min(track[:,1])), int(np.max(track[:,1])) )
 559        return None, None
 560
 561    def get_mean_position(self, track_id, only_first=False):
 562        """ Get mean position of the track """
 563        indexes = self.get_track_indexes( track_id )
 564        return self.mean_position( indexes, only_first )
 565
 566    def update_centroid(self, track_id, frame, ind=None, cx=None, cy=None):
 567        """ Update track at given frame """
 568        if ind is None:
 569            ind = self.get_index( track_id, frame )
 570        if cx is None:
 571            prop = ut.getPropLabel( self.epicure.seg[frame], track_id )
 572            self.track_data[ind, 2:4] = prop.centroid[1]
 573        else:
 574            self.track_data[ind, 2] = cx
 575            self.track_data[ind, 3] = cy
 576
 577    def replace_on_frames( self, tids, new_tids, frames ):
 578        """ Replace the id tid by new_tid in all given frames """
 579        ind = self.get_track_indexes_onframes( tids, frames )
 580        cur_track = np.copy(self.track_data[ind])
 581        new_ids = np.repeat(-1, len(ind))
 582        for tid, new_tid in zip(tids, new_tids):
 583            self.update_graph_frames( tid, cur_track[cur_track[:,0]==tid,1] )
 584            new_ids[cur_track[:,0]==tid] = new_tid
 585        self.track_data[ind, 0] = new_ids
 586        
 587    def swap_frame_id(self, tid, otid, frame):
 588        """ Swap the ids of two tracks at frame """
 589        ind = int(self.get_index(tid, frame))
 590        oind = int(self.get_index(otid, frame))
 591        ## check if one of the label is an extreme of a track and potentially in the graph
 592        for track_index in [tid, otid]:
 593            min_frame, max_frame = self.get_extreme_frames( track_index )
 594            if (min_frame == frame) or (max_frame == frame):
 595                self.update_graph( track_index, frame )
 596        self.track_data[[ind, oind],0] = [otid, tid]
 597
 598    def update_track_on_frame(self, track_ids, frame):
 599        """ Update (add or modify) tracks at given frame """
 600        frame_table = ut.labels_table( labimg = np.where(np.isin(self.epicure.seg[frame], track_ids), self.epicure.seg[frame], 0), properties=self.properties )
 601        for x, y, tid in zip(frame_table["centroid-0"], frame_table["centroid-1"], frame_table["label"]):
 602            index = self.get_index(tid, frame)
 603            if len(index) > 0:
 604                self.update_centroid( tid, frame, index, int(x), int(y) )
 605            else:
 606                cur_cell = np.array( [[tid, frame, int(x), int(y)]] )
 607                self.track_data = np.append(self.track_data, cur_cell, axis=0)
 608
 609    def add_tracks_fromindices( self, indices, track_ids ):
 610        """ Add tracks of given track ids from the indices"""
 611        new_data = np.empty( (0,4), int )
 612        for tid in np.unique(track_ids):
 613            keep = track_ids == tid 
 614            for frame in np.unique( indices[0][keep] ):
 615                cent0 = np.mean( indices[1][keep] ) 
 616                cent1 = np.mean( indices[2][keep] ) 
 617                new_data = np.append( new_data, np.array([[tid, frame, int(cent0), int(cent1)]]), axis=0 )
 618        self.track_data = np.append( self.track_data, new_data, axis=0)
 619    
 620    def add_one_frame(self, track_ids, frame, refresh=True):
 621        """ Add one frame from track """
 622        for tid in track_ids:
 623            frame_table = ut.labels_table( np.uint8(self.epicure.seg[frame]==tid), properties=self.properties ) 
 624            cur_cell = np.array( [tid, frame, int(frame_table["centroid-0"]), int(frame_table["centroid-1"])], dtype=np.uint32 )
 625            cur_cell = np.expand_dims(cur_cell, axis=0)
 626            self.track_data = np.append(self.track_data, cur_cell, axis=0)
 627
 628    def remove_one_frame( self, track_id, frame, handle_gaps=False, refresh=True ):
 629        """ 
 630        Remove one frame from track(s) 
 631        """
 632        inds = self.get_index( track_id, frame )
 633        if np.isscalar(track_id):
 634            track_id = [track_id]
 635        check_for_gaps = False
 636        for tid in track_id:
 637            ## removed frame is in the extremity of a track, can be in the graph
 638            first_frame, last_frame = self.get_extreme_frames( tid )
 639            if first_frame is None:
 640                continue
 641            if (first_frame == frame) or (last_frame == frame):
 642                self.update_graph( tid, frame )
 643            else:
 644                check_for_gaps = True
 645        self.track_data = np.delete( self.track_data, inds, axis=0 )
 646        ## gaps might have been created in the tracks, for now doesn't allow it so split the tracks
 647        if handle_gaps and check_for_gaps:
 648            gaped = self.check_gap( track_id, verbose=0 )
 649            if len(gaped) > 0:
 650                self.epicure.fix_gaps( gaped )
 651        
 652    def get_current_value(self, track_id, frame):
 653        ind = self.get_index(track_id, frame)
 654        centx, centy = self.track_data[ind, 2:4].astype(int).flatten()
 655        return self.epicure.seg[frame, centx, centy]
 656
 657    def clear_graph( self ):
 658        """ Check the state of the graph and removes non existing keys or values """
 659        if self.graph is None:
 660            return
 661        keys = list(self.graph.keys())
 662        for key in keys:
 663            if key not in self.track_data[:,0]:
 664                del self.graph[key]
 665            else:
 666                vals = self.graph[key]
 667                if isinstance(vals, list):
 668                    for val in vals:
 669                        if val not in self.track_data[:,0]:
 670                            del self.graph[key]
 671                            break
 672                else:
 673                    if vals not in self.track_data[:,0]:
 674                        del self.graph[key]
 675
 676    def set_graph(self, graph):
 677        """ Set the current graph (eg imported from TrackMate XML file) """
 678        self.graph = graph
 679        ## set the divisions from the graph
 680        self.epicure.inspecting.get_divisions()
 681
 682    def update_graph_frames( self, track_id, frames ):
 683        """ Update graph when one label was deleted at given frames """
 684        fframe = np.min(frames)
 685        lframe = np.max(frames)
 686        self.update_graph( track_id, fframe )
 687        self.update_graph( track_id, lframe )
 688
 689    def update_graph(self, track_id, frame):
 690        """ Update graph if deleted label was linked at that frame, assume keys are unique """
 691        if self.graph is not None:
 692            ## handles current node is last of his branch
 693            parents = self.last_in_graph( track_id, frame )
 694            current_label = self.get_current_value( track_id, frame )
 695            for parent in parents:
 696                if current_label == 0:
 697                    del self.graph[parent]
 698                else:
 699                    self.update_child( parent, track_id, current_label )
 700            ## handles when current track is first frame of a division
 701            if self.first_in_graph( track_id, frame ):
 702                if current_label == 0:
 703                    del self.graph[track_id]
 704                else:
 705                    self.update_key( track_id, current_label ) 
 706
 707    def update_child(self, parent, prev_key, new_key):
 708        """ Change the value of a key in the graph """
 709        if isinstance(self.graph[parent], list):
 710            self.graph[parent] = [new_key if val == prev_key else val for val in self.graph[parent]]
 711        else:
 712            if self.graph[parent] == prev_key:
 713                self.graph[parent] = new_key
 714
 715    def update_key(self, prev_key, new_key):
 716        """ Change the value of a key in the graph """
 717        self.graph[new_key] = self.graph.pop(prev_key)
 718
 719    def is_parent( self, cur_id ):
 720        """ Return if the current id is in the graph (as a parent, so in values) """
 721        if self.graph is None:
 722            return False
 723        return any( cur_id in vals if isinstance(vals, list) else cur_id in [vals] for vals in self.graph.values() )
 724
 725    def add_division( self, childa, childb, parent ):
 726        """ Add info of a division to the graph of divisions/merges """
 727        if self.graph is None:
 728            self.graph = {}
 729        self.graph.update({childa: [parent], childb: [parent]})
 730
 731    def remove_division( self, parent ):
 732        """ Remove a division event from the graph """
 733        self.graph = {key: vals for key, vals in self.graph.items() if not ( self.graph_parent(key) == parent )  }
 734
 735    def last_in_graph(self, track_id, frame=None, check_last=True):
 736        """ Check if given label and frame is the last of a branch, in the graph """
 737        if check_last:
 738            return [key for key, vals in self.graph.items() if track_id in (vals if isinstance(vals, list) else [vals]) and self.get_last_frame(track_id) == frame]
 739        return [key for key, vals in self.graph.items() if track_id in (vals if isinstance(vals, list) else [vals])]
 740
 741    def first_in_graph(self, track_id, frame=None, check_first=True):
 742        """ Check if the given label and frame is the first in the branch so the node in the graph """
 743        if check_first:
 744            return track_id in self.graph and self.get_first_frame(track_id) == frame
 745        return track_id in self.graph
 746
 747    def remove_on_frames( self, track_ids, frames ):
 748        """ Remove tracks with given id on specified frames """
 749        track_ids = track_ids.tolist()
 750        if 0 in track_ids:
 751            track_ids.remove(0)
 752        inds = self.get_track_indexes_onframes( track_ids, frames )
 753        for tid in track_ids:
 754            self.update_graph_frames( tid, frames )
 755        self.track_data = np.delete( self.track_data, inds, axis=0 )
 756
 757    def remove_tracks(self, track_ids):
 758        """ Remove track with given id """
 759        inds = self.get_track_indexes(track_ids)
 760        self.track_data = np.delete(self.track_data, inds, axis=0)
 761        self.remove_ids_from_graph( track_ids )
 762    
 763    def remove_ids_from_graph( self, track_ids ):
 764        """ Remove all ids from the graph """
 765        track_ids_set = set( track_ids )
 766        if self.graph is not None:
 767            self.graph = {
 768                key: vals for key, vals in self.graph.items()
 769                if (key not in track_ids_set) and ( not any( val in track_ids_set for val in (vals if isinstance(vals, list) else [vals])) )
 770            }
 771    
 772    def is_single_parent( self, cur_id ):
 773        """ Return if the current id is in the graph (as a single parent, not a merge) """
 774        if self.graph is None:
 775            return False
 776        return any( cur_id in [vals] if not isinstance(vals, list) else (cur_id in vals and len(vals)==1) for vals in self.graph.values() )
 777
 778       
 779    def build_tracks(self, track_df):
 780        """ Create tracks from dataframe (after tracking) """
 781        track = track_df[["track_id", "frame", "centroid-0", "centroid-1"]]
 782        #frame_prop = frame_table[["tree_id", "label", "nframes", "group"]]
 783        return np.array(track, int), None #dict(frame_prop)
 784
 785    def create_tracks(self):
 786        """ Create tracks from labels (without tracking) """
 787        #track_table = np.empty( (0,4), int )   
 788        labels = self.epicure.seg
 789        total = self.epicure.nframes
 790        if self.epicure.process_parallel:
 791            track_tables = Parallel( n_jobs=self.epicure.nparallel ) (
 792                delayed(ut.labels_to_table)(frame, iframe ) for iframe, frame in enumerate(labels)
 793            )
 794        else:
 795            track_tables = [ ut.labels_to_table( frame, iframe) for iframe, frame in progress(enumerate(labels), total=total) ]
 796        track_table = np.concatenate( [ tab for tab in track_tables if tab.shape[0] != 0 ], axis=0 ) # handle empty frame
 797        return track_table, None # track_prop
 798
 799    def add_track_features(self, labels):
 800        """ Add features specific to tracks (eg nframes) """
 801        nframes = np.zeros(len(labels), int)
 802        if self.epicure.verbose > 2:
 803            print("REPLACE BY COUNT METHOD")
 804        for track_id in np.unique(labels):
 805            cur_track = np.argwhere(labels == track_id)
 806            nframes[ list(cur_track) ] = len(cur_track)
 807        return nframes
 808    
 809
 810    ##########################################
 811    #### Tracking functions
 812
 813    def changed_start(self, i):
 814        """ Ensures that end frame > start frame """
 815        if i > self.end_frame.value():
 816            self.end_frame.setValue(i+1)
 817
 818    def changed_end(self, i):
 819        if i < self.start_frame.value():
 820            self.start_frame.setValue(i-1)
 821
 822    def find_parents(self, labels, twoframes):
 823        """ Find in the first frame the parents of labels from second frame """
 824        
 825        if self.track_choice.currentText() == "Laptrack-Centroids":
 826            return self.laptrack_centroids_twoframes(labels, twoframes, loose=True)
 827        
 828        if self.track_choice.currentText() == "Laptrack-Overlaps":
 829            return self.laptrack_overlaps_twoframes(labels, twoframes, loose=True)
 830        
 831
 832    def do_tracking(self):
 833        """ Start the tracking with the selected options """
 834        if self.frame_range.isChecked():
 835            start = self.start_frame.value()
 836            end = self.end_frame.value()
 837        else:
 838            start = 0
 839            end = self.nframes-1
 840        start_time = ut.start_time()
 841        self.viewer.window._status_bar._toggle_activity_dock(True)
 842        self.epicure.inspecting.reset_all_events()
 843        
 844        if self.track_choice.currentText() == "Laptrack-Centroids":
 845            if self.epicure.verbose > 1:
 846                print("Starting track with Laptrack-Centroids")
 847            self.laptrack_centroids( start, end )
 848            self.epicure.tracked = 1
 849        if self.track_choice.currentText() == "Laptrack-Overlaps":
 850            if self.epicure.verbose > 1:
 851                print("Starting track with Laptrack-Centroids")
 852            self.laptrack_overlaps( start, end )
 853            self.epicure.tracked = 1
 854        
 855        self.epicure.finish_update(contour=2)
 856        #self.epicure.reset_free_label()
 857        self.viewer.window._status_bar._toggle_activity_dock(False)
 858        if self.epicure.verbose > 0:
 859            ut.show_duration( start_time, header="Tracking done in " )
 860
 861    def show_trackoptions(self):
 862        self.gLapCentroids.setVisible(self.track_choice.currentText() == "Laptrack-Centroids")
 863        if laptrack_over:
 864            self.gLapOverlap.setVisible(self.track_choice.currentText() == "Laptrack-Overlaps")
 865
 866    def relabel_nonunique_labels(self, track_df):
 867        """ After tracking, some track can be splitted and get same label, fix that """
 868        inittids = np.unique(track_df["track_id"])
 869        labtracks = []
 870        saved_data = np.copy(self.epicure.seglayer.data)
 871        mframes = []
 872        tids = []
 873        used = np.unique( saved_data )
 874        all_labels = np.unique(track_df["label"])
 875        for tid in inittids:
 876            cdf = track_df[track_df["track_id"]==tid]
 877            #print(cdf)
 878            min_frame = np.min( cdf["frame"] )
 879            #labtrack = int( cdf["label"][cdf["frame"]==min_frame] )
 880            for lab in np.unique(cdf["label"]):
 881                labtracks.append(lab)
 882                mframes.append( min_frame )
 883                tids.append(tid)
 884        if len(labtracks) != len(np.unique(labtracks)):
 885            ## some labels are present several times
 886            used = used.tolist()
 887            for lab in all_labels :
 888                indexes = list(np.where(np.array(labtracks)==lab)[0])
 889                if len(indexes)>1:
 890                    minframes = [mframes[indy] for indy in range(len(labtracks)) if labtracks[indy]==lab]
 891                    indmin = indexes[ np.argmin( minframes ) ]
 892                    ## for the other(s), change the label
 893                    newvals = ut.get_free_labels( used, len(indexes) )
 894                    used = used + newvals
 895                    for i, ind in enumerate(indexes):
 896                        if ind != indmin:
 897                            tid = tids[ind]
 898                            newval = newvals[i]
 899                            track_df.loc[ (track_df["track_id"]==tid)  & (track_df["label"]==lab) , "label"] = newval
 900                            for frame in track_df["frame"][(track_df["track_id"]==tid) & (track_df["label"]==newval)]:
 901                                mask = (saved_data[frame]==lab)
 902                                self.epicure.seglayer.data[frame][mask] = newval
 903        
 904
 905    def relabel_trackids(self, track_df, splitdf, mergedf):
 906        """ Change the trackids to take the first label of each track """
 907        start_time = ut.start_time()
 908        new_trackids = track_df['track_id'].copy()
 909        new_splitdf = splitdf.copy()
 910        new_mergedf = mergedf.copy()
 911        
 912        unique_track_ids = np.unique(track_df['track_id'])
 913        if ut.version_python_minor(10):
 914            ## from python3.10, get futurewarning on groupby without group_keys and include_groups keywords
 915            first_labels = track_df.groupby('track_id', group_keys=False).apply(lambda x: x.loc[x['frame'].idxmin(), 'label'], include_groups=False).to_dict()
 916        else:
 917            first_labels = track_df.groupby('track_id').apply(lambda x: x.loc[x['frame'].idxmin(), 'label']).to_dict()
 918        
 919        for tid in unique_track_ids:
 920            newval = first_labels[tid]
 921            if tid != newval:
 922                new_trackids[track_df['track_id'] == tid] = newval
 923                if not new_splitdf.empty:
 924                    new_splitdf.loc[splitdf["parent_track_id"] == tid, "parent_track_id"] = newval
 925                    new_splitdf.loc[splitdf["child_track_id"] == tid, "child_track_id"] = newval
 926                if not new_mergedf.empty:
 927                    new_mergedf.loc[mergedf["parent_track_id"] == tid, "parent_track_id"] = newval
 928                    new_mergedf.loc[mergedf["child_track_id"] == tid, "child_track_id"] = newval
 929        if self.epicure.verbose > 1:
 930            ut.show_duration( start_time, header="Relabeling done in " )            
 931        return new_trackids, new_splitdf, new_mergedf
 932
 933    def change_labels(self, track_df):
 934        """ Change the labels at each frame according to tracks """
 935        for frame, frame_df in track_df.groupby("frame"):
 936            self.change_frame_labels(frame, frame_df)
 937
 938    def change_frame_labels(self, frame, frame_df):
 939        """ Change the labels at given frame according to tracks """
 940        track_ids = frame_df['track_id'].astype(int).values
 941        old_labels = frame_df["label"].astype(int).values
 942        seglayer = np.copy(self.epicure.seglayer.data[frame])
 943        for old_lab, new_lab in zip(old_labels, track_ids):
 944            mask = (seglayer==old_lab)
 945            self.epicure.seglayer.data[frame][mask] = new_lab
 946
 947    def label_to_dataframe( self, labimg, frame ):
 948        """ from label, get dataframe of centroids with properties """
 949        df = pd.DataFrame( ut.labels_table(labimg, properties=self.region_properties) )
 950        if df.shape[0] == 0:
 951            ## no labels in this frame
 952            return None
 953        df["frame"] = frame
 954        return df
 955    
 956    def optical_flow( self, img0, img1, radius ):
 957        """ Compute the optical flow between two images """
 958        v, u = optical_flow_ilk( img0, img1, radius=radius)
 959        return v, u
 960    
 961    def apply_flow( self, flowv, flowu, labimg ):
 962        """ Apply the calculated optical flow on a label image """
 963        nr, nc = labimg.shape
 964        rowc, colc = np.meshgrid( np.arange(nr), np.arange(nc), indexing="ij" )
 965        lab_reg = warp( labimg, np.array( [rowc+flowv, colc+flowu] ), order=0, mode="edge" )
 966        return lab_reg
 967    
 968    def labels_to_centroids( self, start_frame, end_frame ):
 969        """ Get centroids of each cell in dataframe """
 970        regionprops = [
 971            result
 972            for frame in range(start_frame, end_frame + 1)
 973            if (result := self.label_to_dataframe(self.epicure.seg[frame], frame)) is not None
 974        ]
 975        return pd.concat(regionprops)
 976    
 977    def labels_to_centroids_flow(self, start_frame, end_frame):
 978        """ Get centroids of each cell in dataframe """
 979        regionprops = []    
 980        radius = float( self.drift_radius.text() )
 981        if self.epicure.verbose > 1:
 982            if self.drift_correction.isChecked():
 983                print( "Apply drift correction to tracking with optical flow of radius "+str(radius) )
 984        prev_movie = None
 985        flow_v = None
 986        for frame in range(start_frame, end_frame+1):
 987            if self.drift_correction.isChecked():
 988                cur_movie = self.epicure.img[frame]
 989                if frame > start_frame:
 990                    v, u = self.optical_flow( prev_movie, cur_movie, radius )
 991                    if flow_v is None:
 992                        flow_v = v
 993                        flow_u = u
 994                    else:
 995                        flow_v = flow_v + v
 996                        flow_u = flow_u + u
 997                prev_movie = cur_movie
 998            clabel = self.epicure.seg[frame]  
 999            df = self.label_to_dataframe( clabel, frame )
1000            if flow_v is not None:
1001                c0 = np.array( np.floor( df["centroid-0"] ), dtype="uint8" )
1002                c1 = np.array( np.floor( df["centroid-1"] ), dtype="uint8" )
1003                df["centroid-0"] = df["centroid-0"] - flow_v[c0,c1]
1004                df["centroid-1"] = df["centroid-1"] - flow_u[c0,c1]
1005            regionprops.append(df)
1006        regionprops_df = pd.concat(regionprops)
1007        return regionprops_df
1008    
1009    def labels_flow(self, start_frame, end_frame ):
1010        """ Get registered label image corrected for optical flow """
1011        radius = float( self.drift_radius.text() )
1012        flow_v = None
1013        prev_movie = None
1014        res_labels = []
1015        for frame in range(start_frame, end_frame+1):
1016            cur_movie = self.epicure.img[frame]
1017            if prev_movie is not None:
1018                v, u = self.optical_flow( prev_movie, cur_movie, radius )
1019                if flow_v is None:
1020                    flow_v = v
1021                    flow_u = u
1022                else:
1023                    flow_v = flow_v + v
1024                    flow_u = flow_u + u
1025            prev_movie = cur_movie
1026            clabel = np.copy( self.epicure.seg[frame] ) 
1027            if flow_v is not None:         
1028                clabel = self.apply_flow( flow_v, flow_u, clabel )
1029            res_labels.append( clabel )
1030        res_labels = np.array(res_labels)
1031        return res_labels
1032
1033    def labels_ready(self, start_frame, end_frame, locked=True):
1034        """ Get labels of unlocked cells to track """
1035        if self.drift_correction.isChecked():
1036            return self.labels_flow( start_frame, end_frame )
1037        res_labels = self.epicure.seg[start_frame:end_frame+1] 
1038        return res_labels
1039    
1040    def label_frame_todf( self, frame ):
1041        """ For current frame, get label frame image then dataframe of centroids """
1042        clabel = self.epicure.seg[frame] #self.current_label_frame(frame)
1043        return self.label_to_dataframe( clabel, frame )
1044    
1045    def current_label_frame( self, frame ):
1046        """ For current frame, get label frame image """
1047        clabel = None
1048        #if self.track_only_in_roi.isChecked():
1049        #    clabel = self.epicure.only_current_roi(frame)
1050        if clabel is None:
1051            clabel = self.epicure.seg[frame]
1052        return clabel
1053
1054    def after_tracking( self, track_df, split_df, merge_df, progress_bar, indprogress ):
1055        """ Steps after tracking: get/show the graph from the track_df """
1056        if "frame_y" in track_df.keys():
1057            track_df["frame"] = track_df["frame_y"]
1058        graph = None
1059        progress_bar.set_description( "Update labels and tracks" )
1060        ## shift all by 1 so that doesn't start at 0
1061        if len(split_df) > 0:
1062            split_df[["parent_track_id"]] += 1
1063            split_df[["child_track_id"]] += 1
1064        if len(merge_df) > 0:
1065            merge_df[["parent_track_id"]] += 1
1066            merge_df[["child_track_id"]] += 1
1067        track_df[["track_id"]] += 1
1068       
1069        ## relabel if some track have the same label
1070        self.relabel_nonunique_labels(track_df)
1071        ## relabel track ids so that they are equal to the first label of the track
1072        newtids, split_df, merge_df = self.relabel_trackids( track_df, split_df, merge_df )
1073        track_df["track_id"] = newtids
1074        self.change_labels( track_df )
1075
1076        # create graph of division/merging
1077        self.graph = to_napari_graph(split_df, merge_df)
1078
1079        progress_bar.update(indprogress+1)
1080        
1081        ## update display if active
1082        self.replace_tracks( track_df )
1083
1084        progress_bar.update(indprogress+2)
1085        ## update the list of events, or others 
1086        self.epicure.updates_after_tracking()
1087        progress_bar.update(indprogress+3)
1088        return graph
1089
1090############ Laptrack centroids option
1091    
1092    def create_laptrack_centroids(self):
1093        """ GUI of the laptrack option """
1094        self.gLapCentroids, glap_layout = wid.group_layout( "Laptrack-Centroids" )
1095        mdist, self.max_dist = wid.value_line( "Max distance", "15.0", "Maximal distance between two labels in consecutive frames to link them (in pixels)" )
1096        glap_layout.addLayout(mdist)
1097        ## splitting ~ cell division
1098        scost, self.splitting_cost = wid.value_line( "Splitting cutoff", "1", "Weight to split a track in two (increasing it favors division)" )
1099        glap_layout.addLayout(scost)
1100        ## merging ~ error ?
1101        mcost, self.merging_cost = wid.value_line( "Merging cutoff", "0", "Weight to merge to labels together" )
1102        glap_layout.addLayout(mcost)
1103
1104        add_feat, self.check_penalties, self.bpenalties = wid.checkgroup_help( "Add features cost", True, "Add cell features in the tracking calculation", None )
1105        self.create_penalties()
1106        glap_layout.addWidget(self.check_penalties)
1107        glap_layout.addWidget(self.bpenalties)
1108        self.gLapCentroids.setLayout(glap_layout)
1109
1110    def show_penalties(self):
1111        self.bpenalties.setVisible(not self.bpenalties.isVisible())
1112
1113    def create_penalties(self):
1114        pen_layout = QVBoxLayout()
1115        areaCost, self.area_cost = wid.value_line( "Area difference", "2", "Weight of the difference of area between two labels to link them (0 to ignore)" )
1116        pen_layout.addLayout(areaCost)
1117        solidCost, self.solidity_cost = wid.value_line( "Solidity difference", "0", "Weight of the difference of solidity between two labels to link them (0 to ignore)" )
1118        pen_layout.addLayout(solidCost)
1119        self.bpenalties.setLayout(pen_layout)
1120
1121    def laptrack_centroids_twoframes(self, labels, twoframes, loose=False):
1122        """ Perform tracking of two frames with strict parameters """
1123        laptrack = LaptrackCentroids(self, self.epicure)
1124        laptrack.max_distance = float(self.max_dist.text()) 
1125        if loose:
1126            laptrack.max_distance = min(50, laptrack.max_distance) ## more probable to find a parent
1127        self.region_properties = ["label", "centroid"]
1128        #if self.check_penalties.isChecked():
1129        #    self.region_properties.append("area")
1130        #    self.region_properties.append("solidity")
1131        #    laptrack.penal_area = float(self.area_cost.text())
1132        #    laptrack.penal_solidity = float(self.solidity_cost.text())
1133        #laptrack.set_region_properties(with_extra=self.check_penalties.isChecked())
1134        laptrack.set_region_properties(with_extra=False)
1135            
1136        df = self.twoframes_centroid(twoframes)
1137        if set(np.unique(df["label"])) == set(labels):
1138            ## no other labels
1139            return [None]*len(labels) 
1140        laptrack.splitting_cost = False ## disable splitting option
1141        laptrack.merging_cost = False ## disable merging option
1142        parent_labels = laptrack.twoframes_track(df, labels)
1143        return parent_labels
1144    
1145    def twoframes_centroid(self, img):
1146        """ Get centroids of first frame only """
1147        df0 = self.label_to_dataframe( img[0], 0 )
1148        df1 = self.label_to_dataframe( img[1], 1 )
1149        return pd.concat([df0, df1])
1150    
1151    def laptrack_centroids(self, start, end):
1152        """ Perform track with laptrack option and chosen parameters """
1153        ## Laptrack tracker
1154        laptrack = LaptrackCentroids(self, self.epicure)
1155        laptrack.max_distance = float(self.max_dist.text())
1156        laptrack.splitting_cost = float(self.splitting_cost.text())
1157        laptrack.merging_cost = float(self.merging_cost.text())
1158        self.region_properties = ["label", "centroid"]
1159        if self.check_penalties.isChecked():
1160            self.region_properties.append("area")
1161            self.region_properties.append("solidity")
1162            laptrack.penal_area = float(self.area_cost.text())
1163            laptrack.penal_solidity = float(self.solidity_cost.text())
1164        laptrack.set_region_properties(with_extra=self.check_penalties.isChecked())
1165
1166        progress_bar = progress(total=7)
1167        progress_bar.set_description( "Prepare tracking" )
1168        if self.epicure.verbose > 1:
1169            print("Convert labels to centroids: use track info ?")
1170        self.undrifted = False
1171        if self.drift_correction.isChecked():
1172            df = self.labels_to_centroids_flow( start, end )
1173        else:
1174            df = self.labels_to_centroids( start, end )
1175        progress_bar.update(1)
1176        if self.epicure.verbose > 1:
1177            print("GO tracking")
1178        progress_bar.set_description( "Do tracking with LapTrack Centroids" )
1179        track_df, split_df, merge_df = laptrack.track_centroids(df)
1180        progress_bar.update(2)
1181        if self.epicure.verbose > 1:
1182            print("After tracking, update everything")
1183        self.after_tracking(track_df, split_df, merge_df, progress_bar, 2)
1184        progress_bar.update(6)
1185        progress_bar.close()
1186    
1187############ Laptrack overlap option
1188
1189    def create_laptrack_overlap(self):
1190        """ GUI of the laptrack overlap option """
1191        self.gLapOverlap, glap_layout = wid.group_layout( "Laptrack-Overlaps" )
1192        miou, self.min_iou = wid.value_line( "Min IOU", "0.1", "Minimum Intersection Over Union score to link to labels together" )
1193        glap_layout.addLayout(miou)
1194        
1195        scost, self.split_cost = wid.value_line( "Splitting cost", "0.2", "Weight of linking a parent label with two labels (increasing it for more divisions)" )
1196        glap_layout.addLayout(scost)
1197        
1198        mcost, self.merg_cost = wid.value_line( "Merging cost", "0", "Weight of merging two parent labels into one" )
1199        glap_layout.addLayout(mcost)
1200
1201        self.gLapOverlap.setLayout(glap_layout)
1202
1203    def laptrack_overlaps(self, start, end):
1204        """ Perform track with laptrack overlap option and chosen parameters """
1205        ## Laptrack tracker
1206        laptrack = LaptrackOverlaps(self, self.epicure)
1207        miniou = float(self.min_iou.text())
1208        if miniou >= 1.0:
1209            miniou = 1.0
1210        laptrack.cost_cutoff = 1.0 - miniou
1211        laptrack.splitting_cost = float(self.split_cost.text())
1212        laptrack.merging_cost = float(self.merg_cost.text())
1213        self.region_properties = ["label", "centroid"]
1214
1215        progress_bar = progress(total=6)
1216        progress_bar.set_description( "Prepare tracking" )
1217        labels = self.labels_ready( start, end )
1218        self.undrifted = False
1219        progress_bar.update(1)
1220        progress_bar.set_description( "Do tracking with LapTrack Overlaps" )
1221        track_df, split_df, merge_df = laptrack.track_overlaps( labels )
1222        progress_bar.update(2)
1223        
1224        ## get dataframe of coordinates to create the graph 
1225        df = self.labels_to_centroids( start, end )
1226        self.undrifted = True
1227        progress_bar.update(3)
1228        coordinate_df = df.set_index(["frame", "label"])
1229        tdf = track_df.set_index(["frame", "label"])
1230        track_df2 = pd.merge( tdf, coordinate_df, right_index=True, left_index=True).reset_index()
1231        self.after_tracking( track_df2, split_df, merge_df, progress_bar, 3 )
1232        progress_bar.update(6)
1233        progress_bar.close()
1234    
1235    def laptrack_overlaps_twoframes(self, labels, twoframes, loose=False):
1236        """ Perform tracking of two frames with strict parameters """
1237        laptrack = LaptrackOverlaps(self, self.epicure)
1238        miniou = min( float(self.min_iou.text()), 0.9999 ) ## ensure that miniou is < 1
1239        laptrack.cost_cutoff = 1.0 - miniou
1240        if loose:
1241            laptrack.cost_cutoff = 0.95 ## more probable to find a parent/child
1242        self.region_properties = ["label", "centroid"]
1243
1244        laptrack.splitting_cost = False ## disable splitting option
1245        laptrack.merging_cost = False ## disable merging option
1246        parent_labels = laptrack.twoframes_track(twoframes, labels)
1247        return parent_labels
laptrack_over = True
class Tracking(PyQt6.QtWidgets.QWidget):
  43class Tracking(QWidget):
  44    """
  45        Handles tracking of cells, track operations with the Tracks layer
  46    """
  47    def __init__(self, napari_viewer, epic):
  48        super().__init__()
  49        self.viewer = napari_viewer
  50        self.epicure = epic
  51        self.graph = None      ## init 
  52        self.tracklayer = None      ## track layer with information (centroids, labels, tree..)
  53        self.track_data = None ## keep the updated data, and update the layer only from time to time (slow to do)
  54        self.tracklayer_name = "Tracks"  ## name of the layer containing tracks
  55        self.nframes = self.epicure.nframes
  56        self.properties = ["label", "centroid"]
  57
  58        layout = QVBoxLayout()
  59        
  60        ## Add update track button 
  61        self.track_update = wid.add_button( "Update tracks display", self.update_track_layer, "Update the Track layer with the changements made since the last update" )
  62        layout.addWidget(self.track_update)
  63        
  64        ## Correct track button 
  65        #track_reset = wid.add_button( "Correct track data", self.reset_tracks, "Correct the track data after some track was lost" )
  66        #layout.addWidget(track_reset)
  67
  68        ## Method specific
  69        track_method, self.track_choice = wid.list_line( "Tracking method", "Choose the tracking method to use and display its parameter", func=None )
  70        layout.addWidget(self.track_choice)
  71        
  72        self.track_choice.addItem("Laptrack-Centroids")
  73        self.create_laptrack_centroids()
  74        layout.addWidget(self.gLapCentroids)
  75
  76        if laptrack_over: 
  77            self.track_choice.addItem("Laptrack-Overlaps")
  78            self.create_laptrack_overlap()
  79            layout.addWidget(self.gLapOverlap)
  80        else:
  81            self.min_iou = None
  82            self.split_cost = None
  83            self.merg_cost = None
  84
  85        drift_layout, self.drift_correction, self.drift_radius = wid.check_value( check="With drift correction", checked=False, value=str(50), descr="Taking into account local drift in tracking calculations") 
  86        layout.addLayout( drift_layout )
  87        
  88        self.track_go = wid.add_button( "Track", self.do_tracking, "Launch the tracking with the current parameter. Can take time" )
  89        layout.addWidget(self.track_go)
  90        self.setLayout(layout)
  91
  92        ## General tracking options
  93        frame_line, self.frame_range, self.range_group = wid.checkgroup_help( "Track only some frames", False, "Option to track only a given range of frames", None ) 
  94        self.frame_range.clicked.connect( self.show_frame_range )
  95        range_layout = QVBoxLayout()
  96        ntrack, self.start_frame = wid.ranged_value_line( "Track from frame:", 0, self.nframes-1, 1, 0, "Set first frame to begin tracking" )
  97        range_layout.addLayout(ntrack)
  98        
  99        entrack, self.end_frame = wid.ranged_value_line( "Until frame:", 1, self.nframes-1, 1, self.nframes-1, "Set the last frame unitl which to track" )
 100        range_layout.addLayout(entrack)
 101        self.start_frame.valueChanged.connect( self.changed_start )
 102        self.end_frame.valueChanged.connect( self.changed_end )
 103        
 104        self.range_group.setLayout( range_layout )
 105        layout.addWidget( self.frame_range )
 106        layout.addWidget( self.range_group )
 107        
 108        self.show_frame_range()
 109        self.show_trackoptions()
 110        self.track_choice.currentIndexChanged.connect(self.show_trackoptions)
 111        
 112
 113    def show_frame_range( self ):
 114        """ Show/Hide frame range options """
 115        self.range_group.setVisible( self.frame_range.isChecked() )
 116        
 117    #### settings
 118
 119    def get_current_settings( self ):
 120        """ Get current settings to save as preferences """
 121        settings = {}
 122        settings["Track method"] = self.track_choice.currentText() 
 123        settings["Add feat"] = self.check_penalties.isChecked()
 124        settings["Max distance"] = self.max_dist.text()
 125        settings["Splitting cost"] = self.splitting_cost.text()
 126        settings["Merging cutoff"] = self.merging_cost.text()
 127        settings["Min IOU"] = self.min_iou.text()
 128        settings["Over split"] = self.split_cost.text()
 129        settings["Over merge"] = self.merg_cost.text()
 130        return settings
 131
 132    def apply_settings( self, settings ):
 133        """ Set the parameters/current display from the prefered settings """
 134        for setty, val in settings.items():
 135            if setty == "Track method":
 136                self.track_choice.setCurrentText( val )
 137            if setty == "Add feat":
 138                self.check_penalties.setChecked( val )
 139            if setty == "Max distance":
 140                self.max_dist.setText( val )
 141            if setty == "Splitting cost":
 142                self.splitting_cost.setText( val )
 143            if setty == "Merging cutoff":
 144                self.merging_cost.setText( val )
 145            if laptrack_over:
 146                if setty == "Min IOU":
 147                    self.min_iou.setText( val )
 148                if setty == "Over split":
 149                    self.split_cost.setText( val )
 150                if setty == "Over merge":
 151                    self.merg_cost.setText( val )
 152            
 153    ##########################################
 154    #### Tracks layer and function
 155
 156    def reset( self ):
 157        """ Reset Tracks layer and data """
 158        self.graph = None
 159        self.track_data = None
 160        ut.remove_layer( self.viewer, "Tracks" )
 161
 162    def init_tracks(self, track_table=None, track_prop=None ):
 163        """ Add a track layer with the new tracks """
 164        if track_table is None:
 165            track_table, track_prop = self.create_tracks()
 166        #print(track_table)
 167        
 168        ## plot tracks
 169        if len(track_table) > 0:
 170            self.clear_graph()
 171            self.viewer.add_tracks(
 172                track_table,
 173                graph=self.graph, 
 174                name=self.tracklayer_name,
 175                properties = track_prop,
 176                scale = self.viewer.layers["Segmentation"].scale,
 177                )
 178            self.viewer.layers[self.tracklayer_name].visible=True
 179            self.viewer.layers[self.tracklayer_name].color_by="track_id"
 180            ut.set_active_layer(self.viewer, "Segmentation")
 181            self.tracklayer = self.viewer.layers[self.tracklayer_name]
 182            self.track_data = self.tracklayer.data
 183            #self.track.display_id = True
 184            self.color_tracks_as_labels()
 185
 186    def color_tracks_as_labels(self):
 187        """ Color the tracks the same as the label layer """
 188        ## must color it manually by getting the Label layer colors for each track_id
 189        cols = np.zeros((len(self.tracklayer.data[:,0]),4))
 190        for i, tr in enumerate(self.tracklayer.data[:,0]):
 191            cols[i] = (self.epicure.seglayer.get_color(tr))
 192        self.tracklayer._track_colors = cols
 193        self.tracklayer.events.color_by()
 194    
 195    def color_tracks_by_lineage(self):
 196        """ Color the tracks by their lineage (daughters same colors as parents) """
 197        ## must color it manually by getting the Label layer colors for each track_id
 198        cols = np.zeros((len(self.tracklayer.data[:,0]),4))
 199        for i, tr in enumerate(self.tracklayer.data[:,0]):
 200            ## find the parent cell,n going up the tree until no more parent
 201            while tr in self.graph.keys():
 202                tr = self.graph_parent( tr )
 203            cols[i] = (self.epicure.seglayer.get_color(tr))
 204        self.tracklayer._track_colors = cols
 205        self.tracklayer.events.color_by()
 206
 207    def graph_parent( self, ind ):
 208        """ Get the value of the parent from the graph """
 209        if ind not in self.graph.keys():
 210            return None
 211        if isinstance(self.graph[ind], list):
 212            return self.graph[ind][0]
 213        return self.graph[ind]
 214
 215    def replace_tracks(self, track_df):
 216        """ Replace all tracks based on the dataframe """
 217        if not self.undrifted and self.drift_correction.isChecked():
 218            ## recalculate the label centroids as it was corrected for drift
 219            track_table, track_prop = self.create_tracks()
 220        else:
 221            track_table, track_prop = self.build_tracks( track_df )
 222        self.tracklayer.data = track_table
 223        self.track_data = self.tracklayer.data
 224        self.tracklayer.properties = track_prop
 225        self.tracklayer.refresh()
 226        self.color_tracks_as_labels()
 227
 228    def reset_tracks(self):
 229        """ Reset tracks and reload them from labels """
 230        ut.remove_layer(self.viewer, "Tracks")
 231        self.init_tracks()
 232
 233    def update_track_layer(self):
 234        """ Update the track layer (slow) """
 235        self.viewer.window._status_bar._toggle_activity_dock(True)
 236        progress_bar = progress(total=1)
 237        progress_bar.set_description( "Updating track layer" )
 238        self.tracklayer.data = self.track_data
 239        progress_bar.close()
 240        self.color_tracks_as_labels()
 241        self.viewer.window._status_bar._toggle_activity_dock(False)
 242
 243    def measure_intensity_features( self, feat, intimg=None, frames=None ):
 244        """ Measure mean value of a feature in a track """
 245        if ( intimg is not None ):
 246            if frames is None:
 247                tracks = self.get_track_list()
 248                seg = self.epicure.seg
 249                iimg = intimg
 250            else:
 251                tracks = self.get_tracks_list_frames( frames )
 252                seg = self.epicure.seg[frames]
 253                iimg = intimg[frames]
 254        if feat == "intensity_mean":
 255            mean_intensities = ndi.mean( iimg, seg, tracks )
 256            return tracks, mean_intensities
 257        if feat == "intensity_sum":
 258            sum_intensities = ndi.sum( iimg, seg, tracks )
 259            return tracks, sum_intensities
 260        if feat == "intensity_max":
 261            sum_intensities = ndi.maximum( iimg, seg, tracks )
 262            return tracks, sum_intensities
 263        if feat == "intensity_min":
 264            sum_intensities = ndi.minimum( iimg, seg, tracks )
 265            return tracks, sum_intensities
 266        if feat == "intensity_median":
 267            sum_intensities = ndi.median( iimg, seg, tracks )
 268            return tracks, sum_intensities
 269        print( "Mean feature on track not implemented" )
 270        return None
 271
 272    def measure_track_features( self, track_id, scaling=False ):
 273        """ Measure features (length, total displacement...) of given track """
 274        features = {}
 275        track = self.get_track_data( track_id )
 276        if track.shape[0] == 0:
 277            return features
 278        track = track[track[:,1].argsort()]
 279        start = int(np.min(track[:,1]))
 280        end = int(np.max(track[:,1]))
 281        temp_unit = ""
 282        vel_unit = ""
 283        disp_unit = ""
 284        temp_scale = 1
 285        vel_scale = 1
 286        disp_scale = 1
 287        if scaling:
 288            temp_unit = "_"+self.epicure.epi_metadata["UnitT"]
 289            vel_unit = "_"+self.epicure.epi_metadata["UnitXY"]+"/"+self.epicure.epi_metadata["UnitT"]
 290            disp_unit = "_"+self.epicure.epi_metadata["UnitXY"]
 291            temp_scale = self.epicure.epi_metadata["ScaleT"]
 292            vel_scale = self.epicure.epi_metadata["ScaleXY"]/self.epicure.epi_metadata["ScaleT"]
 293            disp_scale = self.epicure.epi_metadata["ScaleXY"]
 294        features["Label"] = track_id
 295        features["TrackDuration"+temp_unit] = (end - start + 1)*temp_scale
 296        features["TrackStart"+temp_unit] = start * temp_scale
 297        features["TrackEnd"+temp_unit] = end * temp_scale
 298        features["NbGaps"] = end - start + 1 - len(track)
 299        if (end-start) == 0:
 300            ## only one frame
 301            features["TotalDisplacement"+disp_unit] = None
 302            features["NetDisplacement"+disp_unit] = None
 303            features["Straightness"] = None
 304            features["MeanVelocity"+vel_unit] = None
 305        else:
 306            features["TotalDisplacement"+disp_unit] = ut.total_distance( track[:,2:4] ) * disp_scale
 307            features["NetDisplacement"+disp_unit] = ut.net_distance( track[:,2:4] ) * disp_scale
 308            features["MeanVelocity"+vel_unit] = np.mean( ut.velocities( track[:,1:4] ) ) * vel_scale 
 309            if features["TotalDisplacement"+disp_unit] > 0:
 310                features["Straightness"] = features["NetDisplacement"+disp_unit]/features["TotalDisplacement"+disp_unit]
 311            else:
 312                features["Straightness"] = None
 313        return features
 314
 315    def measure_speed( self, track_id ):
 316        """ Returns the velocities of the track """
 317        track = self.get_track_data( track_id )
 318        if track.shape[0] == 0:
 319            return None 
 320        track = track[track[:,1].argsort()]
 321        return ut.velocities( track[:,1:4] )
 322
 323    def measure_features( self, track_id, features ):
 324        """ Measure features along all the track """
 325        mask = self.epicure.get_mask( track_id )
 326        res = {}
 327        for feat in features:
 328            res[feat] = []
 329        for frame in mask:
 330            props = ut.labels_properties( frame )
 331            if len(props) > 0:
 332                if "Area" in features:
 333                    res["Area"].append( props[0].area )
 334                if "Hull" in features:
 335                    res["Hull"].append( props[0].area_convex )
 336                if "Elongation" in features:
 337                    res["Elongation"].append( props[0].axis_major_length )
 338                if "Eccentricity" in features:
 339                    res["Eccentricity"].append( props[0].eccentricity )
 340                if "Perimeter" in features:
 341                    res["Perimeter"].append( props[0].perimeter )
 342                if "Solidity" in features:
 343                    res["Solidity"].append( props[0].solidity )
 344        return res
 345
 346    def measure_specific_feature( self, track_id, featureName ):
 347        """ Measure some specific feature """
 348        if featureName == "Similarity":
 349            import skimage.metrics as imetrics
 350            movie = self.epicure.get_label_movie( track_id, extend=1.5 )
 351            sim_scores = []
 352            for i in range(0, len(movie)-1):
 353                score = imetrics.normalized_mutual_information( movie[i], movie[i+1] ) 
 354                sim_scores.append(score)
 355            return sim_scores
 356
 357    def measure_labels(self, segimg):
 358        """ Get the dataframe of the labels in the segmented image """
 359        resdf = None
 360        for iframe, frame in progress(enumerate(segimg)):
 361            frame_table = ut.labels_to_table( frame, iframe )
 362            if resdf is None:
 363                resdf = pd.DataFrame(frame_table)
 364            else:
 365                resdf = pd.concat([resdf, pd.DataFrame(frame_table)])
 366        return resdf
 367
 368    def add_track_frame(self, label, frame, centroid, tree=None, group=None):
 369        """ Add one frame to the track """
 370        new_frame = np.array([label, frame, centroid[0], centroid[1]])
 371        self.track_data = np.vstack((self.track_data, new_frame))
 372            
 373    def get_track_list(self):
 374        """ Get list of unique track_ids """
 375        return np.unique( self.track_data[:,0] )
 376    
 377    def get_tracks_list_frames( self, frames ):
 378        """ Return list of tracks present on list of frames """
 379        return np.unique( self.track_data[ np.isin( self.track_data[:,1], frames), 0] ) 
 380    
 381    def get_tracks_on_frame( self, tframe ):
 382        """ Return list of tracks present on given frame """
 383        return np.unique( self.track_data[ self.track_data[:,1]==tframe, 0] ) 
 384
 385    def has_track(self, label):
 386        """ Test if track label is present """
 387        return label in self.track_data[:,0]
 388    
 389    def has_tracks(self, labels):
 390        """ Test if track labels are present """
 391        return np.isin( labels, self.track_data[:,0] )
 392    
 393    def nb_points(self):
 394        """ Number of points in the tracks """
 395        return self.track_data.shape[0]
 396
 397    def nb_tracks(self):
 398        """ Return number of tracks """
 399        #return self.track._manager.__len__()
 400        return len(self.get_track_list())
 401
 402    def gaped_track(self, track_id):
 403        """ Check if there is a gap (missing frame) in a track """
 404        indexes = self.get_track_indexes(track_id)
 405        if len(indexes) <= 0:
 406            return False
 407        track_frames = self.track_data[indexes,1]
 408        return ((np.max(track_frames)-np.min(track_frames)+1) > len(track_frames) )
 409
 410    def gap_frames(self, track_id):
 411        """ Returns the frame(s) at which the gap(s) are """
 412        track_frames = self.get_track_column( track_id, "frame" )
 413        gaps = []
 414        if len( track_frames ) > 0:
 415            min_frame = int( np.min(track_frames) )
 416            max_frame = int( np.max(track_frames) )
 417            gaps = np.setdiff1d( np.arange(min_frame+1, max_frame), track_frames ).tolist()
 418            if len(gaps) > 0:
 419                gaps.sort()
 420        return gaps
 421            
 422    def check_gap(self, tracks=None, verbose=None):
 423        """ Check if there is a track with a gap, flag it if yes """
 424        if tracks is None:
 425            tracks = self.get_track_list()
 426        gaped = []
 427        for track in tracks:
 428            if self.gaped_track( track ):
 429                gaped.append(track)
 430        if verbose is None:
 431            verbose = self.epicure.verbose
 432        if verbose > 0 and len(gaped)>0:
 433            ut.show_warning("Gap in track(s) "+str(gaped)+"\n"
 434            +"Consider doing sanity_check in Editing onglet to fix it")
 435        return gaped
 436
 437    def get_track_indexes(self, track_id):
 438        """ Get indexes of track_id tracks position in the arrays """
 439        if isinstance( track_id,  int ):
 440            return (np.flatnonzero( self.track_data[:,0] == track_id ) )
 441        return (np.flatnonzero( np.isin( self.track_data[:,0], track_id ) ) )
 442    
 443    def get_track_indexes_onframes( self, track_id, frames ):
 444        """ Get indexes of track_id tracks position in the arrays """
 445        if isinstance( frames, int ):
 446            frames = [frames]
 447        if isinstance( track_id,  int ):
 448            return (np.flatnonzero( (self.track_data[:,0] == track_id) * np.isin( self.track_data[:,1], frames) ) )
 449        return (np.flatnonzero( np.isin( self.track_data[:,0], track_id ) * np.isin( self.track_data[:,1], frames) ) )
 450
 451    def get_track_indexes_from_frame(self, track_id, frame):
 452        """ Get indexes of track_id tracks position in the arrays from the given frame """
 453        if type(track_id) == int:
 454            return (np.argwhere( (self.track_data[:,0] == track_id)*(self.track_data[:,1]>= frame) )).flatten()
 455        return (np.argwhere( np.isin( self.track_data[:,0], track_id )*(self.track_data[:,1]>= frame) )).flatten()
 456
 457    def get_index(self, track_id, frame ):
 458        """ Get index of track_id at given frame """
 459        if np.isscalar(track_id):
 460            track_id = [track_id]
 461        return np.argwhere( (np.isin(self.track_data[:,0], track_id))*(self.track_data[:,1] == frame) )
 462
 463    def get_small_tracks(self, max_length=1):
 464        """ Get tracks smaller than the given threshold """
 465        labels = []
 466        lengths = []
 467        positions = []
 468        for lab in self.get_track_list():
 469            indexes = self.get_track_indexes(lab)
 470            length = len(indexes)
 471            if length <= max_length:
 472                pos = self.mean_position( indexes, only_first=False )
 473                labels.append(lab)
 474                lengths.append(length)
 475                positions.append(pos)
 476        return labels, lengths, positions
 477
 478    def get_track_data(self, track_id):
 479        """ Return the data of track track_id """
 480        indexes = self.get_track_indexes( track_id )
 481        track = self.track_data[indexes,]
 482        return track
 483    
 484    def get_track_column( self, track_id, column ):
 485        """ Return the chosen column (frame, x, y, label) of track track_id """
 486        indexes = self.get_track_indexes( track_id )
 487        if column == "frame":
 488            return self.track_data[indexes, 1]
 489        if column == "label":
 490            return self.track_data[indexes, 0]
 491        if column == "pos":
 492            return self.track_data[indexes, 2:4]
 493        if column == "fullpos":
 494            return self.track_data[indexes, 1:4]
 495        track = self.track_data[indexes]
 496        return track
 497
 498    def get_frame_data( self, track_id, ind ):
 499        """ Get ind-th data of track track_id """
 500        track = self.get_track_data( track_id )
 501        return track[ind]
 502    
 503    def get_middle_position( self, track_id, framea, frameb ):
 504        """ Get track position in middle of frame a and frame b """
 505        inda = self.get_index( track_id, framea ) 
 506        indb = self.get_index( track_id, frameb )
 507        return self.mean_position( np.ravel( np.vstack((inda, indb)) ), only_first=False )
 508
 509    def get_position( self, track_id, frame ):
 510        """ Get position of the track at given frame """
 511        ind = self.get_index( track_id, frame )
 512        ind = ind.flatten()[0] ## ensure it's single element
 513        x,y = self.track_data[ind,2], self.track_data[ind,3]
 514        return [int(x), int(y)]
 515
 516    def get_full_position( self, track_id, frame ):
 517        """ Get position of the track at given frame, with the frame itself """
 518        ind = self.get_index( track_id, frame )
 519        ind = ind.flatten()[0] ## ensure it's single element
 520        x,y = self.track_data[ind,2], self.track_data[ind,3]
 521        return [frame,x,y]
 522
 523    def mean_position(self, indexes, only_first=False):
 524        """ Mean positions of tracks at indexes """
 525        if len(indexes) <= 0:
 526            return None
 527        track = self.track_data[indexes,]
 528        ## keep only the first frame of the tracks
 529        if only_first:
 530            min_frame = np.min(track[:,1])
 531            track = track[track[:,1]==min_frame,]
 532        return ( int(np.mean(track[:,1])), int(np.mean(track[:,2])), int(np.mean(track[:,3])) )
 533
 534    def get_first_frame(self, track_id):
 535        """ Returns first frame where track_id is present """
 536        track = self.get_track_data( track_id )
 537        if len(track) <= 0:
 538            return None
 539        return int( np.min(track[:,1]) )
 540
 541    def is_in_frame( self, track_id, frame ):
 542        """ Returns if track_id is present at given frame """
 543        track = self.get_track_data( track_id )
 544        if len(track) > 0:
 545            return frame in track[:,1]
 546        return False
 547    
 548    def get_last_frame(self, track_id):
 549        """ Returns last frame where track_id is present """
 550        track = self.get_track_data( track_id )
 551        if len(track) > 0:
 552            return int(np.max(track[:,1]))
 553        return None
 554    
 555    def get_extreme_frames(self, track_id):
 556        """ Returns the first and last frames where track_id is present """
 557        track = self.get_track_data( track_id )
 558        if track.shape[0] > 0:
 559            return (int(np.min(track[:,1])), int(np.max(track[:,1])) )
 560        return None, None
 561
 562    def get_mean_position(self, track_id, only_first=False):
 563        """ Get mean position of the track """
 564        indexes = self.get_track_indexes( track_id )
 565        return self.mean_position( indexes, only_first )
 566
 567    def update_centroid(self, track_id, frame, ind=None, cx=None, cy=None):
 568        """ Update track at given frame """
 569        if ind is None:
 570            ind = self.get_index( track_id, frame )
 571        if cx is None:
 572            prop = ut.getPropLabel( self.epicure.seg[frame], track_id )
 573            self.track_data[ind, 2:4] = prop.centroid[1]
 574        else:
 575            self.track_data[ind, 2] = cx
 576            self.track_data[ind, 3] = cy
 577
 578    def replace_on_frames( self, tids, new_tids, frames ):
 579        """ Replace the id tid by new_tid in all given frames """
 580        ind = self.get_track_indexes_onframes( tids, frames )
 581        cur_track = np.copy(self.track_data[ind])
 582        new_ids = np.repeat(-1, len(ind))
 583        for tid, new_tid in zip(tids, new_tids):
 584            self.update_graph_frames( tid, cur_track[cur_track[:,0]==tid,1] )
 585            new_ids[cur_track[:,0]==tid] = new_tid
 586        self.track_data[ind, 0] = new_ids
 587        
 588    def swap_frame_id(self, tid, otid, frame):
 589        """ Swap the ids of two tracks at frame """
 590        ind = int(self.get_index(tid, frame))
 591        oind = int(self.get_index(otid, frame))
 592        ## check if one of the label is an extreme of a track and potentially in the graph
 593        for track_index in [tid, otid]:
 594            min_frame, max_frame = self.get_extreme_frames( track_index )
 595            if (min_frame == frame) or (max_frame == frame):
 596                self.update_graph( track_index, frame )
 597        self.track_data[[ind, oind],0] = [otid, tid]
 598
 599    def update_track_on_frame(self, track_ids, frame):
 600        """ Update (add or modify) tracks at given frame """
 601        frame_table = ut.labels_table( labimg = np.where(np.isin(self.epicure.seg[frame], track_ids), self.epicure.seg[frame], 0), properties=self.properties )
 602        for x, y, tid in zip(frame_table["centroid-0"], frame_table["centroid-1"], frame_table["label"]):
 603            index = self.get_index(tid, frame)
 604            if len(index) > 0:
 605                self.update_centroid( tid, frame, index, int(x), int(y) )
 606            else:
 607                cur_cell = np.array( [[tid, frame, int(x), int(y)]] )
 608                self.track_data = np.append(self.track_data, cur_cell, axis=0)
 609
 610    def add_tracks_fromindices( self, indices, track_ids ):
 611        """ Add tracks of given track ids from the indices"""
 612        new_data = np.empty( (0,4), int )
 613        for tid in np.unique(track_ids):
 614            keep = track_ids == tid 
 615            for frame in np.unique( indices[0][keep] ):
 616                cent0 = np.mean( indices[1][keep] ) 
 617                cent1 = np.mean( indices[2][keep] ) 
 618                new_data = np.append( new_data, np.array([[tid, frame, int(cent0), int(cent1)]]), axis=0 )
 619        self.track_data = np.append( self.track_data, new_data, axis=0)
 620    
 621    def add_one_frame(self, track_ids, frame, refresh=True):
 622        """ Add one frame from track """
 623        for tid in track_ids:
 624            frame_table = ut.labels_table( np.uint8(self.epicure.seg[frame]==tid), properties=self.properties ) 
 625            cur_cell = np.array( [tid, frame, int(frame_table["centroid-0"]), int(frame_table["centroid-1"])], dtype=np.uint32 )
 626            cur_cell = np.expand_dims(cur_cell, axis=0)
 627            self.track_data = np.append(self.track_data, cur_cell, axis=0)
 628
 629    def remove_one_frame( self, track_id, frame, handle_gaps=False, refresh=True ):
 630        """ 
 631        Remove one frame from track(s) 
 632        """
 633        inds = self.get_index( track_id, frame )
 634        if np.isscalar(track_id):
 635            track_id = [track_id]
 636        check_for_gaps = False
 637        for tid in track_id:
 638            ## removed frame is in the extremity of a track, can be in the graph
 639            first_frame, last_frame = self.get_extreme_frames( tid )
 640            if first_frame is None:
 641                continue
 642            if (first_frame == frame) or (last_frame == frame):
 643                self.update_graph( tid, frame )
 644            else:
 645                check_for_gaps = True
 646        self.track_data = np.delete( self.track_data, inds, axis=0 )
 647        ## gaps might have been created in the tracks, for now doesn't allow it so split the tracks
 648        if handle_gaps and check_for_gaps:
 649            gaped = self.check_gap( track_id, verbose=0 )
 650            if len(gaped) > 0:
 651                self.epicure.fix_gaps( gaped )
 652        
 653    def get_current_value(self, track_id, frame):
 654        ind = self.get_index(track_id, frame)
 655        centx, centy = self.track_data[ind, 2:4].astype(int).flatten()
 656        return self.epicure.seg[frame, centx, centy]
 657
 658    def clear_graph( self ):
 659        """ Check the state of the graph and removes non existing keys or values """
 660        if self.graph is None:
 661            return
 662        keys = list(self.graph.keys())
 663        for key in keys:
 664            if key not in self.track_data[:,0]:
 665                del self.graph[key]
 666            else:
 667                vals = self.graph[key]
 668                if isinstance(vals, list):
 669                    for val in vals:
 670                        if val not in self.track_data[:,0]:
 671                            del self.graph[key]
 672                            break
 673                else:
 674                    if vals not in self.track_data[:,0]:
 675                        del self.graph[key]
 676
 677    def set_graph(self, graph):
 678        """ Set the current graph (eg imported from TrackMate XML file) """
 679        self.graph = graph
 680        ## set the divisions from the graph
 681        self.epicure.inspecting.get_divisions()
 682
 683    def update_graph_frames( self, track_id, frames ):
 684        """ Update graph when one label was deleted at given frames """
 685        fframe = np.min(frames)
 686        lframe = np.max(frames)
 687        self.update_graph( track_id, fframe )
 688        self.update_graph( track_id, lframe )
 689
 690    def update_graph(self, track_id, frame):
 691        """ Update graph if deleted label was linked at that frame, assume keys are unique """
 692        if self.graph is not None:
 693            ## handles current node is last of his branch
 694            parents = self.last_in_graph( track_id, frame )
 695            current_label = self.get_current_value( track_id, frame )
 696            for parent in parents:
 697                if current_label == 0:
 698                    del self.graph[parent]
 699                else:
 700                    self.update_child( parent, track_id, current_label )
 701            ## handles when current track is first frame of a division
 702            if self.first_in_graph( track_id, frame ):
 703                if current_label == 0:
 704                    del self.graph[track_id]
 705                else:
 706                    self.update_key( track_id, current_label ) 
 707
 708    def update_child(self, parent, prev_key, new_key):
 709        """ Change the value of a key in the graph """
 710        if isinstance(self.graph[parent], list):
 711            self.graph[parent] = [new_key if val == prev_key else val for val in self.graph[parent]]
 712        else:
 713            if self.graph[parent] == prev_key:
 714                self.graph[parent] = new_key
 715
 716    def update_key(self, prev_key, new_key):
 717        """ Change the value of a key in the graph """
 718        self.graph[new_key] = self.graph.pop(prev_key)
 719
 720    def is_parent( self, cur_id ):
 721        """ Return if the current id is in the graph (as a parent, so in values) """
 722        if self.graph is None:
 723            return False
 724        return any( cur_id in vals if isinstance(vals, list) else cur_id in [vals] for vals in self.graph.values() )
 725
 726    def add_division( self, childa, childb, parent ):
 727        """ Add info of a division to the graph of divisions/merges """
 728        if self.graph is None:
 729            self.graph = {}
 730        self.graph.update({childa: [parent], childb: [parent]})
 731
 732    def remove_division( self, parent ):
 733        """ Remove a division event from the graph """
 734        self.graph = {key: vals for key, vals in self.graph.items() if not ( self.graph_parent(key) == parent )  }
 735
 736    def last_in_graph(self, track_id, frame=None, check_last=True):
 737        """ Check if given label and frame is the last of a branch, in the graph """
 738        if check_last:
 739            return [key for key, vals in self.graph.items() if track_id in (vals if isinstance(vals, list) else [vals]) and self.get_last_frame(track_id) == frame]
 740        return [key for key, vals in self.graph.items() if track_id in (vals if isinstance(vals, list) else [vals])]
 741
 742    def first_in_graph(self, track_id, frame=None, check_first=True):
 743        """ Check if the given label and frame is the first in the branch so the node in the graph """
 744        if check_first:
 745            return track_id in self.graph and self.get_first_frame(track_id) == frame
 746        return track_id in self.graph
 747
 748    def remove_on_frames( self, track_ids, frames ):
 749        """ Remove tracks with given id on specified frames """
 750        track_ids = track_ids.tolist()
 751        if 0 in track_ids:
 752            track_ids.remove(0)
 753        inds = self.get_track_indexes_onframes( track_ids, frames )
 754        for tid in track_ids:
 755            self.update_graph_frames( tid, frames )
 756        self.track_data = np.delete( self.track_data, inds, axis=0 )
 757
 758    def remove_tracks(self, track_ids):
 759        """ Remove track with given id """
 760        inds = self.get_track_indexes(track_ids)
 761        self.track_data = np.delete(self.track_data, inds, axis=0)
 762        self.remove_ids_from_graph( track_ids )
 763    
 764    def remove_ids_from_graph( self, track_ids ):
 765        """ Remove all ids from the graph """
 766        track_ids_set = set( track_ids )
 767        if self.graph is not None:
 768            self.graph = {
 769                key: vals for key, vals in self.graph.items()
 770                if (key not in track_ids_set) and ( not any( val in track_ids_set for val in (vals if isinstance(vals, list) else [vals])) )
 771            }
 772    
 773    def is_single_parent( self, cur_id ):
 774        """ Return if the current id is in the graph (as a single parent, not a merge) """
 775        if self.graph is None:
 776            return False
 777        return any( cur_id in [vals] if not isinstance(vals, list) else (cur_id in vals and len(vals)==1) for vals in self.graph.values() )
 778
 779       
 780    def build_tracks(self, track_df):
 781        """ Create tracks from dataframe (after tracking) """
 782        track = track_df[["track_id", "frame", "centroid-0", "centroid-1"]]
 783        #frame_prop = frame_table[["tree_id", "label", "nframes", "group"]]
 784        return np.array(track, int), None #dict(frame_prop)
 785
 786    def create_tracks(self):
 787        """ Create tracks from labels (without tracking) """
 788        #track_table = np.empty( (0,4), int )   
 789        labels = self.epicure.seg
 790        total = self.epicure.nframes
 791        if self.epicure.process_parallel:
 792            track_tables = Parallel( n_jobs=self.epicure.nparallel ) (
 793                delayed(ut.labels_to_table)(frame, iframe ) for iframe, frame in enumerate(labels)
 794            )
 795        else:
 796            track_tables = [ ut.labels_to_table( frame, iframe) for iframe, frame in progress(enumerate(labels), total=total) ]
 797        track_table = np.concatenate( [ tab for tab in track_tables if tab.shape[0] != 0 ], axis=0 ) # handle empty frame
 798        return track_table, None # track_prop
 799
 800    def add_track_features(self, labels):
 801        """ Add features specific to tracks (eg nframes) """
 802        nframes = np.zeros(len(labels), int)
 803        if self.epicure.verbose > 2:
 804            print("REPLACE BY COUNT METHOD")
 805        for track_id in np.unique(labels):
 806            cur_track = np.argwhere(labels == track_id)
 807            nframes[ list(cur_track) ] = len(cur_track)
 808        return nframes
 809    
 810
 811    ##########################################
 812    #### Tracking functions
 813
 814    def changed_start(self, i):
 815        """ Ensures that end frame > start frame """
 816        if i > self.end_frame.value():
 817            self.end_frame.setValue(i+1)
 818
 819    def changed_end(self, i):
 820        if i < self.start_frame.value():
 821            self.start_frame.setValue(i-1)
 822
 823    def find_parents(self, labels, twoframes):
 824        """ Find in the first frame the parents of labels from second frame """
 825        
 826        if self.track_choice.currentText() == "Laptrack-Centroids":
 827            return self.laptrack_centroids_twoframes(labels, twoframes, loose=True)
 828        
 829        if self.track_choice.currentText() == "Laptrack-Overlaps":
 830            return self.laptrack_overlaps_twoframes(labels, twoframes, loose=True)
 831        
 832
 833    def do_tracking(self):
 834        """ Start the tracking with the selected options """
 835        if self.frame_range.isChecked():
 836            start = self.start_frame.value()
 837            end = self.end_frame.value()
 838        else:
 839            start = 0
 840            end = self.nframes-1
 841        start_time = ut.start_time()
 842        self.viewer.window._status_bar._toggle_activity_dock(True)
 843        self.epicure.inspecting.reset_all_events()
 844        
 845        if self.track_choice.currentText() == "Laptrack-Centroids":
 846            if self.epicure.verbose > 1:
 847                print("Starting track with Laptrack-Centroids")
 848            self.laptrack_centroids( start, end )
 849            self.epicure.tracked = 1
 850        if self.track_choice.currentText() == "Laptrack-Overlaps":
 851            if self.epicure.verbose > 1:
 852                print("Starting track with Laptrack-Centroids")
 853            self.laptrack_overlaps( start, end )
 854            self.epicure.tracked = 1
 855        
 856        self.epicure.finish_update(contour=2)
 857        #self.epicure.reset_free_label()
 858        self.viewer.window._status_bar._toggle_activity_dock(False)
 859        if self.epicure.verbose > 0:
 860            ut.show_duration( start_time, header="Tracking done in " )
 861
 862    def show_trackoptions(self):
 863        self.gLapCentroids.setVisible(self.track_choice.currentText() == "Laptrack-Centroids")
 864        if laptrack_over:
 865            self.gLapOverlap.setVisible(self.track_choice.currentText() == "Laptrack-Overlaps")
 866
 867    def relabel_nonunique_labels(self, track_df):
 868        """ After tracking, some track can be splitted and get same label, fix that """
 869        inittids = np.unique(track_df["track_id"])
 870        labtracks = []
 871        saved_data = np.copy(self.epicure.seglayer.data)
 872        mframes = []
 873        tids = []
 874        used = np.unique( saved_data )
 875        all_labels = np.unique(track_df["label"])
 876        for tid in inittids:
 877            cdf = track_df[track_df["track_id"]==tid]
 878            #print(cdf)
 879            min_frame = np.min( cdf["frame"] )
 880            #labtrack = int( cdf["label"][cdf["frame"]==min_frame] )
 881            for lab in np.unique(cdf["label"]):
 882                labtracks.append(lab)
 883                mframes.append( min_frame )
 884                tids.append(tid)
 885        if len(labtracks) != len(np.unique(labtracks)):
 886            ## some labels are present several times
 887            used = used.tolist()
 888            for lab in all_labels :
 889                indexes = list(np.where(np.array(labtracks)==lab)[0])
 890                if len(indexes)>1:
 891                    minframes = [mframes[indy] for indy in range(len(labtracks)) if labtracks[indy]==lab]
 892                    indmin = indexes[ np.argmin( minframes ) ]
 893                    ## for the other(s), change the label
 894                    newvals = ut.get_free_labels( used, len(indexes) )
 895                    used = used + newvals
 896                    for i, ind in enumerate(indexes):
 897                        if ind != indmin:
 898                            tid = tids[ind]
 899                            newval = newvals[i]
 900                            track_df.loc[ (track_df["track_id"]==tid)  & (track_df["label"]==lab) , "label"] = newval
 901                            for frame in track_df["frame"][(track_df["track_id"]==tid) & (track_df["label"]==newval)]:
 902                                mask = (saved_data[frame]==lab)
 903                                self.epicure.seglayer.data[frame][mask] = newval
 904        
 905
 906    def relabel_trackids(self, track_df, splitdf, mergedf):
 907        """ Change the trackids to take the first label of each track """
 908        start_time = ut.start_time()
 909        new_trackids = track_df['track_id'].copy()
 910        new_splitdf = splitdf.copy()
 911        new_mergedf = mergedf.copy()
 912        
 913        unique_track_ids = np.unique(track_df['track_id'])
 914        if ut.version_python_minor(10):
 915            ## from python3.10, get futurewarning on groupby without group_keys and include_groups keywords
 916            first_labels = track_df.groupby('track_id', group_keys=False).apply(lambda x: x.loc[x['frame'].idxmin(), 'label'], include_groups=False).to_dict()
 917        else:
 918            first_labels = track_df.groupby('track_id').apply(lambda x: x.loc[x['frame'].idxmin(), 'label']).to_dict()
 919        
 920        for tid in unique_track_ids:
 921            newval = first_labels[tid]
 922            if tid != newval:
 923                new_trackids[track_df['track_id'] == tid] = newval
 924                if not new_splitdf.empty:
 925                    new_splitdf.loc[splitdf["parent_track_id"] == tid, "parent_track_id"] = newval
 926                    new_splitdf.loc[splitdf["child_track_id"] == tid, "child_track_id"] = newval
 927                if not new_mergedf.empty:
 928                    new_mergedf.loc[mergedf["parent_track_id"] == tid, "parent_track_id"] = newval
 929                    new_mergedf.loc[mergedf["child_track_id"] == tid, "child_track_id"] = newval
 930        if self.epicure.verbose > 1:
 931            ut.show_duration( start_time, header="Relabeling done in " )            
 932        return new_trackids, new_splitdf, new_mergedf
 933
 934    def change_labels(self, track_df):
 935        """ Change the labels at each frame according to tracks """
 936        for frame, frame_df in track_df.groupby("frame"):
 937            self.change_frame_labels(frame, frame_df)
 938
 939    def change_frame_labels(self, frame, frame_df):
 940        """ Change the labels at given frame according to tracks """
 941        track_ids = frame_df['track_id'].astype(int).values
 942        old_labels = frame_df["label"].astype(int).values
 943        seglayer = np.copy(self.epicure.seglayer.data[frame])
 944        for old_lab, new_lab in zip(old_labels, track_ids):
 945            mask = (seglayer==old_lab)
 946            self.epicure.seglayer.data[frame][mask] = new_lab
 947
 948    def label_to_dataframe( self, labimg, frame ):
 949        """ from label, get dataframe of centroids with properties """
 950        df = pd.DataFrame( ut.labels_table(labimg, properties=self.region_properties) )
 951        if df.shape[0] == 0:
 952            ## no labels in this frame
 953            return None
 954        df["frame"] = frame
 955        return df
 956    
 957    def optical_flow( self, img0, img1, radius ):
 958        """ Compute the optical flow between two images """
 959        v, u = optical_flow_ilk( img0, img1, radius=radius)
 960        return v, u
 961    
 962    def apply_flow( self, flowv, flowu, labimg ):
 963        """ Apply the calculated optical flow on a label image """
 964        nr, nc = labimg.shape
 965        rowc, colc = np.meshgrid( np.arange(nr), np.arange(nc), indexing="ij" )
 966        lab_reg = warp( labimg, np.array( [rowc+flowv, colc+flowu] ), order=0, mode="edge" )
 967        return lab_reg
 968    
 969    def labels_to_centroids( self, start_frame, end_frame ):
 970        """ Get centroids of each cell in dataframe """
 971        regionprops = [
 972            result
 973            for frame in range(start_frame, end_frame + 1)
 974            if (result := self.label_to_dataframe(self.epicure.seg[frame], frame)) is not None
 975        ]
 976        return pd.concat(regionprops)
 977    
 978    def labels_to_centroids_flow(self, start_frame, end_frame):
 979        """ Get centroids of each cell in dataframe """
 980        regionprops = []    
 981        radius = float( self.drift_radius.text() )
 982        if self.epicure.verbose > 1:
 983            if self.drift_correction.isChecked():
 984                print( "Apply drift correction to tracking with optical flow of radius "+str(radius) )
 985        prev_movie = None
 986        flow_v = None
 987        for frame in range(start_frame, end_frame+1):
 988            if self.drift_correction.isChecked():
 989                cur_movie = self.epicure.img[frame]
 990                if frame > start_frame:
 991                    v, u = self.optical_flow( prev_movie, cur_movie, radius )
 992                    if flow_v is None:
 993                        flow_v = v
 994                        flow_u = u
 995                    else:
 996                        flow_v = flow_v + v
 997                        flow_u = flow_u + u
 998                prev_movie = cur_movie
 999            clabel = self.epicure.seg[frame]  
1000            df = self.label_to_dataframe( clabel, frame )
1001            if flow_v is not None:
1002                c0 = np.array( np.floor( df["centroid-0"] ), dtype="uint8" )
1003                c1 = np.array( np.floor( df["centroid-1"] ), dtype="uint8" )
1004                df["centroid-0"] = df["centroid-0"] - flow_v[c0,c1]
1005                df["centroid-1"] = df["centroid-1"] - flow_u[c0,c1]
1006            regionprops.append(df)
1007        regionprops_df = pd.concat(regionprops)
1008        return regionprops_df
1009    
1010    def labels_flow(self, start_frame, end_frame ):
1011        """ Get registered label image corrected for optical flow """
1012        radius = float( self.drift_radius.text() )
1013        flow_v = None
1014        prev_movie = None
1015        res_labels = []
1016        for frame in range(start_frame, end_frame+1):
1017            cur_movie = self.epicure.img[frame]
1018            if prev_movie is not None:
1019                v, u = self.optical_flow( prev_movie, cur_movie, radius )
1020                if flow_v is None:
1021                    flow_v = v
1022                    flow_u = u
1023                else:
1024                    flow_v = flow_v + v
1025                    flow_u = flow_u + u
1026            prev_movie = cur_movie
1027            clabel = np.copy( self.epicure.seg[frame] ) 
1028            if flow_v is not None:         
1029                clabel = self.apply_flow( flow_v, flow_u, clabel )
1030            res_labels.append( clabel )
1031        res_labels = np.array(res_labels)
1032        return res_labels
1033
1034    def labels_ready(self, start_frame, end_frame, locked=True):
1035        """ Get labels of unlocked cells to track """
1036        if self.drift_correction.isChecked():
1037            return self.labels_flow( start_frame, end_frame )
1038        res_labels = self.epicure.seg[start_frame:end_frame+1] 
1039        return res_labels
1040    
1041    def label_frame_todf( self, frame ):
1042        """ For current frame, get label frame image then dataframe of centroids """
1043        clabel = self.epicure.seg[frame] #self.current_label_frame(frame)
1044        return self.label_to_dataframe( clabel, frame )
1045    
1046    def current_label_frame( self, frame ):
1047        """ For current frame, get label frame image """
1048        clabel = None
1049        #if self.track_only_in_roi.isChecked():
1050        #    clabel = self.epicure.only_current_roi(frame)
1051        if clabel is None:
1052            clabel = self.epicure.seg[frame]
1053        return clabel
1054
1055    def after_tracking( self, track_df, split_df, merge_df, progress_bar, indprogress ):
1056        """ Steps after tracking: get/show the graph from the track_df """
1057        if "frame_y" in track_df.keys():
1058            track_df["frame"] = track_df["frame_y"]
1059        graph = None
1060        progress_bar.set_description( "Update labels and tracks" )
1061        ## shift all by 1 so that doesn't start at 0
1062        if len(split_df) > 0:
1063            split_df[["parent_track_id"]] += 1
1064            split_df[["child_track_id"]] += 1
1065        if len(merge_df) > 0:
1066            merge_df[["parent_track_id"]] += 1
1067            merge_df[["child_track_id"]] += 1
1068        track_df[["track_id"]] += 1
1069       
1070        ## relabel if some track have the same label
1071        self.relabel_nonunique_labels(track_df)
1072        ## relabel track ids so that they are equal to the first label of the track
1073        newtids, split_df, merge_df = self.relabel_trackids( track_df, split_df, merge_df )
1074        track_df["track_id"] = newtids
1075        self.change_labels( track_df )
1076
1077        # create graph of division/merging
1078        self.graph = to_napari_graph(split_df, merge_df)
1079
1080        progress_bar.update(indprogress+1)
1081        
1082        ## update display if active
1083        self.replace_tracks( track_df )
1084
1085        progress_bar.update(indprogress+2)
1086        ## update the list of events, or others 
1087        self.epicure.updates_after_tracking()
1088        progress_bar.update(indprogress+3)
1089        return graph
1090
1091############ Laptrack centroids option
1092    
1093    def create_laptrack_centroids(self):
1094        """ GUI of the laptrack option """
1095        self.gLapCentroids, glap_layout = wid.group_layout( "Laptrack-Centroids" )
1096        mdist, self.max_dist = wid.value_line( "Max distance", "15.0", "Maximal distance between two labels in consecutive frames to link them (in pixels)" )
1097        glap_layout.addLayout(mdist)
1098        ## splitting ~ cell division
1099        scost, self.splitting_cost = wid.value_line( "Splitting cutoff", "1", "Weight to split a track in two (increasing it favors division)" )
1100        glap_layout.addLayout(scost)
1101        ## merging ~ error ?
1102        mcost, self.merging_cost = wid.value_line( "Merging cutoff", "0", "Weight to merge to labels together" )
1103        glap_layout.addLayout(mcost)
1104
1105        add_feat, self.check_penalties, self.bpenalties = wid.checkgroup_help( "Add features cost", True, "Add cell features in the tracking calculation", None )
1106        self.create_penalties()
1107        glap_layout.addWidget(self.check_penalties)
1108        glap_layout.addWidget(self.bpenalties)
1109        self.gLapCentroids.setLayout(glap_layout)
1110
1111    def show_penalties(self):
1112        self.bpenalties.setVisible(not self.bpenalties.isVisible())
1113
1114    def create_penalties(self):
1115        pen_layout = QVBoxLayout()
1116        areaCost, self.area_cost = wid.value_line( "Area difference", "2", "Weight of the difference of area between two labels to link them (0 to ignore)" )
1117        pen_layout.addLayout(areaCost)
1118        solidCost, self.solidity_cost = wid.value_line( "Solidity difference", "0", "Weight of the difference of solidity between two labels to link them (0 to ignore)" )
1119        pen_layout.addLayout(solidCost)
1120        self.bpenalties.setLayout(pen_layout)
1121
1122    def laptrack_centroids_twoframes(self, labels, twoframes, loose=False):
1123        """ Perform tracking of two frames with strict parameters """
1124        laptrack = LaptrackCentroids(self, self.epicure)
1125        laptrack.max_distance = float(self.max_dist.text()) 
1126        if loose:
1127            laptrack.max_distance = min(50, laptrack.max_distance) ## more probable to find a parent
1128        self.region_properties = ["label", "centroid"]
1129        #if self.check_penalties.isChecked():
1130        #    self.region_properties.append("area")
1131        #    self.region_properties.append("solidity")
1132        #    laptrack.penal_area = float(self.area_cost.text())
1133        #    laptrack.penal_solidity = float(self.solidity_cost.text())
1134        #laptrack.set_region_properties(with_extra=self.check_penalties.isChecked())
1135        laptrack.set_region_properties(with_extra=False)
1136            
1137        df = self.twoframes_centroid(twoframes)
1138        if set(np.unique(df["label"])) == set(labels):
1139            ## no other labels
1140            return [None]*len(labels) 
1141        laptrack.splitting_cost = False ## disable splitting option
1142        laptrack.merging_cost = False ## disable merging option
1143        parent_labels = laptrack.twoframes_track(df, labels)
1144        return parent_labels
1145    
1146    def twoframes_centroid(self, img):
1147        """ Get centroids of first frame only """
1148        df0 = self.label_to_dataframe( img[0], 0 )
1149        df1 = self.label_to_dataframe( img[1], 1 )
1150        return pd.concat([df0, df1])
1151    
1152    def laptrack_centroids(self, start, end):
1153        """ Perform track with laptrack option and chosen parameters """
1154        ## Laptrack tracker
1155        laptrack = LaptrackCentroids(self, self.epicure)
1156        laptrack.max_distance = float(self.max_dist.text())
1157        laptrack.splitting_cost = float(self.splitting_cost.text())
1158        laptrack.merging_cost = float(self.merging_cost.text())
1159        self.region_properties = ["label", "centroid"]
1160        if self.check_penalties.isChecked():
1161            self.region_properties.append("area")
1162            self.region_properties.append("solidity")
1163            laptrack.penal_area = float(self.area_cost.text())
1164            laptrack.penal_solidity = float(self.solidity_cost.text())
1165        laptrack.set_region_properties(with_extra=self.check_penalties.isChecked())
1166
1167        progress_bar = progress(total=7)
1168        progress_bar.set_description( "Prepare tracking" )
1169        if self.epicure.verbose > 1:
1170            print("Convert labels to centroids: use track info ?")
1171        self.undrifted = False
1172        if self.drift_correction.isChecked():
1173            df = self.labels_to_centroids_flow( start, end )
1174        else:
1175            df = self.labels_to_centroids( start, end )
1176        progress_bar.update(1)
1177        if self.epicure.verbose > 1:
1178            print("GO tracking")
1179        progress_bar.set_description( "Do tracking with LapTrack Centroids" )
1180        track_df, split_df, merge_df = laptrack.track_centroids(df)
1181        progress_bar.update(2)
1182        if self.epicure.verbose > 1:
1183            print("After tracking, update everything")
1184        self.after_tracking(track_df, split_df, merge_df, progress_bar, 2)
1185        progress_bar.update(6)
1186        progress_bar.close()
1187    
1188############ Laptrack overlap option
1189
1190    def create_laptrack_overlap(self):
1191        """ GUI of the laptrack overlap option """
1192        self.gLapOverlap, glap_layout = wid.group_layout( "Laptrack-Overlaps" )
1193        miou, self.min_iou = wid.value_line( "Min IOU", "0.1", "Minimum Intersection Over Union score to link to labels together" )
1194        glap_layout.addLayout(miou)
1195        
1196        scost, self.split_cost = wid.value_line( "Splitting cost", "0.2", "Weight of linking a parent label with two labels (increasing it for more divisions)" )
1197        glap_layout.addLayout(scost)
1198        
1199        mcost, self.merg_cost = wid.value_line( "Merging cost", "0", "Weight of merging two parent labels into one" )
1200        glap_layout.addLayout(mcost)
1201
1202        self.gLapOverlap.setLayout(glap_layout)
1203
1204    def laptrack_overlaps(self, start, end):
1205        """ Perform track with laptrack overlap option and chosen parameters """
1206        ## Laptrack tracker
1207        laptrack = LaptrackOverlaps(self, self.epicure)
1208        miniou = float(self.min_iou.text())
1209        if miniou >= 1.0:
1210            miniou = 1.0
1211        laptrack.cost_cutoff = 1.0 - miniou
1212        laptrack.splitting_cost = float(self.split_cost.text())
1213        laptrack.merging_cost = float(self.merg_cost.text())
1214        self.region_properties = ["label", "centroid"]
1215
1216        progress_bar = progress(total=6)
1217        progress_bar.set_description( "Prepare tracking" )
1218        labels = self.labels_ready( start, end )
1219        self.undrifted = False
1220        progress_bar.update(1)
1221        progress_bar.set_description( "Do tracking with LapTrack Overlaps" )
1222        track_df, split_df, merge_df = laptrack.track_overlaps( labels )
1223        progress_bar.update(2)
1224        
1225        ## get dataframe of coordinates to create the graph 
1226        df = self.labels_to_centroids( start, end )
1227        self.undrifted = True
1228        progress_bar.update(3)
1229        coordinate_df = df.set_index(["frame", "label"])
1230        tdf = track_df.set_index(["frame", "label"])
1231        track_df2 = pd.merge( tdf, coordinate_df, right_index=True, left_index=True).reset_index()
1232        self.after_tracking( track_df2, split_df, merge_df, progress_bar, 3 )
1233        progress_bar.update(6)
1234        progress_bar.close()
1235    
1236    def laptrack_overlaps_twoframes(self, labels, twoframes, loose=False):
1237        """ Perform tracking of two frames with strict parameters """
1238        laptrack = LaptrackOverlaps(self, self.epicure)
1239        miniou = min( float(self.min_iou.text()), 0.9999 ) ## ensure that miniou is < 1
1240        laptrack.cost_cutoff = 1.0 - miniou
1241        if loose:
1242            laptrack.cost_cutoff = 0.95 ## more probable to find a parent/child
1243        self.region_properties = ["label", "centroid"]
1244
1245        laptrack.splitting_cost = False ## disable splitting option
1246        laptrack.merging_cost = False ## disable merging option
1247        parent_labels = laptrack.twoframes_track(twoframes, labels)
1248        return parent_labels

Handles tracking of cells, track operations with the Tracks layer

Tracking(napari_viewer, epic)
 47    def __init__(self, napari_viewer, epic):
 48        super().__init__()
 49        self.viewer = napari_viewer
 50        self.epicure = epic
 51        self.graph = None      ## init 
 52        self.tracklayer = None      ## track layer with information (centroids, labels, tree..)
 53        self.track_data = None ## keep the updated data, and update the layer only from time to time (slow to do)
 54        self.tracklayer_name = "Tracks"  ## name of the layer containing tracks
 55        self.nframes = self.epicure.nframes
 56        self.properties = ["label", "centroid"]
 57
 58        layout = QVBoxLayout()
 59        
 60        ## Add update track button 
 61        self.track_update = wid.add_button( "Update tracks display", self.update_track_layer, "Update the Track layer with the changements made since the last update" )
 62        layout.addWidget(self.track_update)
 63        
 64        ## Correct track button 
 65        #track_reset = wid.add_button( "Correct track data", self.reset_tracks, "Correct the track data after some track was lost" )
 66        #layout.addWidget(track_reset)
 67
 68        ## Method specific
 69        track_method, self.track_choice = wid.list_line( "Tracking method", "Choose the tracking method to use and display its parameter", func=None )
 70        layout.addWidget(self.track_choice)
 71        
 72        self.track_choice.addItem("Laptrack-Centroids")
 73        self.create_laptrack_centroids()
 74        layout.addWidget(self.gLapCentroids)
 75
 76        if laptrack_over: 
 77            self.track_choice.addItem("Laptrack-Overlaps")
 78            self.create_laptrack_overlap()
 79            layout.addWidget(self.gLapOverlap)
 80        else:
 81            self.min_iou = None
 82            self.split_cost = None
 83            self.merg_cost = None
 84
 85        drift_layout, self.drift_correction, self.drift_radius = wid.check_value( check="With drift correction", checked=False, value=str(50), descr="Taking into account local drift in tracking calculations") 
 86        layout.addLayout( drift_layout )
 87        
 88        self.track_go = wid.add_button( "Track", self.do_tracking, "Launch the tracking with the current parameter. Can take time" )
 89        layout.addWidget(self.track_go)
 90        self.setLayout(layout)
 91
 92        ## General tracking options
 93        frame_line, self.frame_range, self.range_group = wid.checkgroup_help( "Track only some frames", False, "Option to track only a given range of frames", None ) 
 94        self.frame_range.clicked.connect( self.show_frame_range )
 95        range_layout = QVBoxLayout()
 96        ntrack, self.start_frame = wid.ranged_value_line( "Track from frame:", 0, self.nframes-1, 1, 0, "Set first frame to begin tracking" )
 97        range_layout.addLayout(ntrack)
 98        
 99        entrack, self.end_frame = wid.ranged_value_line( "Until frame:", 1, self.nframes-1, 1, self.nframes-1, "Set the last frame unitl which to track" )
100        range_layout.addLayout(entrack)
101        self.start_frame.valueChanged.connect( self.changed_start )
102        self.end_frame.valueChanged.connect( self.changed_end )
103        
104        self.range_group.setLayout( range_layout )
105        layout.addWidget( self.frame_range )
106        layout.addWidget( self.range_group )
107        
108        self.show_frame_range()
109        self.show_trackoptions()
110        self.track_choice.currentIndexChanged.connect(self.show_trackoptions)
viewer
epicure
graph
tracklayer
track_data
tracklayer_name
nframes
properties
track_update
track_go
def show_frame_range(self):
113    def show_frame_range( self ):
114        """ Show/Hide frame range options """
115        self.range_group.setVisible( self.frame_range.isChecked() )

Show/Hide frame range options

def get_current_settings(self):
119    def get_current_settings( self ):
120        """ Get current settings to save as preferences """
121        settings = {}
122        settings["Track method"] = self.track_choice.currentText() 
123        settings["Add feat"] = self.check_penalties.isChecked()
124        settings["Max distance"] = self.max_dist.text()
125        settings["Splitting cost"] = self.splitting_cost.text()
126        settings["Merging cutoff"] = self.merging_cost.text()
127        settings["Min IOU"] = self.min_iou.text()
128        settings["Over split"] = self.split_cost.text()
129        settings["Over merge"] = self.merg_cost.text()
130        return settings

Get current settings to save as preferences

def apply_settings(self, settings):
132    def apply_settings( self, settings ):
133        """ Set the parameters/current display from the prefered settings """
134        for setty, val in settings.items():
135            if setty == "Track method":
136                self.track_choice.setCurrentText( val )
137            if setty == "Add feat":
138                self.check_penalties.setChecked( val )
139            if setty == "Max distance":
140                self.max_dist.setText( val )
141            if setty == "Splitting cost":
142                self.splitting_cost.setText( val )
143            if setty == "Merging cutoff":
144                self.merging_cost.setText( val )
145            if laptrack_over:
146                if setty == "Min IOU":
147                    self.min_iou.setText( val )
148                if setty == "Over split":
149                    self.split_cost.setText( val )
150                if setty == "Over merge":
151                    self.merg_cost.setText( val )

Set the parameters/current display from the prefered settings

def reset(self):
156    def reset( self ):
157        """ Reset Tracks layer and data """
158        self.graph = None
159        self.track_data = None
160        ut.remove_layer( self.viewer, "Tracks" )

Reset Tracks layer and data

def init_tracks(self, track_table=None, track_prop=None):
162    def init_tracks(self, track_table=None, track_prop=None ):
163        """ Add a track layer with the new tracks """
164        if track_table is None:
165            track_table, track_prop = self.create_tracks()
166        #print(track_table)
167        
168        ## plot tracks
169        if len(track_table) > 0:
170            self.clear_graph()
171            self.viewer.add_tracks(
172                track_table,
173                graph=self.graph, 
174                name=self.tracklayer_name,
175                properties = track_prop,
176                scale = self.viewer.layers["Segmentation"].scale,
177                )
178            self.viewer.layers[self.tracklayer_name].visible=True
179            self.viewer.layers[self.tracklayer_name].color_by="track_id"
180            ut.set_active_layer(self.viewer, "Segmentation")
181            self.tracklayer = self.viewer.layers[self.tracklayer_name]
182            self.track_data = self.tracklayer.data
183            #self.track.display_id = True
184            self.color_tracks_as_labels()

Add a track layer with the new tracks

def color_tracks_as_labels(self):
186    def color_tracks_as_labels(self):
187        """ Color the tracks the same as the label layer """
188        ## must color it manually by getting the Label layer colors for each track_id
189        cols = np.zeros((len(self.tracklayer.data[:,0]),4))
190        for i, tr in enumerate(self.tracklayer.data[:,0]):
191            cols[i] = (self.epicure.seglayer.get_color(tr))
192        self.tracklayer._track_colors = cols
193        self.tracklayer.events.color_by()

Color the tracks the same as the label layer

def color_tracks_by_lineage(self):
195    def color_tracks_by_lineage(self):
196        """ Color the tracks by their lineage (daughters same colors as parents) """
197        ## must color it manually by getting the Label layer colors for each track_id
198        cols = np.zeros((len(self.tracklayer.data[:,0]),4))
199        for i, tr in enumerate(self.tracklayer.data[:,0]):
200            ## find the parent cell,n going up the tree until no more parent
201            while tr in self.graph.keys():
202                tr = self.graph_parent( tr )
203            cols[i] = (self.epicure.seglayer.get_color(tr))
204        self.tracklayer._track_colors = cols
205        self.tracklayer.events.color_by()

Color the tracks by their lineage (daughters same colors as parents)

def graph_parent(self, ind):
207    def graph_parent( self, ind ):
208        """ Get the value of the parent from the graph """
209        if ind not in self.graph.keys():
210            return None
211        if isinstance(self.graph[ind], list):
212            return self.graph[ind][0]
213        return self.graph[ind]

Get the value of the parent from the graph

def replace_tracks(self, track_df):
215    def replace_tracks(self, track_df):
216        """ Replace all tracks based on the dataframe """
217        if not self.undrifted and self.drift_correction.isChecked():
218            ## recalculate the label centroids as it was corrected for drift
219            track_table, track_prop = self.create_tracks()
220        else:
221            track_table, track_prop = self.build_tracks( track_df )
222        self.tracklayer.data = track_table
223        self.track_data = self.tracklayer.data
224        self.tracklayer.properties = track_prop
225        self.tracklayer.refresh()
226        self.color_tracks_as_labels()

Replace all tracks based on the dataframe

def reset_tracks(self):
228    def reset_tracks(self):
229        """ Reset tracks and reload them from labels """
230        ut.remove_layer(self.viewer, "Tracks")
231        self.init_tracks()

Reset tracks and reload them from labels

def update_track_layer(self):
233    def update_track_layer(self):
234        """ Update the track layer (slow) """
235        self.viewer.window._status_bar._toggle_activity_dock(True)
236        progress_bar = progress(total=1)
237        progress_bar.set_description( "Updating track layer" )
238        self.tracklayer.data = self.track_data
239        progress_bar.close()
240        self.color_tracks_as_labels()
241        self.viewer.window._status_bar._toggle_activity_dock(False)

Update the track layer (slow)

def measure_intensity_features(self, feat, intimg=None, frames=None):
243    def measure_intensity_features( self, feat, intimg=None, frames=None ):
244        """ Measure mean value of a feature in a track """
245        if ( intimg is not None ):
246            if frames is None:
247                tracks = self.get_track_list()
248                seg = self.epicure.seg
249                iimg = intimg
250            else:
251                tracks = self.get_tracks_list_frames( frames )
252                seg = self.epicure.seg[frames]
253                iimg = intimg[frames]
254        if feat == "intensity_mean":
255            mean_intensities = ndi.mean( iimg, seg, tracks )
256            return tracks, mean_intensities
257        if feat == "intensity_sum":
258            sum_intensities = ndi.sum( iimg, seg, tracks )
259            return tracks, sum_intensities
260        if feat == "intensity_max":
261            sum_intensities = ndi.maximum( iimg, seg, tracks )
262            return tracks, sum_intensities
263        if feat == "intensity_min":
264            sum_intensities = ndi.minimum( iimg, seg, tracks )
265            return tracks, sum_intensities
266        if feat == "intensity_median":
267            sum_intensities = ndi.median( iimg, seg, tracks )
268            return tracks, sum_intensities
269        print( "Mean feature on track not implemented" )
270        return None

Measure mean value of a feature in a track

def measure_track_features(self, track_id, scaling=False):
272    def measure_track_features( self, track_id, scaling=False ):
273        """ Measure features (length, total displacement...) of given track """
274        features = {}
275        track = self.get_track_data( track_id )
276        if track.shape[0] == 0:
277            return features
278        track = track[track[:,1].argsort()]
279        start = int(np.min(track[:,1]))
280        end = int(np.max(track[:,1]))
281        temp_unit = ""
282        vel_unit = ""
283        disp_unit = ""
284        temp_scale = 1
285        vel_scale = 1
286        disp_scale = 1
287        if scaling:
288            temp_unit = "_"+self.epicure.epi_metadata["UnitT"]
289            vel_unit = "_"+self.epicure.epi_metadata["UnitXY"]+"/"+self.epicure.epi_metadata["UnitT"]
290            disp_unit = "_"+self.epicure.epi_metadata["UnitXY"]
291            temp_scale = self.epicure.epi_metadata["ScaleT"]
292            vel_scale = self.epicure.epi_metadata["ScaleXY"]/self.epicure.epi_metadata["ScaleT"]
293            disp_scale = self.epicure.epi_metadata["ScaleXY"]
294        features["Label"] = track_id
295        features["TrackDuration"+temp_unit] = (end - start + 1)*temp_scale
296        features["TrackStart"+temp_unit] = start * temp_scale
297        features["TrackEnd"+temp_unit] = end * temp_scale
298        features["NbGaps"] = end - start + 1 - len(track)
299        if (end-start) == 0:
300            ## only one frame
301            features["TotalDisplacement"+disp_unit] = None
302            features["NetDisplacement"+disp_unit] = None
303            features["Straightness"] = None
304            features["MeanVelocity"+vel_unit] = None
305        else:
306            features["TotalDisplacement"+disp_unit] = ut.total_distance( track[:,2:4] ) * disp_scale
307            features["NetDisplacement"+disp_unit] = ut.net_distance( track[:,2:4] ) * disp_scale
308            features["MeanVelocity"+vel_unit] = np.mean( ut.velocities( track[:,1:4] ) ) * vel_scale 
309            if features["TotalDisplacement"+disp_unit] > 0:
310                features["Straightness"] = features["NetDisplacement"+disp_unit]/features["TotalDisplacement"+disp_unit]
311            else:
312                features["Straightness"] = None
313        return features

Measure features (length, total displacement...) of given track

def measure_speed(self, track_id):
315    def measure_speed( self, track_id ):
316        """ Returns the velocities of the track """
317        track = self.get_track_data( track_id )
318        if track.shape[0] == 0:
319            return None 
320        track = track[track[:,1].argsort()]
321        return ut.velocities( track[:,1:4] )

Returns the velocities of the track

def measure_features(self, track_id, features):
323    def measure_features( self, track_id, features ):
324        """ Measure features along all the track """
325        mask = self.epicure.get_mask( track_id )
326        res = {}
327        for feat in features:
328            res[feat] = []
329        for frame in mask:
330            props = ut.labels_properties( frame )
331            if len(props) > 0:
332                if "Area" in features:
333                    res["Area"].append( props[0].area )
334                if "Hull" in features:
335                    res["Hull"].append( props[0].area_convex )
336                if "Elongation" in features:
337                    res["Elongation"].append( props[0].axis_major_length )
338                if "Eccentricity" in features:
339                    res["Eccentricity"].append( props[0].eccentricity )
340                if "Perimeter" in features:
341                    res["Perimeter"].append( props[0].perimeter )
342                if "Solidity" in features:
343                    res["Solidity"].append( props[0].solidity )
344        return res

Measure features along all the track

def measure_specific_feature(self, track_id, featureName):
346    def measure_specific_feature( self, track_id, featureName ):
347        """ Measure some specific feature """
348        if featureName == "Similarity":
349            import skimage.metrics as imetrics
350            movie = self.epicure.get_label_movie( track_id, extend=1.5 )
351            sim_scores = []
352            for i in range(0, len(movie)-1):
353                score = imetrics.normalized_mutual_information( movie[i], movie[i+1] ) 
354                sim_scores.append(score)
355            return sim_scores

Measure some specific feature

def measure_labels(self, segimg):
357    def measure_labels(self, segimg):
358        """ Get the dataframe of the labels in the segmented image """
359        resdf = None
360        for iframe, frame in progress(enumerate(segimg)):
361            frame_table = ut.labels_to_table( frame, iframe )
362            if resdf is None:
363                resdf = pd.DataFrame(frame_table)
364            else:
365                resdf = pd.concat([resdf, pd.DataFrame(frame_table)])
366        return resdf

Get the dataframe of the labels in the segmented image

def add_track_frame(self, label, frame, centroid, tree=None, group=None):
368    def add_track_frame(self, label, frame, centroid, tree=None, group=None):
369        """ Add one frame to the track """
370        new_frame = np.array([label, frame, centroid[0], centroid[1]])
371        self.track_data = np.vstack((self.track_data, new_frame))

Add one frame to the track

def get_track_list(self):
373    def get_track_list(self):
374        """ Get list of unique track_ids """
375        return np.unique( self.track_data[:,0] )

Get list of unique track_ids

def get_tracks_list_frames(self, frames):
377    def get_tracks_list_frames( self, frames ):
378        """ Return list of tracks present on list of frames """
379        return np.unique( self.track_data[ np.isin( self.track_data[:,1], frames), 0] ) 

Return list of tracks present on list of frames

def get_tracks_on_frame(self, tframe):
381    def get_tracks_on_frame( self, tframe ):
382        """ Return list of tracks present on given frame """
383        return np.unique( self.track_data[ self.track_data[:,1]==tframe, 0] ) 

Return list of tracks present on given frame

def has_track(self, label):
385    def has_track(self, label):
386        """ Test if track label is present """
387        return label in self.track_data[:,0]

Test if track label is present

def has_tracks(self, labels):
389    def has_tracks(self, labels):
390        """ Test if track labels are present """
391        return np.isin( labels, self.track_data[:,0] )

Test if track labels are present

def nb_points(self):
393    def nb_points(self):
394        """ Number of points in the tracks """
395        return self.track_data.shape[0]

Number of points in the tracks

def nb_tracks(self):
397    def nb_tracks(self):
398        """ Return number of tracks """
399        #return self.track._manager.__len__()
400        return len(self.get_track_list())

Return number of tracks

def gaped_track(self, track_id):
402    def gaped_track(self, track_id):
403        """ Check if there is a gap (missing frame) in a track """
404        indexes = self.get_track_indexes(track_id)
405        if len(indexes) <= 0:
406            return False
407        track_frames = self.track_data[indexes,1]
408        return ((np.max(track_frames)-np.min(track_frames)+1) > len(track_frames) )

Check if there is a gap (missing frame) in a track

def gap_frames(self, track_id):
410    def gap_frames(self, track_id):
411        """ Returns the frame(s) at which the gap(s) are """
412        track_frames = self.get_track_column( track_id, "frame" )
413        gaps = []
414        if len( track_frames ) > 0:
415            min_frame = int( np.min(track_frames) )
416            max_frame = int( np.max(track_frames) )
417            gaps = np.setdiff1d( np.arange(min_frame+1, max_frame), track_frames ).tolist()
418            if len(gaps) > 0:
419                gaps.sort()
420        return gaps

Returns the frame(s) at which the gap(s) are

def check_gap(self, tracks=None, verbose=None):
422    def check_gap(self, tracks=None, verbose=None):
423        """ Check if there is a track with a gap, flag it if yes """
424        if tracks is None:
425            tracks = self.get_track_list()
426        gaped = []
427        for track in tracks:
428            if self.gaped_track( track ):
429                gaped.append(track)
430        if verbose is None:
431            verbose = self.epicure.verbose
432        if verbose > 0 and len(gaped)>0:
433            ut.show_warning("Gap in track(s) "+str(gaped)+"\n"
434            +"Consider doing sanity_check in Editing onglet to fix it")
435        return gaped

Check if there is a track with a gap, flag it if yes

def get_track_indexes(self, track_id):
437    def get_track_indexes(self, track_id):
438        """ Get indexes of track_id tracks position in the arrays """
439        if isinstance( track_id,  int ):
440            return (np.flatnonzero( self.track_data[:,0] == track_id ) )
441        return (np.flatnonzero( np.isin( self.track_data[:,0], track_id ) ) )

Get indexes of track_id tracks position in the arrays

def get_track_indexes_onframes(self, track_id, frames):
443    def get_track_indexes_onframes( self, track_id, frames ):
444        """ Get indexes of track_id tracks position in the arrays """
445        if isinstance( frames, int ):
446            frames = [frames]
447        if isinstance( track_id,  int ):
448            return (np.flatnonzero( (self.track_data[:,0] == track_id) * np.isin( self.track_data[:,1], frames) ) )
449        return (np.flatnonzero( np.isin( self.track_data[:,0], track_id ) * np.isin( self.track_data[:,1], frames) ) )

Get indexes of track_id tracks position in the arrays

def get_track_indexes_from_frame(self, track_id, frame):
451    def get_track_indexes_from_frame(self, track_id, frame):
452        """ Get indexes of track_id tracks position in the arrays from the given frame """
453        if type(track_id) == int:
454            return (np.argwhere( (self.track_data[:,0] == track_id)*(self.track_data[:,1]>= frame) )).flatten()
455        return (np.argwhere( np.isin( self.track_data[:,0], track_id )*(self.track_data[:,1]>= frame) )).flatten()

Get indexes of track_id tracks position in the arrays from the given frame

def get_index(self, track_id, frame):
457    def get_index(self, track_id, frame ):
458        """ Get index of track_id at given frame """
459        if np.isscalar(track_id):
460            track_id = [track_id]
461        return np.argwhere( (np.isin(self.track_data[:,0], track_id))*(self.track_data[:,1] == frame) )

Get index of track_id at given frame

def get_small_tracks(self, max_length=1):
463    def get_small_tracks(self, max_length=1):
464        """ Get tracks smaller than the given threshold """
465        labels = []
466        lengths = []
467        positions = []
468        for lab in self.get_track_list():
469            indexes = self.get_track_indexes(lab)
470            length = len(indexes)
471            if length <= max_length:
472                pos = self.mean_position( indexes, only_first=False )
473                labels.append(lab)
474                lengths.append(length)
475                positions.append(pos)
476        return labels, lengths, positions

Get tracks smaller than the given threshold

def get_track_data(self, track_id):
478    def get_track_data(self, track_id):
479        """ Return the data of track track_id """
480        indexes = self.get_track_indexes( track_id )
481        track = self.track_data[indexes,]
482        return track

Return the data of track track_id

def get_track_column(self, track_id, column):
484    def get_track_column( self, track_id, column ):
485        """ Return the chosen column (frame, x, y, label) of track track_id """
486        indexes = self.get_track_indexes( track_id )
487        if column == "frame":
488            return self.track_data[indexes, 1]
489        if column == "label":
490            return self.track_data[indexes, 0]
491        if column == "pos":
492            return self.track_data[indexes, 2:4]
493        if column == "fullpos":
494            return self.track_data[indexes, 1:4]
495        track = self.track_data[indexes]
496        return track

Return the chosen column (frame, x, y, label) of track track_id

def get_frame_data(self, track_id, ind):
498    def get_frame_data( self, track_id, ind ):
499        """ Get ind-th data of track track_id """
500        track = self.get_track_data( track_id )
501        return track[ind]

Get ind-th data of track track_id

def get_middle_position(self, track_id, framea, frameb):
503    def get_middle_position( self, track_id, framea, frameb ):
504        """ Get track position in middle of frame a and frame b """
505        inda = self.get_index( track_id, framea ) 
506        indb = self.get_index( track_id, frameb )
507        return self.mean_position( np.ravel( np.vstack((inda, indb)) ), only_first=False )

Get track position in middle of frame a and frame b

def get_position(self, track_id, frame):
509    def get_position( self, track_id, frame ):
510        """ Get position of the track at given frame """
511        ind = self.get_index( track_id, frame )
512        ind = ind.flatten()[0] ## ensure it's single element
513        x,y = self.track_data[ind,2], self.track_data[ind,3]
514        return [int(x), int(y)]

Get position of the track at given frame

def get_full_position(self, track_id, frame):
516    def get_full_position( self, track_id, frame ):
517        """ Get position of the track at given frame, with the frame itself """
518        ind = self.get_index( track_id, frame )
519        ind = ind.flatten()[0] ## ensure it's single element
520        x,y = self.track_data[ind,2], self.track_data[ind,3]
521        return [frame,x,y]

Get position of the track at given frame, with the frame itself

def mean_position(self, indexes, only_first=False):
523    def mean_position(self, indexes, only_first=False):
524        """ Mean positions of tracks at indexes """
525        if len(indexes) <= 0:
526            return None
527        track = self.track_data[indexes,]
528        ## keep only the first frame of the tracks
529        if only_first:
530            min_frame = np.min(track[:,1])
531            track = track[track[:,1]==min_frame,]
532        return ( int(np.mean(track[:,1])), int(np.mean(track[:,2])), int(np.mean(track[:,3])) )

Mean positions of tracks at indexes

def get_first_frame(self, track_id):
534    def get_first_frame(self, track_id):
535        """ Returns first frame where track_id is present """
536        track = self.get_track_data( track_id )
537        if len(track) <= 0:
538            return None
539        return int( np.min(track[:,1]) )

Returns first frame where track_id is present

def is_in_frame(self, track_id, frame):
541    def is_in_frame( self, track_id, frame ):
542        """ Returns if track_id is present at given frame """
543        track = self.get_track_data( track_id )
544        if len(track) > 0:
545            return frame in track[:,1]
546        return False

Returns if track_id is present at given frame

def get_last_frame(self, track_id):
548    def get_last_frame(self, track_id):
549        """ Returns last frame where track_id is present """
550        track = self.get_track_data( track_id )
551        if len(track) > 0:
552            return int(np.max(track[:,1]))
553        return None

Returns last frame where track_id is present

def get_extreme_frames(self, track_id):
555    def get_extreme_frames(self, track_id):
556        """ Returns the first and last frames where track_id is present """
557        track = self.get_track_data( track_id )
558        if track.shape[0] > 0:
559            return (int(np.min(track[:,1])), int(np.max(track[:,1])) )
560        return None, None

Returns the first and last frames where track_id is present

def get_mean_position(self, track_id, only_first=False):
562    def get_mean_position(self, track_id, only_first=False):
563        """ Get mean position of the track """
564        indexes = self.get_track_indexes( track_id )
565        return self.mean_position( indexes, only_first )

Get mean position of the track

def update_centroid(self, track_id, frame, ind=None, cx=None, cy=None):
567    def update_centroid(self, track_id, frame, ind=None, cx=None, cy=None):
568        """ Update track at given frame """
569        if ind is None:
570            ind = self.get_index( track_id, frame )
571        if cx is None:
572            prop = ut.getPropLabel( self.epicure.seg[frame], track_id )
573            self.track_data[ind, 2:4] = prop.centroid[1]
574        else:
575            self.track_data[ind, 2] = cx
576            self.track_data[ind, 3] = cy

Update track at given frame

def replace_on_frames(self, tids, new_tids, frames):
578    def replace_on_frames( self, tids, new_tids, frames ):
579        """ Replace the id tid by new_tid in all given frames """
580        ind = self.get_track_indexes_onframes( tids, frames )
581        cur_track = np.copy(self.track_data[ind])
582        new_ids = np.repeat(-1, len(ind))
583        for tid, new_tid in zip(tids, new_tids):
584            self.update_graph_frames( tid, cur_track[cur_track[:,0]==tid,1] )
585            new_ids[cur_track[:,0]==tid] = new_tid
586        self.track_data[ind, 0] = new_ids

Replace the id tid by new_tid in all given frames

def swap_frame_id(self, tid, otid, frame):
588    def swap_frame_id(self, tid, otid, frame):
589        """ Swap the ids of two tracks at frame """
590        ind = int(self.get_index(tid, frame))
591        oind = int(self.get_index(otid, frame))
592        ## check if one of the label is an extreme of a track and potentially in the graph
593        for track_index in [tid, otid]:
594            min_frame, max_frame = self.get_extreme_frames( track_index )
595            if (min_frame == frame) or (max_frame == frame):
596                self.update_graph( track_index, frame )
597        self.track_data[[ind, oind],0] = [otid, tid]

Swap the ids of two tracks at frame

def update_track_on_frame(self, track_ids, frame):
599    def update_track_on_frame(self, track_ids, frame):
600        """ Update (add or modify) tracks at given frame """
601        frame_table = ut.labels_table( labimg = np.where(np.isin(self.epicure.seg[frame], track_ids), self.epicure.seg[frame], 0), properties=self.properties )
602        for x, y, tid in zip(frame_table["centroid-0"], frame_table["centroid-1"], frame_table["label"]):
603            index = self.get_index(tid, frame)
604            if len(index) > 0:
605                self.update_centroid( tid, frame, index, int(x), int(y) )
606            else:
607                cur_cell = np.array( [[tid, frame, int(x), int(y)]] )
608                self.track_data = np.append(self.track_data, cur_cell, axis=0)

Update (add or modify) tracks at given frame

def add_tracks_fromindices(self, indices, track_ids):
610    def add_tracks_fromindices( self, indices, track_ids ):
611        """ Add tracks of given track ids from the indices"""
612        new_data = np.empty( (0,4), int )
613        for tid in np.unique(track_ids):
614            keep = track_ids == tid 
615            for frame in np.unique( indices[0][keep] ):
616                cent0 = np.mean( indices[1][keep] ) 
617                cent1 = np.mean( indices[2][keep] ) 
618                new_data = np.append( new_data, np.array([[tid, frame, int(cent0), int(cent1)]]), axis=0 )
619        self.track_data = np.append( self.track_data, new_data, axis=0)

Add tracks of given track ids from the indices

def add_one_frame(self, track_ids, frame, refresh=True):
621    def add_one_frame(self, track_ids, frame, refresh=True):
622        """ Add one frame from track """
623        for tid in track_ids:
624            frame_table = ut.labels_table( np.uint8(self.epicure.seg[frame]==tid), properties=self.properties ) 
625            cur_cell = np.array( [tid, frame, int(frame_table["centroid-0"]), int(frame_table["centroid-1"])], dtype=np.uint32 )
626            cur_cell = np.expand_dims(cur_cell, axis=0)
627            self.track_data = np.append(self.track_data, cur_cell, axis=0)

Add one frame from track

def remove_one_frame(self, track_id, frame, handle_gaps=False, refresh=True):
629    def remove_one_frame( self, track_id, frame, handle_gaps=False, refresh=True ):
630        """ 
631        Remove one frame from track(s) 
632        """
633        inds = self.get_index( track_id, frame )
634        if np.isscalar(track_id):
635            track_id = [track_id]
636        check_for_gaps = False
637        for tid in track_id:
638            ## removed frame is in the extremity of a track, can be in the graph
639            first_frame, last_frame = self.get_extreme_frames( tid )
640            if first_frame is None:
641                continue
642            if (first_frame == frame) or (last_frame == frame):
643                self.update_graph( tid, frame )
644            else:
645                check_for_gaps = True
646        self.track_data = np.delete( self.track_data, inds, axis=0 )
647        ## gaps might have been created in the tracks, for now doesn't allow it so split the tracks
648        if handle_gaps and check_for_gaps:
649            gaped = self.check_gap( track_id, verbose=0 )
650            if len(gaped) > 0:
651                self.epicure.fix_gaps( gaped )

Remove one frame from track(s)

def get_current_value(self, track_id, frame):
653    def get_current_value(self, track_id, frame):
654        ind = self.get_index(track_id, frame)
655        centx, centy = self.track_data[ind, 2:4].astype(int).flatten()
656        return self.epicure.seg[frame, centx, centy]
def clear_graph(self):
658    def clear_graph( self ):
659        """ Check the state of the graph and removes non existing keys or values """
660        if self.graph is None:
661            return
662        keys = list(self.graph.keys())
663        for key in keys:
664            if key not in self.track_data[:,0]:
665                del self.graph[key]
666            else:
667                vals = self.graph[key]
668                if isinstance(vals, list):
669                    for val in vals:
670                        if val not in self.track_data[:,0]:
671                            del self.graph[key]
672                            break
673                else:
674                    if vals not in self.track_data[:,0]:
675                        del self.graph[key]

Check the state of the graph and removes non existing keys or values

def set_graph(self, graph):
677    def set_graph(self, graph):
678        """ Set the current graph (eg imported from TrackMate XML file) """
679        self.graph = graph
680        ## set the divisions from the graph
681        self.epicure.inspecting.get_divisions()

Set the current graph (eg imported from TrackMate XML file)

def update_graph_frames(self, track_id, frames):
683    def update_graph_frames( self, track_id, frames ):
684        """ Update graph when one label was deleted at given frames """
685        fframe = np.min(frames)
686        lframe = np.max(frames)
687        self.update_graph( track_id, fframe )
688        self.update_graph( track_id, lframe )

Update graph when one label was deleted at given frames

def update_graph(self, track_id, frame):
690    def update_graph(self, track_id, frame):
691        """ Update graph if deleted label was linked at that frame, assume keys are unique """
692        if self.graph is not None:
693            ## handles current node is last of his branch
694            parents = self.last_in_graph( track_id, frame )
695            current_label = self.get_current_value( track_id, frame )
696            for parent in parents:
697                if current_label == 0:
698                    del self.graph[parent]
699                else:
700                    self.update_child( parent, track_id, current_label )
701            ## handles when current track is first frame of a division
702            if self.first_in_graph( track_id, frame ):
703                if current_label == 0:
704                    del self.graph[track_id]
705                else:
706                    self.update_key( track_id, current_label ) 

Update graph if deleted label was linked at that frame, assume keys are unique

def update_child(self, parent, prev_key, new_key):
708    def update_child(self, parent, prev_key, new_key):
709        """ Change the value of a key in the graph """
710        if isinstance(self.graph[parent], list):
711            self.graph[parent] = [new_key if val == prev_key else val for val in self.graph[parent]]
712        else:
713            if self.graph[parent] == prev_key:
714                self.graph[parent] = new_key

Change the value of a key in the graph

def update_key(self, prev_key, new_key):
716    def update_key(self, prev_key, new_key):
717        """ Change the value of a key in the graph """
718        self.graph[new_key] = self.graph.pop(prev_key)

Change the value of a key in the graph

def is_parent(self, cur_id):
720    def is_parent( self, cur_id ):
721        """ Return if the current id is in the graph (as a parent, so in values) """
722        if self.graph is None:
723            return False
724        return any( cur_id in vals if isinstance(vals, list) else cur_id in [vals] for vals in self.graph.values() )

Return if the current id is in the graph (as a parent, so in values)

def add_division(self, childa, childb, parent):
726    def add_division( self, childa, childb, parent ):
727        """ Add info of a division to the graph of divisions/merges """
728        if self.graph is None:
729            self.graph = {}
730        self.graph.update({childa: [parent], childb: [parent]})

Add info of a division to the graph of divisions/merges

def remove_division(self, parent):
732    def remove_division( self, parent ):
733        """ Remove a division event from the graph """
734        self.graph = {key: vals for key, vals in self.graph.items() if not ( self.graph_parent(key) == parent )  }

Remove a division event from the graph

def last_in_graph(self, track_id, frame=None, check_last=True):
736    def last_in_graph(self, track_id, frame=None, check_last=True):
737        """ Check if given label and frame is the last of a branch, in the graph """
738        if check_last:
739            return [key for key, vals in self.graph.items() if track_id in (vals if isinstance(vals, list) else [vals]) and self.get_last_frame(track_id) == frame]
740        return [key for key, vals in self.graph.items() if track_id in (vals if isinstance(vals, list) else [vals])]

Check if given label and frame is the last of a branch, in the graph

def first_in_graph(self, track_id, frame=None, check_first=True):
742    def first_in_graph(self, track_id, frame=None, check_first=True):
743        """ Check if the given label and frame is the first in the branch so the node in the graph """
744        if check_first:
745            return track_id in self.graph and self.get_first_frame(track_id) == frame
746        return track_id in self.graph

Check if the given label and frame is the first in the branch so the node in the graph

def remove_on_frames(self, track_ids, frames):
748    def remove_on_frames( self, track_ids, frames ):
749        """ Remove tracks with given id on specified frames """
750        track_ids = track_ids.tolist()
751        if 0 in track_ids:
752            track_ids.remove(0)
753        inds = self.get_track_indexes_onframes( track_ids, frames )
754        for tid in track_ids:
755            self.update_graph_frames( tid, frames )
756        self.track_data = np.delete( self.track_data, inds, axis=0 )

Remove tracks with given id on specified frames

def remove_tracks(self, track_ids):
758    def remove_tracks(self, track_ids):
759        """ Remove track with given id """
760        inds = self.get_track_indexes(track_ids)
761        self.track_data = np.delete(self.track_data, inds, axis=0)
762        self.remove_ids_from_graph( track_ids )

Remove track with given id

def remove_ids_from_graph(self, track_ids):
764    def remove_ids_from_graph( self, track_ids ):
765        """ Remove all ids from the graph """
766        track_ids_set = set( track_ids )
767        if self.graph is not None:
768            self.graph = {
769                key: vals for key, vals in self.graph.items()
770                if (key not in track_ids_set) and ( not any( val in track_ids_set for val in (vals if isinstance(vals, list) else [vals])) )
771            }

Remove all ids from the graph

def is_single_parent(self, cur_id):
773    def is_single_parent( self, cur_id ):
774        """ Return if the current id is in the graph (as a single parent, not a merge) """
775        if self.graph is None:
776            return False
777        return any( cur_id in [vals] if not isinstance(vals, list) else (cur_id in vals and len(vals)==1) for vals in self.graph.values() )

Return if the current id is in the graph (as a single parent, not a merge)

def build_tracks(self, track_df):
780    def build_tracks(self, track_df):
781        """ Create tracks from dataframe (after tracking) """
782        track = track_df[["track_id", "frame", "centroid-0", "centroid-1"]]
783        #frame_prop = frame_table[["tree_id", "label", "nframes", "group"]]
784        return np.array(track, int), None #dict(frame_prop)

Create tracks from dataframe (after tracking)

def create_tracks(self):
786    def create_tracks(self):
787        """ Create tracks from labels (without tracking) """
788        #track_table = np.empty( (0,4), int )   
789        labels = self.epicure.seg
790        total = self.epicure.nframes
791        if self.epicure.process_parallel:
792            track_tables = Parallel( n_jobs=self.epicure.nparallel ) (
793                delayed(ut.labels_to_table)(frame, iframe ) for iframe, frame in enumerate(labels)
794            )
795        else:
796            track_tables = [ ut.labels_to_table( frame, iframe) for iframe, frame in progress(enumerate(labels), total=total) ]
797        track_table = np.concatenate( [ tab for tab in track_tables if tab.shape[0] != 0 ], axis=0 ) # handle empty frame
798        return track_table, None # track_prop

Create tracks from labels (without tracking)

def add_track_features(self, labels):
800    def add_track_features(self, labels):
801        """ Add features specific to tracks (eg nframes) """
802        nframes = np.zeros(len(labels), int)
803        if self.epicure.verbose > 2:
804            print("REPLACE BY COUNT METHOD")
805        for track_id in np.unique(labels):
806            cur_track = np.argwhere(labels == track_id)
807            nframes[ list(cur_track) ] = len(cur_track)
808        return nframes

Add features specific to tracks (eg nframes)

def changed_start(self, i):
814    def changed_start(self, i):
815        """ Ensures that end frame > start frame """
816        if i > self.end_frame.value():
817            self.end_frame.setValue(i+1)

Ensures that end frame > start frame

def changed_end(self, i):
819    def changed_end(self, i):
820        if i < self.start_frame.value():
821            self.start_frame.setValue(i-1)
def find_parents(self, labels, twoframes):
823    def find_parents(self, labels, twoframes):
824        """ Find in the first frame the parents of labels from second frame """
825        
826        if self.track_choice.currentText() == "Laptrack-Centroids":
827            return self.laptrack_centroids_twoframes(labels, twoframes, loose=True)
828        
829        if self.track_choice.currentText() == "Laptrack-Overlaps":
830            return self.laptrack_overlaps_twoframes(labels, twoframes, loose=True)

Find in the first frame the parents of labels from second frame

def do_tracking(self):
833    def do_tracking(self):
834        """ Start the tracking with the selected options """
835        if self.frame_range.isChecked():
836            start = self.start_frame.value()
837            end = self.end_frame.value()
838        else:
839            start = 0
840            end = self.nframes-1
841        start_time = ut.start_time()
842        self.viewer.window._status_bar._toggle_activity_dock(True)
843        self.epicure.inspecting.reset_all_events()
844        
845        if self.track_choice.currentText() == "Laptrack-Centroids":
846            if self.epicure.verbose > 1:
847                print("Starting track with Laptrack-Centroids")
848            self.laptrack_centroids( start, end )
849            self.epicure.tracked = 1
850        if self.track_choice.currentText() == "Laptrack-Overlaps":
851            if self.epicure.verbose > 1:
852                print("Starting track with Laptrack-Centroids")
853            self.laptrack_overlaps( start, end )
854            self.epicure.tracked = 1
855        
856        self.epicure.finish_update(contour=2)
857        #self.epicure.reset_free_label()
858        self.viewer.window._status_bar._toggle_activity_dock(False)
859        if self.epicure.verbose > 0:
860            ut.show_duration( start_time, header="Tracking done in " )

Start the tracking with the selected options

def show_trackoptions(self):
862    def show_trackoptions(self):
863        self.gLapCentroids.setVisible(self.track_choice.currentText() == "Laptrack-Centroids")
864        if laptrack_over:
865            self.gLapOverlap.setVisible(self.track_choice.currentText() == "Laptrack-Overlaps")
def relabel_nonunique_labels(self, track_df):
867    def relabel_nonunique_labels(self, track_df):
868        """ After tracking, some track can be splitted and get same label, fix that """
869        inittids = np.unique(track_df["track_id"])
870        labtracks = []
871        saved_data = np.copy(self.epicure.seglayer.data)
872        mframes = []
873        tids = []
874        used = np.unique( saved_data )
875        all_labels = np.unique(track_df["label"])
876        for tid in inittids:
877            cdf = track_df[track_df["track_id"]==tid]
878            #print(cdf)
879            min_frame = np.min( cdf["frame"] )
880            #labtrack = int( cdf["label"][cdf["frame"]==min_frame] )
881            for lab in np.unique(cdf["label"]):
882                labtracks.append(lab)
883                mframes.append( min_frame )
884                tids.append(tid)
885        if len(labtracks) != len(np.unique(labtracks)):
886            ## some labels are present several times
887            used = used.tolist()
888            for lab in all_labels :
889                indexes = list(np.where(np.array(labtracks)==lab)[0])
890                if len(indexes)>1:
891                    minframes = [mframes[indy] for indy in range(len(labtracks)) if labtracks[indy]==lab]
892                    indmin = indexes[ np.argmin( minframes ) ]
893                    ## for the other(s), change the label
894                    newvals = ut.get_free_labels( used, len(indexes) )
895                    used = used + newvals
896                    for i, ind in enumerate(indexes):
897                        if ind != indmin:
898                            tid = tids[ind]
899                            newval = newvals[i]
900                            track_df.loc[ (track_df["track_id"]==tid)  & (track_df["label"]==lab) , "label"] = newval
901                            for frame in track_df["frame"][(track_df["track_id"]==tid) & (track_df["label"]==newval)]:
902                                mask = (saved_data[frame]==lab)
903                                self.epicure.seglayer.data[frame][mask] = newval

After tracking, some track can be splitted and get same label, fix that

def relabel_trackids(self, track_df, splitdf, mergedf):
906    def relabel_trackids(self, track_df, splitdf, mergedf):
907        """ Change the trackids to take the first label of each track """
908        start_time = ut.start_time()
909        new_trackids = track_df['track_id'].copy()
910        new_splitdf = splitdf.copy()
911        new_mergedf = mergedf.copy()
912        
913        unique_track_ids = np.unique(track_df['track_id'])
914        if ut.version_python_minor(10):
915            ## from python3.10, get futurewarning on groupby without group_keys and include_groups keywords
916            first_labels = track_df.groupby('track_id', group_keys=False).apply(lambda x: x.loc[x['frame'].idxmin(), 'label'], include_groups=False).to_dict()
917        else:
918            first_labels = track_df.groupby('track_id').apply(lambda x: x.loc[x['frame'].idxmin(), 'label']).to_dict()
919        
920        for tid in unique_track_ids:
921            newval = first_labels[tid]
922            if tid != newval:
923                new_trackids[track_df['track_id'] == tid] = newval
924                if not new_splitdf.empty:
925                    new_splitdf.loc[splitdf["parent_track_id"] == tid, "parent_track_id"] = newval
926                    new_splitdf.loc[splitdf["child_track_id"] == tid, "child_track_id"] = newval
927                if not new_mergedf.empty:
928                    new_mergedf.loc[mergedf["parent_track_id"] == tid, "parent_track_id"] = newval
929                    new_mergedf.loc[mergedf["child_track_id"] == tid, "child_track_id"] = newval
930        if self.epicure.verbose > 1:
931            ut.show_duration( start_time, header="Relabeling done in " )            
932        return new_trackids, new_splitdf, new_mergedf

Change the trackids to take the first label of each track

def change_labels(self, track_df):
934    def change_labels(self, track_df):
935        """ Change the labels at each frame according to tracks """
936        for frame, frame_df in track_df.groupby("frame"):
937            self.change_frame_labels(frame, frame_df)

Change the labels at each frame according to tracks

def change_frame_labels(self, frame, frame_df):
939    def change_frame_labels(self, frame, frame_df):
940        """ Change the labels at given frame according to tracks """
941        track_ids = frame_df['track_id'].astype(int).values
942        old_labels = frame_df["label"].astype(int).values
943        seglayer = np.copy(self.epicure.seglayer.data[frame])
944        for old_lab, new_lab in zip(old_labels, track_ids):
945            mask = (seglayer==old_lab)
946            self.epicure.seglayer.data[frame][mask] = new_lab

Change the labels at given frame according to tracks

def label_to_dataframe(self, labimg, frame):
948    def label_to_dataframe( self, labimg, frame ):
949        """ from label, get dataframe of centroids with properties """
950        df = pd.DataFrame( ut.labels_table(labimg, properties=self.region_properties) )
951        if df.shape[0] == 0:
952            ## no labels in this frame
953            return None
954        df["frame"] = frame
955        return df

from label, get dataframe of centroids with properties

def optical_flow(self, img0, img1, radius):
957    def optical_flow( self, img0, img1, radius ):
958        """ Compute the optical flow between two images """
959        v, u = optical_flow_ilk( img0, img1, radius=radius)
960        return v, u

Compute the optical flow between two images

def apply_flow(self, flowv, flowu, labimg):
962    def apply_flow( self, flowv, flowu, labimg ):
963        """ Apply the calculated optical flow on a label image """
964        nr, nc = labimg.shape
965        rowc, colc = np.meshgrid( np.arange(nr), np.arange(nc), indexing="ij" )
966        lab_reg = warp( labimg, np.array( [rowc+flowv, colc+flowu] ), order=0, mode="edge" )
967        return lab_reg

Apply the calculated optical flow on a label image

def labels_to_centroids(self, start_frame, end_frame):
969    def labels_to_centroids( self, start_frame, end_frame ):
970        """ Get centroids of each cell in dataframe """
971        regionprops = [
972            result
973            for frame in range(start_frame, end_frame + 1)
974            if (result := self.label_to_dataframe(self.epicure.seg[frame], frame)) is not None
975        ]
976        return pd.concat(regionprops)

Get centroids of each cell in dataframe

def labels_to_centroids_flow(self, start_frame, end_frame):
 978    def labels_to_centroids_flow(self, start_frame, end_frame):
 979        """ Get centroids of each cell in dataframe """
 980        regionprops = []    
 981        radius = float( self.drift_radius.text() )
 982        if self.epicure.verbose > 1:
 983            if self.drift_correction.isChecked():
 984                print( "Apply drift correction to tracking with optical flow of radius "+str(radius) )
 985        prev_movie = None
 986        flow_v = None
 987        for frame in range(start_frame, end_frame+1):
 988            if self.drift_correction.isChecked():
 989                cur_movie = self.epicure.img[frame]
 990                if frame > start_frame:
 991                    v, u = self.optical_flow( prev_movie, cur_movie, radius )
 992                    if flow_v is None:
 993                        flow_v = v
 994                        flow_u = u
 995                    else:
 996                        flow_v = flow_v + v
 997                        flow_u = flow_u + u
 998                prev_movie = cur_movie
 999            clabel = self.epicure.seg[frame]  
1000            df = self.label_to_dataframe( clabel, frame )
1001            if flow_v is not None:
1002                c0 = np.array( np.floor( df["centroid-0"] ), dtype="uint8" )
1003                c1 = np.array( np.floor( df["centroid-1"] ), dtype="uint8" )
1004                df["centroid-0"] = df["centroid-0"] - flow_v[c0,c1]
1005                df["centroid-1"] = df["centroid-1"] - flow_u[c0,c1]
1006            regionprops.append(df)
1007        regionprops_df = pd.concat(regionprops)
1008        return regionprops_df

Get centroids of each cell in dataframe

def labels_flow(self, start_frame, end_frame):
1010    def labels_flow(self, start_frame, end_frame ):
1011        """ Get registered label image corrected for optical flow """
1012        radius = float( self.drift_radius.text() )
1013        flow_v = None
1014        prev_movie = None
1015        res_labels = []
1016        for frame in range(start_frame, end_frame+1):
1017            cur_movie = self.epicure.img[frame]
1018            if prev_movie is not None:
1019                v, u = self.optical_flow( prev_movie, cur_movie, radius )
1020                if flow_v is None:
1021                    flow_v = v
1022                    flow_u = u
1023                else:
1024                    flow_v = flow_v + v
1025                    flow_u = flow_u + u
1026            prev_movie = cur_movie
1027            clabel = np.copy( self.epicure.seg[frame] ) 
1028            if flow_v is not None:         
1029                clabel = self.apply_flow( flow_v, flow_u, clabel )
1030            res_labels.append( clabel )
1031        res_labels = np.array(res_labels)
1032        return res_labels

Get registered label image corrected for optical flow

def labels_ready(self, start_frame, end_frame, locked=True):
1034    def labels_ready(self, start_frame, end_frame, locked=True):
1035        """ Get labels of unlocked cells to track """
1036        if self.drift_correction.isChecked():
1037            return self.labels_flow( start_frame, end_frame )
1038        res_labels = self.epicure.seg[start_frame:end_frame+1] 
1039        return res_labels

Get labels of unlocked cells to track

def label_frame_todf(self, frame):
1041    def label_frame_todf( self, frame ):
1042        """ For current frame, get label frame image then dataframe of centroids """
1043        clabel = self.epicure.seg[frame] #self.current_label_frame(frame)
1044        return self.label_to_dataframe( clabel, frame )

For current frame, get label frame image then dataframe of centroids

def current_label_frame(self, frame):
1046    def current_label_frame( self, frame ):
1047        """ For current frame, get label frame image """
1048        clabel = None
1049        #if self.track_only_in_roi.isChecked():
1050        #    clabel = self.epicure.only_current_roi(frame)
1051        if clabel is None:
1052            clabel = self.epicure.seg[frame]
1053        return clabel

For current frame, get label frame image

def after_tracking(self, track_df, split_df, merge_df, progress_bar, indprogress):
1055    def after_tracking( self, track_df, split_df, merge_df, progress_bar, indprogress ):
1056        """ Steps after tracking: get/show the graph from the track_df """
1057        if "frame_y" in track_df.keys():
1058            track_df["frame"] = track_df["frame_y"]
1059        graph = None
1060        progress_bar.set_description( "Update labels and tracks" )
1061        ## shift all by 1 so that doesn't start at 0
1062        if len(split_df) > 0:
1063            split_df[["parent_track_id"]] += 1
1064            split_df[["child_track_id"]] += 1
1065        if len(merge_df) > 0:
1066            merge_df[["parent_track_id"]] += 1
1067            merge_df[["child_track_id"]] += 1
1068        track_df[["track_id"]] += 1
1069       
1070        ## relabel if some track have the same label
1071        self.relabel_nonunique_labels(track_df)
1072        ## relabel track ids so that they are equal to the first label of the track
1073        newtids, split_df, merge_df = self.relabel_trackids( track_df, split_df, merge_df )
1074        track_df["track_id"] = newtids
1075        self.change_labels( track_df )
1076
1077        # create graph of division/merging
1078        self.graph = to_napari_graph(split_df, merge_df)
1079
1080        progress_bar.update(indprogress+1)
1081        
1082        ## update display if active
1083        self.replace_tracks( track_df )
1084
1085        progress_bar.update(indprogress+2)
1086        ## update the list of events, or others 
1087        self.epicure.updates_after_tracking()
1088        progress_bar.update(indprogress+3)
1089        return graph

Steps after tracking: get/show the graph from the track_df

def create_laptrack_centroids(self):
1093    def create_laptrack_centroids(self):
1094        """ GUI of the laptrack option """
1095        self.gLapCentroids, glap_layout = wid.group_layout( "Laptrack-Centroids" )
1096        mdist, self.max_dist = wid.value_line( "Max distance", "15.0", "Maximal distance between two labels in consecutive frames to link them (in pixels)" )
1097        glap_layout.addLayout(mdist)
1098        ## splitting ~ cell division
1099        scost, self.splitting_cost = wid.value_line( "Splitting cutoff", "1", "Weight to split a track in two (increasing it favors division)" )
1100        glap_layout.addLayout(scost)
1101        ## merging ~ error ?
1102        mcost, self.merging_cost = wid.value_line( "Merging cutoff", "0", "Weight to merge to labels together" )
1103        glap_layout.addLayout(mcost)
1104
1105        add_feat, self.check_penalties, self.bpenalties = wid.checkgroup_help( "Add features cost", True, "Add cell features in the tracking calculation", None )
1106        self.create_penalties()
1107        glap_layout.addWidget(self.check_penalties)
1108        glap_layout.addWidget(self.bpenalties)
1109        self.gLapCentroids.setLayout(glap_layout)

GUI of the laptrack option

def show_penalties(self):
1111    def show_penalties(self):
1112        self.bpenalties.setVisible(not self.bpenalties.isVisible())
def create_penalties(self):
1114    def create_penalties(self):
1115        pen_layout = QVBoxLayout()
1116        areaCost, self.area_cost = wid.value_line( "Area difference", "2", "Weight of the difference of area between two labels to link them (0 to ignore)" )
1117        pen_layout.addLayout(areaCost)
1118        solidCost, self.solidity_cost = wid.value_line( "Solidity difference", "0", "Weight of the difference of solidity between two labels to link them (0 to ignore)" )
1119        pen_layout.addLayout(solidCost)
1120        self.bpenalties.setLayout(pen_layout)
def laptrack_centroids_twoframes(self, labels, twoframes, loose=False):
1122    def laptrack_centroids_twoframes(self, labels, twoframes, loose=False):
1123        """ Perform tracking of two frames with strict parameters """
1124        laptrack = LaptrackCentroids(self, self.epicure)
1125        laptrack.max_distance = float(self.max_dist.text()) 
1126        if loose:
1127            laptrack.max_distance = min(50, laptrack.max_distance) ## more probable to find a parent
1128        self.region_properties = ["label", "centroid"]
1129        #if self.check_penalties.isChecked():
1130        #    self.region_properties.append("area")
1131        #    self.region_properties.append("solidity")
1132        #    laptrack.penal_area = float(self.area_cost.text())
1133        #    laptrack.penal_solidity = float(self.solidity_cost.text())
1134        #laptrack.set_region_properties(with_extra=self.check_penalties.isChecked())
1135        laptrack.set_region_properties(with_extra=False)
1136            
1137        df = self.twoframes_centroid(twoframes)
1138        if set(np.unique(df["label"])) == set(labels):
1139            ## no other labels
1140            return [None]*len(labels) 
1141        laptrack.splitting_cost = False ## disable splitting option
1142        laptrack.merging_cost = False ## disable merging option
1143        parent_labels = laptrack.twoframes_track(df, labels)
1144        return parent_labels

Perform tracking of two frames with strict parameters

def twoframes_centroid(self, img):
1146    def twoframes_centroid(self, img):
1147        """ Get centroids of first frame only """
1148        df0 = self.label_to_dataframe( img[0], 0 )
1149        df1 = self.label_to_dataframe( img[1], 1 )
1150        return pd.concat([df0, df1])

Get centroids of first frame only

def laptrack_centroids(self, start, end):
1152    def laptrack_centroids(self, start, end):
1153        """ Perform track with laptrack option and chosen parameters """
1154        ## Laptrack tracker
1155        laptrack = LaptrackCentroids(self, self.epicure)
1156        laptrack.max_distance = float(self.max_dist.text())
1157        laptrack.splitting_cost = float(self.splitting_cost.text())
1158        laptrack.merging_cost = float(self.merging_cost.text())
1159        self.region_properties = ["label", "centroid"]
1160        if self.check_penalties.isChecked():
1161            self.region_properties.append("area")
1162            self.region_properties.append("solidity")
1163            laptrack.penal_area = float(self.area_cost.text())
1164            laptrack.penal_solidity = float(self.solidity_cost.text())
1165        laptrack.set_region_properties(with_extra=self.check_penalties.isChecked())
1166
1167        progress_bar = progress(total=7)
1168        progress_bar.set_description( "Prepare tracking" )
1169        if self.epicure.verbose > 1:
1170            print("Convert labels to centroids: use track info ?")
1171        self.undrifted = False
1172        if self.drift_correction.isChecked():
1173            df = self.labels_to_centroids_flow( start, end )
1174        else:
1175            df = self.labels_to_centroids( start, end )
1176        progress_bar.update(1)
1177        if self.epicure.verbose > 1:
1178            print("GO tracking")
1179        progress_bar.set_description( "Do tracking with LapTrack Centroids" )
1180        track_df, split_df, merge_df = laptrack.track_centroids(df)
1181        progress_bar.update(2)
1182        if self.epicure.verbose > 1:
1183            print("After tracking, update everything")
1184        self.after_tracking(track_df, split_df, merge_df, progress_bar, 2)
1185        progress_bar.update(6)
1186        progress_bar.close()

Perform track with laptrack option and chosen parameters

def create_laptrack_overlap(self):
1190    def create_laptrack_overlap(self):
1191        """ GUI of the laptrack overlap option """
1192        self.gLapOverlap, glap_layout = wid.group_layout( "Laptrack-Overlaps" )
1193        miou, self.min_iou = wid.value_line( "Min IOU", "0.1", "Minimum Intersection Over Union score to link to labels together" )
1194        glap_layout.addLayout(miou)
1195        
1196        scost, self.split_cost = wid.value_line( "Splitting cost", "0.2", "Weight of linking a parent label with two labels (increasing it for more divisions)" )
1197        glap_layout.addLayout(scost)
1198        
1199        mcost, self.merg_cost = wid.value_line( "Merging cost", "0", "Weight of merging two parent labels into one" )
1200        glap_layout.addLayout(mcost)
1201
1202        self.gLapOverlap.setLayout(glap_layout)

GUI of the laptrack overlap option

def laptrack_overlaps(self, start, end):
1204    def laptrack_overlaps(self, start, end):
1205        """ Perform track with laptrack overlap option and chosen parameters """
1206        ## Laptrack tracker
1207        laptrack = LaptrackOverlaps(self, self.epicure)
1208        miniou = float(self.min_iou.text())
1209        if miniou >= 1.0:
1210            miniou = 1.0
1211        laptrack.cost_cutoff = 1.0 - miniou
1212        laptrack.splitting_cost = float(self.split_cost.text())
1213        laptrack.merging_cost = float(self.merg_cost.text())
1214        self.region_properties = ["label", "centroid"]
1215
1216        progress_bar = progress(total=6)
1217        progress_bar.set_description( "Prepare tracking" )
1218        labels = self.labels_ready( start, end )
1219        self.undrifted = False
1220        progress_bar.update(1)
1221        progress_bar.set_description( "Do tracking with LapTrack Overlaps" )
1222        track_df, split_df, merge_df = laptrack.track_overlaps( labels )
1223        progress_bar.update(2)
1224        
1225        ## get dataframe of coordinates to create the graph 
1226        df = self.labels_to_centroids( start, end )
1227        self.undrifted = True
1228        progress_bar.update(3)
1229        coordinate_df = df.set_index(["frame", "label"])
1230        tdf = track_df.set_index(["frame", "label"])
1231        track_df2 = pd.merge( tdf, coordinate_df, right_index=True, left_index=True).reset_index()
1232        self.after_tracking( track_df2, split_df, merge_df, progress_bar, 3 )
1233        progress_bar.update(6)
1234        progress_bar.close()

Perform track with laptrack overlap option and chosen parameters

def laptrack_overlaps_twoframes(self, labels, twoframes, loose=False):
1236    def laptrack_overlaps_twoframes(self, labels, twoframes, loose=False):
1237        """ Perform tracking of two frames with strict parameters """
1238        laptrack = LaptrackOverlaps(self, self.epicure)
1239        miniou = min( float(self.min_iou.text()), 0.9999 ) ## ensure that miniou is < 1
1240        laptrack.cost_cutoff = 1.0 - miniou
1241        if loose:
1242            laptrack.cost_cutoff = 0.95 ## more probable to find a parent/child
1243        self.region_properties = ["label", "centroid"]
1244
1245        laptrack.splitting_cost = False ## disable splitting option
1246        laptrack.merging_cost = False ## disable merging option
1247        parent_labels = laptrack.twoframes_track(twoframes, labels)
1248        return parent_labels

Perform tracking of two frames with strict parameters