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