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
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
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)
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
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
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
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
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
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
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)
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
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
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
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
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
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)
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
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
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
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
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)
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
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
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
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
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
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
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
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)
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)
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)
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
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
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
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
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
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
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