epicure.inspecting
** EpiCure - Inspect panel interface **
Handle supects and events detection layer. Inspection functions propose to look for user selected features to detect potential errors in the segmentration from inspecting the tracks.
Extrusion events are detected as disappearance of a track & a cell size small enough. Divisions events can be detected by the tracking algorithm directly and saved in the track graph, or they can be detected by the inspection as the disappearance of one track at the same time that two new tracks appear in the same area.
Inpsection could also be performed on a static image by looking at outlier values of a selected feature.
1""" 2 ** EpiCure - Inspect panel interface ** 3 4 Handle supects and events detection layer. 5 Inspection functions propose to look for user selected features to detect potential errors in the segmentration from inspecting the tracks. 6 7 Extrusion events are detected as disappearance of a track & a cell size small enough. 8 Divisions events can be detected by the tracking algorithm directly and saved in the track graph, or they can be detected by the inspection as the disappearance of one track at the same time that two new tracks appear in the same area. 9 10 Inpsection could also be performed on a static image by looking at outlier values of a selected feature. 11""" 12 13 14import numpy as np 15from skimage import filters 16from skimage.measure import regionprops 17from skimage.morphology import binary_erosion, binary_dilation, disk 18from qtpy.QtWidgets import QVBoxLayout, QWidget, QLabel 19from napari.utils import progress 20import epicure.Utils as ut 21import epicure.epiwidgets as wid 22import time 23from joblib import Parallel, delayed 24 25class Inspecting(QWidget): 26 27 def __init__(self, napari_viewer, epic): 28 """ 29 Generate the graphical interface for the inspection panel, and initialize the events layer. 30 """ 31 super().__init__() 32 self.viewer = napari_viewer 33 self.epicure = epic 34 self.seglayer = self.viewer.layers["Segmentation"] 35 self.border_cells = None ## list of cells that are on the image border 36 self.boundary_cells = None ## list of cells that are on the boundary (touch the background) 37 self.eventlayer_name = "Events" 38 self.events = None 39 self.win_size = 10 40 self.event_class = self.epicure.event_class 41 42 ## Print the current number of events 43 self.nevents_print = QLabel("") 44 self.update_nevents_display() 45 46 self.create_eventlayer() 47 layout = QVBoxLayout() 48 layout.addWidget( self.nevents_print ) 49 50 ## Reset or update some events 51 update_events_choice = wid.add_button( btn="Reset/Update some events...", btn_func=self.reset_events_choice, descr="Pops up an interface to choose which event(s) to remove or update" ) 52 layout.addWidget( update_events_choice ) 53 layout.addWidget( wid.separation() ) 54 55 56 ## choose events to display 57 show_label = wid.label_line( "Show events:" ) 58 layout.addWidget( show_label ) 59 show_line = wid.hlayout() 60 self.show_class = [] 61 for i, eclass in enumerate(self.event_class) : 62 check = wid.add_check_tolayout( show_line, eclass, True, None, "Show/hide the "+eclass ) 63 check.stateChanged.connect( lambda state, i=i, eclass=eclass: self.show_hide_events(i, eclass) ) 64 self.show_class.append( check ) 65 layout.addLayout( show_line ) 66 67 ## Visualisation options 68 disp_line, self.event_disp, self.displayevent = wid.checkgroup_help( "Display options", False, "Show/hide event display options panel", "event#visualisation", self.epicure.display_colors, "group3" ) 69 self.create_displayeventBlock() 70 layout.addLayout( disp_line ) 71 layout.addWidget(self.displayevent) 72 73 layout.addWidget( wid.separation() ) 74 ## Error suggestions based on cell features 75 outlier_line, self.outlier_vis, self.featOutliers = wid.checkgroup_help( "Outlier options", False, "Show/Hide outlier options panel", "event#frame-based-events", self.epicure.display_colors, "group" ) 76 layout.addLayout( outlier_line ) 77 self.create_outliersBlock() 78 layout.addWidget(self.featOutliers) 79 80 ## Error suggestions based on tracks 81 track_line, self.track_vis, self.eventTrack = wid.checkgroup_help( "Track options", True, "Show/hide track options", "event#track-based-events", self.epicure.display_colors, "group2" ) 82 self.create_tracksBlock() 83 layout.addLayout( track_line ) 84 layout.addWidget(self.eventTrack) 85 86 self.setLayout(layout) 87 self.key_binding() 88 89 def key_binding(self): 90 """ active key bindings (keyboard and mouse shortcuts) for events options """ 91 sevents = self.epicure.shortcuts["Events"] 92 self.epicure.overtext["events"] = "---- Events editing ---- \n" 93 self.epicure.overtext["events"] += ut.print_shortcuts( sevents ) 94 95 @self.epicure.seglayer.mouse_drag_callbacks.append 96 def handle_event(seglayer, event): 97 if event.type == "mouse_press": 98 ## remove a event 99 if ut.shortcut_click_match( sevents["delete"], event ): 100 ind = ut.getCellValue( self.events, event ) 101 if self.epicure.verbose > 1: 102 print("Removing clicked event, at index "+str(ind)) 103 if ind is None: 104 ## click was not on a event 105 return 106 sid = self.events.properties["id"][ind] 107 if sid is not None: 108 self.exonerate_one(ind, remove_division=True) 109 self.update_nevents_display() 110 else: 111 if self.epicure.verbose > 1: 112 print("event with id "+str(sid)+" not found") 113 self.events.refresh() 114 return 115 116 ## zoom on a event 117 if ut.shortcut_click_match( sevents["zoom"], event ): 118 ind = ut.getCellValue( self.events, event ) 119 if "id" not in self.events.properties.keys(): 120 print("No event under click") 121 return 122 sid = self.events.properties["id"][ind] 123 if self.epicure.verbose > 1: 124 print("Zoom on event with id "+str(sid)+"") 125 self.zoom_on_event( event.position, sid ) 126 return 127 128 @self.epicure.seglayer.bind_key( sevents["next"]["key"], overwrite=True ) 129 def go_next(seglayer): 130 """ Select next suspect event and zoom on it """ 131 num_event = int(self.event_num.value()) 132 nevents = self.nb_events() 133 if num_event < 0: 134 if self.nb_events( only_suspect=True ) == 0: 135 if self.epicure.verbose > 0: 136 print("No more suspect event") 137 return 138 else: 139 self.event_num.setValue(0) 140 else: 141 self.event_num.setValue( (num_event+1)%nevents ) 142 self.skip_nonselected_event( nevents, min(nevents,3000) ) 143 self.go_to_event() 144 145 def skip_nonselected_event( self, nevents, left ): 146 """ Skip next event if not a selected one (show event is not checked) """ 147 if left < 0: 148 return 0 149 150 index = int(self.event_num.value()) 151 nothing_showed = True 152 for i, curclass in enumerate(self.show_class): 153 if curclass.isChecked(): 154 nothing_showed = False 155 break 156 if nothing_showed: 157 ## nothing is shown, then go through all events 158 self.event_num.setValue( index ) 159 return index 160 161 event_class = self.get_event_class( index ) 162 ## Show only if show event class is selected 163 if self.show_class[ event_class ].isChecked(): 164 self.event_num.setValue( index ) 165 return index 166 ## else go to next event 167 index = (index + 1)%nevents 168 self.event_num.setValue( index ) 169 return self.skip_nonselected_event( nevents, left-1 ) 170 171 172 def create_eventlayer(self): 173 """ Create a point layer that contains the events """ 174 features = {} 175 pts = [] 176 self.events = self.viewer.add_points( np.array(pts), properties=features, face_color="red", size = 10, symbol='x', name=self.eventlayer_name, scale=self.viewer.layers["Segmentation"].scale ) 177 self.event_types = {} 178 self.update_nevents_display() 179 self.epicure.finish_update() 180 181 def load_events(self, pts, features, event_types): 182 """ Load events data from file and reinitialize layer with it""" 183 ut.remove_layer(self.viewer, self.eventlayer_name) 184 symbols = np.repeat("x", len(pts)) 185 colors = np.repeat("white", len(pts)) 186 self.events = self.viewer.add_points( np.array(pts), properties=features, face_color=colors, size = 10, symbol=symbols, name=self.eventlayer_name, scale=self.viewer.layers["Segmentation"].scale ) 187 self.event_types = event_types 188 189 ## set the display of division events 190 self.events.selected_data = {} 191 self.select_feature_event( "division" ) 192 self.events.current_symbol = "o" 193 self.events.current_face_color = "#0055ffff" 194 self.events.selected_data = {} 195 self.select_feature_event( "extrusion" ) 196 self.events.current_symbol = "diamond" 197 self.events.current_face_color = "red" 198 self.events.refresh() 199 self.update_nevents_display() 200 self.show_hide_events() 201 self.epicure.finish_update() 202 203 204 ############### Display event options 205 def get_event_types( self ): 206 """ Returns the list of possible event types """ 207 return list( self.event_types.keys() ) 208 209 def update_nevents_display( self ): 210 """ Update the display of number of event""" 211 text = str(self.nb_events(only_suspect=True))+" suspects | " 212 text += str(self.nb_type("division"))+" divisions | " 213 text += str(self.nb_type("extrusion"))+" extrusions" 214 self.nevents_print.setText( text ) 215 216 def nb_events( self, only_suspect=False ): 217 """ Returns current number of events """ 218 if self.events is None: 219 return 0 220 if self.events.properties is None: 221 return 0 222 if "score" not in self.events.properties: 223 return 0 224 if not only_suspect: 225 return len(self.events.properties["score"]) 226 return ( len(self.events.properties["score"]) - self.nb_type("division") - self.nb_type("extrusion") ) 227 228 def get_events_from_type( self, feature ): 229 """ Return the list of events of a given type """ 230 if feature == "suspect": 231 sub_features = self.suspect_subtypes() 232 evts_id = [] 233 for feat in sub_features: 234 evts_id.extend( eid for eid in self.event_types[ feat ] if eid not in evts_id ) 235 return list( evts_id ) 236 if feature in self.event_types: 237 return self.event_types[ feature ] 238 return [] 239 240 def nb_type( self, feature ): 241 """ Return nb of event of given type """ 242 if self.events is None: 243 return 0 244 if (self.event_types is None) or (feature not in self.event_types): 245 return 0 246 return len(self.event_types[feature]) 247 248 def create_displayeventBlock(self): 249 ''' Block interface of displaying event layer options ''' 250 disp_layout = QVBoxLayout() 251 252 ## Color mode 253 colorlay, self.color_choice = wid.list_line( "Color by:", "Choose color to display the events", self.color_events ) 254 self.color_choice.addItem("None") 255 self.color_choice.addItem("score") 256 self.color_choice.addItem("track-2->1") 257 self.color_choice.addItem("track-1-2-*") 258 self.color_choice.addItem("track-length") 259 self.color_choice.addItem("track-gap") 260 self.color_choice.addItem("track-jump") 261 self.color_choice.addItem("division") 262 self.color_choice.addItem("area") 263 self.color_choice.addItem("solidity") 264 self.color_choice.addItem("intensity") 265 self.color_choice.addItem("tubeness") 266 disp_layout.addLayout(colorlay) 267 268 esize = int(self.epicure.reference_size/70+10) 269 msize = 100 270 if esize > 70: 271 msize = 200 272 esize = min( esize, 100 ) 273 sizelay, self.event_size = wid.slider_line( "Point size:", minval=0, maxval=msize, step=1, value=esize, show_value=True, slidefunc=self.display_event_size, descr="Choose the current point size display" ) 274 disp_layout.addLayout(sizelay) 275 276 ### Interface to select a event and zoom on it 277 chooselay, self.event_num = wid.ranged_value_line( label="event n°", minval=0, maxval=1000000, step=1, val=0, descr="Choose current event to display/remove" ) 278 disp_layout.addLayout(chooselay) 279 go_event_btn = wid.add_button( "Go to event", self.go_to_event, "Zoom and display current event" ) 280 disp_layout.addWidget(go_event_btn) 281 clear_event_btn = wid.add_button( "Remove current event", self.clear_event, "Delete current event from the list of events" ) 282 disp_layout.addWidget(clear_event_btn) 283 284 ## all features 285 self.displayevent.setLayout(disp_layout) 286 self.displayevent.setVisible( self.event_disp.isChecked() ) 287 288 ##### 289 def reset_event_range(self): 290 """ Reset the max num of event """ 291 nsus = len(self.events.data)-1 292 if self.event_num.value() > nsus: 293 self.event_num.setValue(0) 294 self.event_num.setMaximum(nsus) 295 296 def go_to_event(self): 297 """ Zoom on the currently selected event """ 298 num_event = int(self.event_num.value()) 299 ## if reached the end of possible events 300 if num_event >= self.nb_events(): 301 num_event = 0 302 self.event_num.setValue(0) 303 if num_event < 0: 304 if self.nb_events() == 0: 305 if self.epicure.verbose > 0: 306 print("No more event") 307 return 308 else: 309 self.event_num.setValue(0) 310 num_event = 0 311 pos = self.events.data[num_event] 312 event_id = self.events.properties["id"][num_event] 313 self.zoom_on_event( pos, event_id ) 314 315 def get_event_infos( self, sid ): 316 """ Get the properties of the event of given id """ 317 index = self.index_from_id( sid ) 318 pos = self.events.data[ index ] 319 label = self.events.properties[ "label" ][index] 320 return pos, label 321 322 def zoom_on_event( self, event_pos, event_id ): 323 """ Zoom on chose event at given position """ 324 evt_lay = self.viewer.layers[self.eventlayer_name] 325 epos = evt_lay.data_to_world(event_pos) 326 #pos = event_pos 327 #print(epos) 328 self.viewer.camera.center = tuple(epos) 329 self.viewer.camera.zoom = 5/self.epicure.epi_metadata["ScaleXY"] 330 ut.set_frame( self.viewer, int(epos[0]) ) 331 crimes = self.get_crimes(event_id) 332 if self.epicure.verbose > 0: 333 print("Suspected because of: "+str(crimes)) 334 335 def color_events(self): 336 """ Color points by the selected mode """ 337 color_mode = self.color_choice.currentText() 338 self.events.refresh_colors() 339 if color_mode == "None": 340 self.events.face_color = "white" 341 elif color_mode == "score": 342 self.set_colors_from_properties("score") 343 else: 344 self.set_colors_from_event_type(color_mode) 345 self.events.refresh_colors() 346 347 def suspect_subtypes( self ): 348 """ Return the list of suspect-related event types """ 349 features = list( self.event_types.keys() ) 350 if "division" in features: 351 features.remove( "division" ) 352 if "extrusion" in features: 353 features.remove( "extrusion" ) 354 return features 355 356 def show_subset_event( self, feature, show=True ): 357 """ Show/hide a subset (type) of event """ 358 tmp_size = int(self.event_size.value()) 359 size = 0.1 360 if show: 361 size = tmp_size 362 ## select the events of corresponding type 363 self.events.selected_data = {} 364 if not isinstance( feature, list ): 365 features = [feature] 366 else: 367 features = feature 368 if "suspect" in features: 369 ## take all possible features except non-suspect ones (division, extrusion..) 370 features.remove( "suspect" ) 371 features = features + self.suspect_subtypes() 372 373 posids = [] 374 for feat in features: 375 if feat in self.event_types: 376 posid = self.event_types[feat] 377 posids = posids + posid 378 nfound = len(posids) 379 if nfound <= 0: 380 return 381 for ind, cid in enumerate( self.events.properties["id"] ): 382 if cid in posids: 383 self.events._size[ind] = size 384 nfound = nfound - 1 385 ## finished, all updated 386 if nfound == 0: 387 break 388 ## reset selection and default size 389 self.events.selected_data = {} 390 self.events.current_size = tmp_size 391 self.events.refresh() 392 393 def select_feature_event( self, feature ): 394 """ Add all event of given feature to currently selected data """ 395 if feature not in self.event_types: 396 return 397 posid = self.event_types[feature] 398 nfound = len(posid) 399 for ind, cid in enumerate(self.events.properties["id"]): 400 if cid in posid: 401 self.events.selected_data.add( ind ) 402 nfound = nfound - 1 403 ## stop if found all of them 404 if nfound == 0: 405 return 406 407 def set_colors_from_event_type(self, feature): 408 """ Set colors from given event_type feature (eg area, tracking..) """ 409 if self.event_types.get(feature) is None: 410 self.events.face_color="white" 411 return 412 posid = self.event_types[feature] 413 colors = ["white"]*len(self.events.data) 414 ## change the color of all the positive events for the chosen feature 415 for sid in posid: 416 ind = self.index_from_id(sid) 417 if ind is not None: 418 colors[ind] = (0.8,0.1,0.1) 419 self.events.face_color = colors 420 421 def set_colors_from_properties(self, feature): 422 """ Set colors from given propertie (eg score, label) """ 423 ncols = (np.max(self.events.properties[feature])) 424 color_cycle = [] 425 for i in range(ncols): 426 color_cycle.append( (0.25+float(i/ncols*0.75), float(i/ncols*0.85), float(i/ncols*0.75)) ) 427 self.events.face_color_cycle = color_cycle 428 self.events.face_color = feature 429 430 def update_display(self): 431 """ Update the display of the events layer """ 432 self.events.refresh() 433 self.color_events() 434 435 def get_current_settings(self): 436 """ Returns current event widget parameters """ 437 disp = {} 438 disp["Point size"] = int(self.event_size.value()) 439 disp["Outliers ON"] = self.outlier_vis.isChecked() 440 disp["Track ON"] = self.track_vis.isChecked() 441 disp["EventDisp ON"] = self.event_disp.isChecked() 442 for i, eclass in enumerate(self.event_class): 443 disp["Show "+eclass] = self.show_class[i].isChecked() 444 disp["Ignore border"] = self.ignore_borders.isChecked() 445 disp["Ignore boundaries"] = self.ignore_boundaries.isChecked() 446 disp["Flag length"] = self.check_length.isChecked() 447 disp["Flag jump"] = self.check_jump.isChecked() 448 disp["length"] = self.min_length.text() 449 disp["Check size"] = self.check_size.isChecked() 450 disp["Check shape"] = self.check_shape.isChecked() 451 disp["Get merging"] = self.get_merge.isChecked() 452 disp["Get apparitions"] = self.get_apparition.isChecked() 453 disp["Get divisions"] = self.get_division.isChecked() 454 disp["Get disparitions"] = self.get_disparition.isChecked() 455 disp["Get extrusions"] = self.get_extrusions.isChecked() 456 disp["Get gaps"] = self.get_gaps.isChecked() 457 disp["threshold disparition"] = self.threshold_disparition.text() 458 disp["Min gap"] = self.min_gaps.text() 459 disp["Min area"] = self.min_area.text() 460 disp["Max area"] = self.max_area.text() 461 disp["Current frame"] = self.feat_onframe.isChecked() 462 return disp 463 464 def apply_settings( self, settings ): 465 """ Set the current state (display, widget) from preferences if any """ 466 for setting, val in settings.items(): 467 if setting == "Outliers ON": 468 self.outlier_vis.setChecked( val ) 469 if setting == "Track ON": 470 self.track_vis.setChecked( val ) 471 if setting =="EventDisp ON": 472 self.event_disp.setChecked( val ) 473 if setting == "Point size": 474 self.event_size.setValue( int(val) ) 475 #self.display_event_size() 476 for i, eclass in enumerate(self.event_class): 477 if setting == "Show "+eclass: 478 self.show_class[i].setChecked( val ) 479 #self.show_hide_events() 480 if setting == "Ignore border": 481 self.ignore_borders.setChecked( val ) 482 if setting == "Ignore boundaries": 483 self.ignore_boundaries.setChecked( val ) 484 if setting == "Flag length": 485 self.check_length.setChecked( val ) 486 if setting == "Flag jump": 487 self.check_jump.setChecked( val ) 488 if setting == "length": 489 self.min_length.setText( val ) 490 if setting == "Check size": 491 self.check_size.setChecked( val ) 492 if setting == "Check shape": 493 self.check_shape.setChecked( val ) 494 if setting == "Get merging": 495 self.get_merge.setChecked( val ) 496 if setting == "Get apparitions": 497 self.get_apparition.setChecked( val ) 498 if setting == "Get divisions": 499 self.get_division.setChecked( val ) 500 if setting == "Get disparitions": 501 self.get_disparition.setChecked( val ) 502 if setting == "Get extrusions": 503 self.get_extrusions.setChecked( val ) 504 if setting == "Get gaps": 505 self.get_gaps.setChecked( val ) 506 if setting == "Threshold disparition": 507 self.threshold_disparition.setText( val ) 508 if setting == "Min gap": 509 self.min_gaps.setText( val ) 510 if setting == "Min area": 511 self.min_area.setText( val ) 512 if setting == "Max area": 513 self.max_area.setText( val ) 514 if setting == "Current frame": 515 self.feat_onframe.setChecked( val ) 516 517 518 def display_event_size(self): 519 """ Change the size of the point display """ 520 size = int(self.event_size.value()) 521 self.events.size = size 522 self.events.refresh() 523 #### Depend on event type, to update 524 525 ############### eventing functions 526 def get_crimes(self, sid): 527 """ For a given event, get its event_type(s) """ 528 crimes = [] 529 for feat in self.event_types.keys(): 530 if sid in self.event_types.get(feat): 531 crimes.append(feat) 532 return crimes 533 534 def add_event_type(self, ind, sid, feature): 535 """ Add 1 to the event_type score for given feature """ 536 #print(self.event_types) 537 if self.event_types.get(feature) is None: 538 self.event_types[feature] = [] 539 self.event_types[feature].append(sid) 540 score = self.events.properties["score"].copy() 541 score[ind] = score[ind] + 1 542 self.events.properties["score"] = score 543 self.events.properties["score"].flags.writeable = True 544 #self.events.properties() 545 546 def first_event(self, pos, label, featurename): 547 """ Addition of the first event (initialize all) """ 548 ut.remove_layer(self.viewer, "Events") 549 features = {} 550 sid = self.new_event_id() 551 features["id"] = np.array([sid], dtype="uint16") 552 features["label"] = np.array([label], dtype=self.epicure.dtype) 553 features["score"] = np.array([0], dtype="uint8") 554 pts = [pos] 555 self.events = self.viewer.add_points( np.array(pts), properties=features, face_color="score", size = int( self.event_size.value() ), symbol="x", name="Events", scale=self.viewer.layers["Segmentation"].scale ) 556 props = self.events.properties 557 props["label"].flags.writeable = True 558 props["score"].flags.writeable = True 559 props["id"].flags.writeable = True 560 self.add_event_type(0, sid, featurename) 561 self.events.refresh() 562 self.update_nevents_display() 563 564 def add_event(self, pos, label, reason, symb="x", color="white", force=False, refresh=True): 565 """ Add a event to the list, evented by a feature """ 566 if (not force) and (self.ignore_borders.isChecked()) and (self.border_cells is not None): 567 tframe = int(pos[0]) 568 if label in self.border_cells[tframe]: 569 return 570 571 if (not force) and (self.ignore_boundaries.isChecked()) and (self.boundary_cells is not None): 572 tframe = int(pos[0]) 573 if label in self.boundary_cells[tframe]: 574 return 575 576 ## initialise if necessary 577 if len(self.events.data) <= 0: 578 self.first_event(pos, label, reason) 579 return 580 581 self.events.selected_data = [] 582 583 ## look if already evented, then add the charge 584 num, sid = self.find_event(pos[0], label) 585 if num is not None: 586 ## event already in the list. For same crime ? 587 if self.event_types.get(reason) is not None: 588 if sid not in self.event_types[reason]: 589 self.add_event_type(num, sid, reason) 590 else: 591 self.add_event_type(num, sid, reason) 592 else: 593 ## new event, add to the Point layer 594 ind = len(self.events.data) 595 sid = self.new_event_id() 596 self.events.add(pos) 597 props = self.events.properties 598 props["label"].flags.writeable = True 599 props["score"].flags.writeable = True 600 props["id"].flags.writeable = True 601 props["label"][ind] = label 602 props["id"][ind] = sid 603 props["score"][ind] = 0 604 self.add_event_type(ind, sid, reason) 605 606 self.events.symbol.flags.writeable = True 607 self.events.current_symbol = symb 608 self.events.current_face_color = color 609 if refresh: 610 self.refresh_events() 611 612 def refresh_events( self ): 613 """ Refresh event view and text """ 614 self.events.refresh() 615 self.update_nevents_display() 616 self.reset_event_range() 617 self.epicure.finish_update() 618 619 def new_event_id(self): 620 """ Find the first unused id """ 621 sid = 0 622 if self.events.properties.get("id") is None: 623 return 0 624 while sid in self.events.properties["id"]: 625 sid = sid + 1 626 return sid 627 628 def reset_events_choice( self ): 629 """ Interface to choose event(s) to reset/update """ 630 631 class ResetChoice( QWidget ): 632 """ Choices of event(s) and update or reset """ 633 def __init__( self, insp ): 634 super().__init__() 635 self.insp = insp 636 poplayout = wid.vlayout() 637 638 ## Handle division events 639 update_div_btn = wid.add_button( btn="Update divisions from graph", btn_func=self.insp.get_divisions, descr="Update the list of division events from the track graph" ) 640 poplayout.addWidget(update_div_btn) 641 poplayout.addWidget( wid.separation() ) 642 643 ### Reset: delete all events 644 reset_color = self.insp.epicure.get_resetbtn_color() 645 reset_event_btn = wid.add_button( btn="Reset all events", btn_func=self.insp.reset_all_events, descr="Delete all current events", color=reset_color ) 646 poplayout.addWidget( reset_event_btn ) 647 648 ## Reset: specific events 649 reset_line = wid.hlayout() 650 for i, eclass in enumerate( self.insp.event_class ): 651 go_btn = wid.add_button( btn="Reset "+eclass, btn_func=None, descr="Reset "+eclass+" events only", color=reset_color ) 652 go_btn.clicked.connect( lambda i=i, eclass=eclass: self.insp.reset_event_type( eclass, frame=None ) ) 653 reset_line.addWidget( go_btn ) 654 poplayout.addLayout( reset_line ) 655 656 poplayout.addWidget( wid.separation() ) 657 ## Remove events on border 658 bord_lab = wid.label_line( "Remove if on BORDER:") 659 bord_line = wid.hlayout() 660 for i, eclass in enumerate( self.insp.event_class ): 661 if eclass != "suspect": 662 go_btn = wid.add_button( btn=""+eclass, btn_func=None, descr="Remove "+eclass+" events if they are on border" ) 663 go_btn.clicked.connect( lambda i=i, eclass=eclass: self.insp.remove_event_border( eclass ) ) 664 bord_line.addWidget( go_btn ) 665 poplayout.addWidget( bord_lab ) 666 poplayout.addLayout( bord_line ) 667 668 ## Remove events on boundaries 669 bound_lab = wid.label_line( "Remove if on BOUNDARY:") 670 bound_line = wid.hlayout() 671 for i, eclass in enumerate( self.insp.event_class ): 672 if eclass != "suspect": 673 go_btn = wid.add_button( btn=""+eclass, btn_func=None, descr="Remove "+eclass+" events if they are on boundary" ) 674 go_btn.clicked.connect( lambda i=i, eclass=eclass: self.insp.remove_event_boundary( eclass ) ) 675 bound_line.addWidget( go_btn ) 676 poplayout.addWidget( bound_lab ) 677 poplayout.addLayout( bound_line ) 678 poplayout.addWidget( wid.separation() ) 679 680 681 self.setLayout( poplayout ) 682 683 #def close( self ): 684 # """ Close the pop-up window """ 685 # self.hide() 686 rc = ResetChoice( self ) 687 rc.show() 688 689 690 def remove_event_border( self, evt_type ): 691 """ Remove events of given types if they are on border cells """ 692 if self.event_types.get( evt_type ) is None: 693 return 694 695 pbar = ut.start_progress( self.viewer, total=self.epicure.nframes+1, descr="Detecting border cells" ) 696 ## get/update the list of border cells 697 self.get_border_cells() 698 699 ## check all event_type events if they are on border cells 700 idlist = self.event_types[ evt_type ].copy() 701 for sid in idlist: 702 ind = self.index_from_id(sid) 703 if ind is not None: 704 ## get the event cell label and frame 705 lab = self.events.properties["label"][ind] 706 frame = self.events.data[ind][0] 707 if evt_type == "division": 708 frame = frame - 1 709 if frame is not None: 710 if lab in self.border_cells[ frame ]: 711 ## event is on border, remove it 712 self.event_types[ evt_type ].remove( sid ) 713 self.decrease_score( ind ) 714 715 ## update displays 716 ut.close_progress( self.viewer, pbar ) 717 self.events.refresh() 718 self.update_nevents_display() 719 720 def remove_event_boundary( self, evt_type ): 721 """ Remove events of given types if they are on boundary cells """ 722 if self.event_types.get( evt_type ) is None: 723 return 724 725 pbar = ut.start_progress( self.viewer, total=self.epicure.nframes+1, descr="Detecting boundary cells" ) 726 ## get/update the list of border cells 727 self.get_boundaries_cells( pbar ) 728 729 ## check all event_type events if they are on border cells 730 idlist = self.event_types[ evt_type ].copy() 731 for sid in idlist: 732 ind = self.index_from_id(sid) 733 if ind is not None: 734 ## get the event cell label and frame 735 lab = self.events.properties["label"][ind] 736 frame = self.events.data[ind][0] 737 if evt_type == "division": 738 frame = frame - 1 739 if frame is not None: 740 if lab in self.boundary_cells[ frame ]: 741 ## event is on border, remove it 742 self.event_types[ evt_type ].remove( sid ) 743 self.decrease_score( ind ) 744 745 ## update displays 746 ut.close_progress( self.viewer, pbar ) 747 self.events.refresh() 748 self.update_nevents_display() 749 750 def reset_all_events(self): 751 """ Remove all event_types """ 752 features = {} 753 pts = [] 754 ut.remove_layer(self.viewer, "Events") 755 self.events = self.viewer.add_points( np.array(pts), properties=features, face_color="red", size = 10, symbol='x', name="Events", scale=self.viewer.layers["Segmentation"].scale ) 756 self.event_types = {} 757 self.update_nevents_display() 758 #self.update_nevents_display() 759 self.epicure.finish_update() 760 761 def reset_event_type(self, feature, frame ): 762 """ Remove all event_types of given feature, for current frame or all if frame is None """ 763 if self.event_types.get(feature) is None: 764 return 765 idlist = self.event_types[feature].copy() 766 for sid in idlist: 767 ind = self.index_from_id(sid) 768 if ind is not None: 769 if frame is not None: 770 if int(self.events.data[ind][0]) == frame: 771 self.event_types[feature].remove(sid) 772 self.decrease_score(ind) 773 else: 774 self.event_types[feature].remove(sid) 775 self.decrease_score(ind) 776 self.events.refresh() 777 self.update_nevents_display() 778 779 def remove_event_types(self, sid): 780 """ Remove all event_types of given event id """ 781 for listval in self.event_types.values(): 782 if sid in listval: 783 listval.remove(sid) 784 785 def decrease_score(self, ind): 786 """ Decrease by one score of event at index ind. Delete it if reach 0""" 787 score = self.events.properties["score"] 788 score.flags.writeable = True 789 score[ind] = score[ind] - 1 790 if self.events.properties["score"][ind] == 0: 791 self.exonerate_one( ind, remove_division=False ) 792 self.update_nevents_display() 793 794 def index_from_id(self, sid): 795 """ From event id, find the corresponding index in the properties array """ 796 for ind, cid in enumerate(self.events.properties["id"]): 797 if cid == sid: 798 return ind 799 return None 800 801 def id_from_index( self, ind ): 802 """ From event index, returns it id """ 803 return self.events.properties["id"][ind] 804 805 def find_event(self, frame, label): 806 """ Find if there is already a event at given frame and label """ 807 events = self.events.data 808 events_lab = self.events.properties["label"] 809 for i, lab in enumerate(events_lab): 810 if lab == label: 811 if events[i][0] == frame: 812 return i, self.events.properties["id"][i] 813 return None, None 814 815 def init_suggestion(self): 816 """ Initialize the layer that will contains propostion of tracks/segmentations """ 817 suggestion = np.zeros(self.seglayer.data.shape, dtype="uint16") 818 self.suggestion = self.viewer.add_labels(suggestion, blending="additive", name="Suggestion") 819 820 @self.seglayer.mouse_drag_callbacks.append 821 def click(layer, event): 822 if event.type == "mouse_press": 823 if 'Alt' in event.modifiers: 824 if event.button == 1: 825 pos = event.position 826 # alt+left click accept suggestion under the mouse pointer (in all frames) 827 self.accept_suggestion(pos) 828 829 def accept_suggestion(self, pos): 830 """ Accept the modifications of the label at position pos (all the label) """ 831 seglayer = self.viewer.layers["Segmentation"] 832 label = self.suggestion.data[tuple(map(int, pos))] 833 found = self.suggestion.data==label 834 self.exonerate( found, seglayer ) 835 indices = np.argwhere( found ) 836 ut.setNewLabel( seglayer, indices, label, add_frame=None ) 837 self.suggestion.data[self.suggestion.data==label] = 0 838 self.suggestion.refresh() 839 self.update_nevents_display() 840 841 def remove_one_event( self, event_id ): 842 """ Remove the given event from its id """ 843 if self.events is None: 844 return 845 ind = self.index_from_id(event_id) 846 if ind is not None: 847 self.exonerate_one( ind ) 848 self.update_nevents_display() 849 self.events.refresh() 850 851 def exonerate_one(self, ind, remove_division=True): 852 """ Remove one event at index ind """ 853 self.events.selected_data = [ind] 854 sid = self.events.properties["id"][ind] 855 if (remove_division) and ("division" in self.event_types.keys()) and (sid in self.event_types["division"]): 856 self.epicure.tracking.remove_division( self.events.properties["label"][ind] ) 857 self.events.remove_selected() 858 self.remove_event_types(sid) 859 860 def clear_event(self): 861 """ Remove the current event """ 862 num_event = int(self.event_num.value()) 863 self.exonerate_one( num_event, remove_division=True ) 864 self.update_nevents_display() 865 866 def exonerate_from_event(self, event): 867 """ Remove all events in the corresponding cell of position """ 868 label = ut.getCellValue( self.seglayer, event ) 869 if len(self.events.data) > 0: 870 for ind, lab in enumerate(self.events.properties["label"]): 871 if lab == label: 872 if self.events.data[ind][0] == event.position[0]: 873 self.exonerate_one(ind, remove_division=True) 874 self.update_nevents_display() 875 876 def exonerate(self, indices, seglayer): 877 """ Remove events that have been corrected/cleared """ 878 seglabels = np.unique(seglayer.data[indices]) 879 selected = [] 880 if self.events.properties.get("label") is None: 881 return 882 for ind, lab in enumerate(self.events.properties["label"]): 883 if lab in seglabels: 884 ## label to remove from event list 885 selected.append(ind) 886 if len(selected) > 0: 887 self.events.selected_data = selected 888 self.events.remove_selected() 889 self.update_nevents_display() 890 891 892 #######################################" 893 ## Outliers suggestion functions 894 def show_outlierBlock(self): 895 self.featOutliers.setVisible( self.outlier_vis.isChecked() ) 896 897 def create_outliersBlock(self): 898 ''' Block interface of functions for error suggestions based on cell features ''' 899 feat_layout = QVBoxLayout() 900 901 self.feat_onframe = wid.add_check( check="Only current frame", checked=True, check_func=None, descr="Search for outliers only in current frame" ) 902 feat_layout.addWidget(self.feat_onframe) 903 904 ## area widget 905 tarea_layout, self.min_area, self.max_area = wid.min_button_max( btn="< Area (pix^2) <", btn_func=self.event_area_threshold, min_val="0", max_val="2000", descr="Look for cell which size is outside the given area range" ) 906 feat_layout.addLayout( tarea_layout ) 907 908 ## solid widget 909 feat_solid_line, self.fsolid_out = wid.button_parameter_line( btn="Solidity outliers", btn_func=self.event_solidity, value="3.0", descr_btn="Search for outliers in solidity value", descr_value="Inter-quartiles range factor to consider outlier" ) 910 feat_layout.addLayout( feat_solid_line ) 911 912 ## intensity widget 913 feat_inten_line, self.fintensity_out = wid.button_parameter_line( btn="Intensity cytoplasm/junction", btn_func=self.event_intensity, value="1.0", descr_btn="Search for outliers in intensity ratio", descr_value="Ratio of intensity above which the cell looks suspect" ) 914 feat_layout.addLayout( feat_inten_line ) 915 916 ## tubeness widget 917 feat_tub_line, self.ftub_out = wid.button_parameter_line( btn="Tubeness cytoplasm/junction", btn_func=self.event_tubeness, value="1.0", descr_btn="Search for outliers in tubeness ratio", descr_value="Ratio of tubeness above which the cell looks suspect" ) 918 feat_layout.addLayout( feat_tub_line ) 919 920 ## all features 921 self.featOutliers.setLayout(feat_layout) 922 self.featOutliers.setVisible( self.outlier_vis.isChecked() ) 923 924 def event_feature(self, featname, funcname ): 925 """ event in one frame or all frames the given feature """ 926 onframe = self.feat_onframe.isChecked() 927 if onframe: 928 tframe = ut.current_frame(self.viewer) 929 self.reset_event_type(featname, tframe) 930 funcname(tframe) 931 else: 932 self.reset_event_type(featname, None) 933 for frame in range(self.seglayer.data.shape[0]): 934 funcname(frame) 935 self.update_display() 936 ut.set_active_layer( self.viewer, "Segmentation" ) 937 938 def inspect_outliers(self, tab, props, tuk, frame, feature): 939 q1 = np.quantile(tab, 0.25) 940 q3 = np.quantile(tab, 0.75) 941 qtuk = tuk * (q3-q1) 942 for sign in [1, -1]: 943 #thresh = np.mean(tab) + sign * np.std(tab)*tuk 944 if sign > 0: 945 thresh = q3 + qtuk 946 else: 947 thresh = q1 - qtuk 948 for i in np.where((tab-thresh)*sign>0)[0]: 949 position = ut.prop_to_pos( props[i], frame ) 950 self.add_event( position, props[i].label, feature ) 951 952 def event_area_threshold(self): 953 """ Look for cell's area below/above a threshold """ 954 self.event_feature( "area", self.event_area_threshold_oneframe ) 955 956 def event_area_threshold_oneframe( self, tframe ): 957 """ Check if area is above/below given threshold """ 958 minarea = int(self.min_area.text()) 959 maxarea = int(self.max_area.text()) 960 frame_props = self.epicure.get_frame_features( tframe ) 961 for prop in frame_props: 962 if (prop.area < minarea) or (prop.area > maxarea): 963 position = ut.prop_to_pos( prop, tframe ) 964 self.add_event( position, prop.label, "area" ) 965 966 967 def event_area(self, state): 968 """ Look for outliers in term of cell area """ 969 self.event_feature( "area", self.event_area_oneframe ) 970 971 def event_area_oneframe(self, frame): 972 seglayer = self.seglayer.data[frame] 973 props = regionprops(seglayer) 974 ncell = len(props) 975 areas = np.zeros((ncell,1), dtype="float") 976 for i, prop in enumerate(props): 977 if prop.label > 0: 978 areas[i] = prop.area 979 tuk = self.farea_out.value() 980 self.inspect_outliers(areas, props, tuk, frame, "area") 981 982 def event_solidity(self, state): 983 """ Look for outliers in term ofz cell solidity """ 984 self.event_feature( "solidity", self.event_solidity_oneframe ) 985 986 def event_solidity_oneframe(self, frame): 987 seglayer = self.seglayer.data[frame] 988 props = regionprops(seglayer) 989 ncell = len(props) 990 sols = np.zeros((ncell,1), dtype="float") 991 for i, prop in enumerate(props): 992 if prop.label > 0: 993 sols[i] = prop.solidity 994 tuk = float(self.fsolid_out.text()) 995 self.inspect_outliers(sols, props, tuk, frame, "solidity") 996 997 def event_intensity(self, state): 998 """ Look for abnormal intensity inside/periph ratio """ 999 self.event_feature( "intensity", self.event_intensity_oneframe ) 1000 1001 def event_intensity_oneframe(self, frame): 1002 seglayer = self.seglayer.data[frame] 1003 intlayer = self.viewer.layers["Movie"].data[frame] 1004 props = regionprops(seglayer) 1005 for i, prop in enumerate(props): 1006 if prop.label > 0: 1007 self.test_intensity( intlayer, prop, frame ) 1008 1009 def test_intensity(self, inten, prop, frame): 1010 """ Test if intensity inside is much smaller than at periphery """ 1011 bbox = prop.bbox 1012 intbb = inten[bbox[0]:bbox[2], bbox[1]:bbox[3]] 1013 footprint = disk(radius=self.epicure.thickness) 1014 inside = binary_erosion(prop.image, footprint) 1015 ininten = np.mean(intbb*inside) 1016 dil_img = binary_dilation(prop.image, footprint) 1017 periph = dil_img^inside 1018 periphint = np.mean(intbb*periph) 1019 if (periphint<=0) or (ininten/periphint > float(self.fintensity_out.text())): 1020 position = ( frame, int(prop.centroid[0]), int(prop.centroid[1]) ) 1021 self.add_event( position, prop.label, "intensity" ) 1022 1023 def event_tubeness(self, state): 1024 """ Look for abnormal tubeness inside vs periph """ 1025 self.event_feature( "tubeness", self.event_tubeness_oneframe ) 1026 1027 def event_tubeness_oneframe(self, frame): 1028 seglayer = self.seglayer.data[frame] 1029 mov = self.viewer.layers["Movie"].data[frame] 1030 sated = np.copy(mov) 1031 sated = filters.sato(sated, black_ridges=False) 1032 props = regionprops(seglayer) 1033 for i, prop in enumerate(props): 1034 if prop.label > 0: 1035 self.test_tubeness( sated, prop, frame ) 1036 1037 def test_tubeness(self, sated, prop, frame): 1038 """ Test if tubeness inside is much smaller than tubeness on periph """ 1039 bbox = prop.bbox 1040 satbb = sated[bbox[0]:bbox[2], bbox[1]:bbox[3]] 1041 footprint = disk(radius=self.epicure.thickness) 1042 inside = binary_erosion(prop.image, footprint) 1043 intub = np.mean(satbb*inside) 1044 periph = prop.image^inside 1045 periphtub = np.mean(satbb*periph) 1046 if periphtub <= 0: 1047 position = ( frame, int(prop.centroid[0]), int(prop.centroid[1]) ) 1048 self.add_event( position, prop.label, "tubeness" ) 1049 else: 1050 if intub/periphtub > float(self.ftub_out.text()): 1051 position = ( frame, int(prop.centroid[0]), int(prop.centroid[1]) ) 1052 self.add_event( position, prop.label, "tubeness" ) 1053 1054 1055############# event based on track 1056 1057 def show_tracksBlock(self): 1058 self.eventTrack.setVisible( self.track_vis.isChecked() ) 1059 1060 def create_tracksBlock(self): 1061 ''' Block interface of functions for error suggestions based on tracks ''' 1062 track_layout = QVBoxLayout() 1063 1064 hign = wid.hlayout() 1065 ignore_label = wid.label_line( "Ignore cells on:") 1066 hign.addWidget( ignore_label ) 1067 vign = wid.vlayout() 1068 self.ignore_borders = wid.add_check( "border (of image)", False, None, "When adding suspect, don't add it if the cell is touching the border of the image" ) 1069 vign.addWidget(self.ignore_borders) 1070 1071 self.ignore_boundaries = wid.add_check( "tissue boundaries", False, None, "When adding suspect, don't add it if the cell is on the tissu boundaries (no neighbor in one side)" ) 1072 vign.addWidget(self.ignore_boundaries) 1073 hign.addLayout( vign ) 1074 track_layout.addLayout( hign ) 1075 1076 ## Look for merging tracks 1077 self.get_merge = wid.add_check( "Flag track merging", True, None, "Add a suspect if two track merge in one" ) 1078 track_layout.addWidget(self.get_merge) 1079 1080 ## Look for sudden appearance of tracks 1081 self.get_apparition = wid.add_check( "Flag track apparition", True, None, "Add a suspect if a track appears in the middle of the movie (not on border)" ) 1082 track_layout.addWidget(self.get_apparition) 1083 1084 self.get_division = wid.add_check( "Get divisions", False, None, "Add a division if two touching track appears while a potential parent track disappear" ) 1085 track_layout.addWidget(self.get_division) 1086 1087 ## Look for sudden disappearance of tracks 1088 dsp_layout = wid.hlayout() 1089 self.get_disparition = wid.add_check( check="Flag track disparition", checked=True, check_func=None, descr="Add a suspect if a track disappears (not last frame, not border)" ) 1090 disp_line, self.threshold_disparition = wid.value_line( label="cell area threshold", default_value="200", descr="Flag cell if cell area is above threshold" ) 1091 self.get_extrusions = wid.add_check( "Get extrusions", True, None, "Add extrusions events when a track is disappearing normally (below cell area threshold)" ) 1092 vlay = wid.vlayout() 1093 vlay.addWidget( self.get_disparition ) 1094 vlay.addWidget( self.get_extrusions ) 1095 dsp_layout.addLayout( vlay ) 1096 dsp_layout.addLayout( disp_line ) 1097 track_layout.addLayout( dsp_layout ) 1098 1099 ## Look for temporal gaps in tracks 1100 gap_line, self.get_gaps, self.min_gaps = wid.check_value( check="Flag track gaps", checkfunc=None, checked=True, value="1", descr="Add a suspect if a track has gaps longer than threshold (in nb of frames)", label="if gap above" ) 1101 track_layout.addLayout( gap_line ) 1102 1103 ## track length event_types 1104 ilengthlay, self.check_length, self.min_length = wid.check_value( check="Flag tracks smaller than", checkfunc=None, checked=True, value="1", descr="Add a suspect event for each track smaller than chosen value (in number of frames)" ) 1105 track_layout.addLayout(ilengthlay) 1106 1107 ## track sudden jump in position 1108 ijumplay, self.check_jump, self.jump_factor = wid.check_value( check="Flag jump in track position", checkfunc=None, checked=True, value="3.0", descr="Add a suspect event for when the position of cell centroid moves suddenly a lot compared to the rest of the track" ) 1109 track_layout.addLayout(ijumplay) 1110 1111 ## Variability in feature event_type 1112 sizevar_line, self.check_size, self.size_variability = wid.check_value( check="Size variation", checkfunc=None, checked=False, value="3", descr="Add a suspect if the size of the cell varies suddenly in the track" ) 1113 track_layout.addLayout( sizevar_line ) 1114 shapevar_line, self.check_shape, self.shape_variability = wid.check_value( check="Shape variation", checkfunc=None, checked=False, value="3.0", descr="Add a suspect if the shape of the cell varies suddenly in the track" ) 1115 track_layout.addLayout( shapevar_line ) 1116 1117 ## merge/split combinaisons 1118 track_btn = wid.add_button( btn="Inspect track", btn_func=self.inspect_tracks, descr="Start track analysis to look for suspects based on selected features" ) 1119 track_layout.addWidget(track_btn) 1120 1121 ## all features 1122 self.eventTrack.setLayout(track_layout) 1123 self.eventTrack.setVisible( self.track_vis.isChecked() ) 1124 1125 def reset_tracking_event(self): 1126 """ Remove events from tracking """ 1127 self.reset_event_type("track-1-2-*", None) 1128 self.reset_event_type("track-2->1", None) 1129 self.reset_event_type("track-length", None) 1130 self.reset_event_type("track-jump", None) 1131 self.reset_event_type("track-size", None) 1132 self.reset_event_type("track-shape", None) 1133 self.reset_event_type("track-apparition", None) 1134 self.reset_event_type("track-disparition", None) 1135 self.reset_event_type("track-gap", None) 1136 if self.get_extrusions.isChecked(): 1137 self.reset_event_type("extrusion", None) 1138 self.reset_event_range() 1139 1140 def track_length(self): 1141 """ Find all cells that are only in one frame """ 1142 max_len = int(self.min_length.text()) 1143 labels, lengths, positions = self.epicure.tracking.get_small_tracks( max_len ) 1144 ## remove track from first and last frame 1145 first_tracks = self.epicure.tracking.get_tracks_on_frame( 0 ) 1146 last_tracks = self.epicure.tracking.get_tracks_on_frame( self.epicure.nframes-1 ) 1147 for label, nframe, pos in zip(labels, lengths, positions): 1148 if label in first_tracks or label in last_tracks: 1149 ## present in the first or last track, don't check it 1150 continue 1151 if self.epicure.verbose > 2: 1152 print("event track length "+str(nframe)+": "+str(label)+" frame "+str(pos[0]) ) 1153 self.add_event(pos, label, "track-length", refresh=False) 1154 self.refresh_events() 1155 1156 def inspect_tracks( self, subprogress=True ): 1157 """ Look for suspicious tracks """ 1158 ut.set_visibility( self.viewer, "Events", True ) 1159 progress_bar = ut.start_progress( self.viewer, total=10 ) 1160 if subprogress: 1161 ## show subprogress bars in sub functions (doesn't work on notebook without interface) 1162 pb = progress_bar 1163 else: 1164 pb= None 1165 progress_bar.update(0) 1166 self.reset_tracking_event() 1167 progress_bar.update(1) 1168 if self.ignore_borders.isChecked() or self.ignore_boundaries.isChecked(): 1169 progress_bar.set_description("Identifying border and/or boundaries cells") 1170 self.get_outside_cells() 1171 progress_bar.update(2) 1172 tracks = self.epicure.tracking.get_track_list() 1173 if self.check_length.isChecked(): 1174 progress_bar.set_description("Identifying too small tracks") 1175 self.track_length() 1176 progress_bar.update(3) 1177 if self.get_merge.isChecked(): 1178 progress_bar.set_description("Inspect tracks 2->1") 1179 self.track_21() 1180 progress_bar.update(4) 1181 if (self.check_size.isChecked()) or self.check_shape.isChecked(): 1182 progress_bar.set_description("Inspect track features") 1183 self.track_features() 1184 progress_bar.update(5) 1185 if self.get_apparition.isChecked() or self.get_division.isChecked(): 1186 progress_bar.set_description("Check new track apparition and/or division") 1187 self.track_apparition( tracks ) 1188 progress_bar.update(6) 1189 if self.get_disparition.isChecked() or self.get_extrusions.isChecked(): 1190 progress_bar.set_description("Check track disparition and/or extrusion") 1191 self.track_disparition( tracks, pb ) 1192 progress_bar.update(7) 1193 if self.get_gaps.isChecked(): 1194 progress_bar.set_description("Check temporal gaps in tracks") 1195 self.track_gaps( tracks, pb ) 1196 progress_bar.update(8) 1197 if self.check_jump.isChecked(): 1198 progress_bar.set_description("Check position jump in tracks") 1199 self.track_position_jump( tracks, pb ) 1200 progress_bar.update(9) 1201 ut.close_progress( self.viewer, progress_bar ) 1202 ut.set_active_layer( self.viewer, "Segmentation" ) 1203 1204 def track_apparition( self, tracks ): 1205 """ Check if some track appears suddenly (in the middle of the movie and not by division) """ 1206 start_time = time.time() 1207 ## remove track on first frame 1208 ctracks = list( set(tracks) - set( self.epicure.tracking.get_tracks_on_frame( 0 ) ) ) 1209 graph = self.epicure.tracking.graph 1210 do_divisions = self.get_division.isChecked() 1211 do_apparition = self.get_apparition.isChecked() 1212 apparitions = {} 1213 for i, track_id in enumerate( ctracks) : 1214 fframe = self.epicure.tracking.get_first_frame( track_id ) 1215 ## If on the border, ignore 1216 #outside = self.epicure.cell_on_border( track_id, fframe ) 1217 #if outside: 1218 # continue 1219 ## Not on border, check if potential division 1220 if (graph is not None) and (track_id in graph.keys()): 1221 continue 1222 ## event apparition 1223 if (not do_divisions) and do_apparition: 1224 self.add_apparition( fframe, track_id ) 1225 else: 1226 if fframe not in apparitions: 1227 apparitions[fframe] = [track_id] 1228 else: 1229 apparitions[fframe].append(track_id) 1230 if do_divisions: 1231 self.apparition_or_division( apparitions, do_apparition ) 1232 self.refresh_events() 1233 if self.epicure.verbose > 1: 1234 ut.show_duration( start_time, "Tracks apparition took " ) 1235 1236 def add_apparition( self, frame, trackid ): 1237 """ Add a suspect apparition to events """ 1238 posxy = self.epicure.tracking.get_position( trackid, frame ) 1239 if posxy is not None: 1240 pos = [ frame, posxy[0], posxy[1] ] 1241 if self.epicure.verbose > 2: 1242 print("Appearing track: "+str(trackid)+" at frame "+str(frame) ) 1243 self.add_event(pos, trackid, "track-apparition", refresh=False) 1244 1245 def apparition_or_division( self, apevents, do_apparition ): 1246 """ Check if detected events are apparitions or divisions """ 1247 for frame, tracks in apevents.items(): 1248 if len(tracks) == 1: 1249 ## only one event, apparition 1250 if do_apparition: 1251 self.add_apparition( frame, tracks[0] ) 1252 else: 1253 # look for potential neighbors for each apparition at this frame 1254 ind = 0 1255 while ind < len(tracks): 1256 ctrack = tracks[ind] 1257 ## already treated 1258 if ctrack < 0: 1259 ind = ind + 1 1260 continue 1261 dind = ind + 1 1262 found = False 1263 while dind < len(tracks): 1264 ## skip if already done 1265 if tracks[dind] < 0: 1266 dind = dind + 1 1267 continue 1268 ## check if labels are touching at the appearing frame 1269 bbox, merged = ut.getBBox2DMerge( self.epicure.seg[frame], ctrack, tracks[dind] ) 1270 bbox = ut.extendBBox2D( bbox, 1.05, self.epicure.imgshape2D ) 1271 segt_crop = ut.cropBBox2D( self.epicure.seg[frame], bbox ) 1272 touched = ut.checkTouchingLabels( segt_crop, ctrack, tracks[dind] ) 1273 if touched: 1274 ## found neighbor, potential division 1275 found = True 1276 if self.epicure.editing.add_division( ctrack, tracks[dind], frame ): 1277 ## division added successfully 1278 tracks[dind] = -1 ## track done 1279 break 1280 else: 1281 ## failed to add a division, so mark it as apparition 1282 if do_apparition: 1283 self.add_apparition( frame, ctrack ) 1284 break 1285 else: 1286 dind = dind + 1 1287 # no neighbor found, so mark this as an apparition 1288 if (not found) and do_apparition: 1289 if ctrack > 0: 1290 self.add_apparition( frame, ctrack ) 1291 ind = ind + 1 1292 #print(self.epicure.tracking.graph) 1293 1294 1295 1296 def track_disparition( self, tracks, progress_bar ): 1297 """ Check if some track disappears suddenly (in the middle of the movie and not by division) """ 1298 start_time = ut.start_time() 1299 ## Track disappears in the movie, not last frame 1300 ctracks = list( set(tracks) - set( self.epicure.tracking.get_tracks_on_frame( self.epicure.nframes-1 ) ) ) 1301 threshold_area = float(self.threshold_disparition.text()) 1302 if progress_bar is not None: 1303 sub_bar = progress( total = len( ctracks ), desc="Check non last frame tracks", nest_under = progress_bar ) 1304 for i, track_id in enumerate( ctracks ): 1305 if progress_bar is not None: 1306 sub_bar.update( i ) 1307 lframe = self.epicure.tracking.get_last_frame( track_id ) 1308 ## If on the border, ignore 1309 #outside = self.epicure.cell_on_border( track_id, lframe ) 1310 #if outside: 1311 # continue 1312 ## Not on border, check if potential division 1313 if self.epicure.tracking.is_single_parent( track_id ): 1314 continue 1315 1316 ## check if the cell area is below the threshold, then considered as ok (likely extrusion) 1317 if (threshold_area > 0): 1318 cell_area = self.epicure.cell_area( track_id, lframe ) 1319 if cell_area < threshold_area: 1320 if self.get_extrusions.isChecked(): 1321 fframe = self.epicure.tracking.get_first_frame( track_id ) 1322 if fframe == lframe: 1323 ## track is only one frame, don't flag as extrusion 1324 continue 1325 ## event extrusion 1326 posxy = self.epicure.tracking.get_position( track_id, lframe ) 1327 if posxy is not None: 1328 pos = [ lframe, posxy[0], posxy[1] ] 1329 if self.epicure.verbose > 2: 1330 print("Add extrusion: "+str(track_id)+" at frame "+str(lframe) ) 1331 self.add_event( pos, track_id, "extrusion", symb="diamond", color="red", refresh=False ) 1332 continue 1333 if not self.get_disparition.isChecked(): 1334 continue 1335 1336 ## event disparition 1337 posxy = self.epicure.tracking.get_position( track_id, lframe ) 1338 if posxy is not None: 1339 pos = [ lframe, posxy[0], posxy[1] ] 1340 if self.epicure.verbose > 2: 1341 print("Disappearing track: "+str(track_id)+" at frame "+str(lframe) ) 1342 self.add_event(pos, track_id, "track-disparition", refresh=False) 1343 if progress_bar is not None: 1344 sub_bar.close() 1345 self.refresh_events() 1346 if self.epicure.verbose > 1: 1347 ut.show_duration( start_time, "Tracks disparition took " ) 1348 1349 1350 def track_gaps( self, tracks, progress_bar ): 1351 """ Check if some track have temporal gaps above a given threshold of frames """ 1352 start_time = time.time() 1353 ## Track disappears in the movie, not last frame 1354 ctracks = tracks 1355 min_gaps = int(self.min_gaps.text()) 1356 if progress_bar is not None: 1357 sub_bar = progress( total = len( ctracks ), desc="Check gaps in tracks", nest_under = progress_bar ) 1358 gaped = self.epicure.tracking.check_gap( ctracks, verbose=0 ) 1359 if len( gaped ) > 0: 1360 for i, track_id in enumerate( gaped ): 1361 if progress_bar is not None: 1362 sub_bar.update( i ) 1363 gap_frames = self.epicure.tracking.gap_frames( track_id ) 1364 if len( gap_frames ) > 0: 1365 gaps = ut.get_consecutives( gap_frames ) 1366 if self.epicure.verbose > 1: 1367 print("Found gaps in track "+str(track_id)+" : "+str(gaps) ) 1368 for gapy in gaps: 1369 if (gapy[1]-gapy[0]+1) >= min_gaps: 1370 ## flag gap as it's long enough 1371 poszxy = self.epicure.tracking.get_middle_position( track_id, gapy[0]-1, gapy[1]+1 ) 1372 if poszxy is not None: 1373 if self.epicure.verbose > 2: 1374 print("Gap in track: "+str(track_id)+" at frame "+str(poszxy[0]) ) 1375 self.add_event(poszxy, track_id, "track-gap", refresh=False) 1376 if progress_bar is not None: 1377 sub_bar.close() 1378 self.refresh_events() 1379 if self.epicure.verbose > 1: 1380 ut.show_duration( start_time, "Tracks gaps took " ) 1381 1382 def track_21(self): 1383 """ Look for event track: 2->1 """ 1384 if self.epicure.tracking.tracklayer is None: 1385 ut.show_error("No tracking done yet!") 1386 return 1387 1388 graph = self.epicure.tracking.graph 1389 if graph is not None: 1390 for child, parent in graph.items(): 1391 ## 2->1, merge, event 1392 if isinstance(parent, list) and len(parent) == 2: 1393 onetwoone = False 1394 ## was it only one before ? 1395 if (parent[0] in graph.keys()) and (parent[1] in graph.keys()): 1396 if graph[parent[0]][0] == graph[parent[1]][0]: 1397 pos = self.epicure.tracking.get_mean_position([parent[0], parent[1]]) 1398 if pos is not None: 1399 if self.epicure.verbose > 1: 1400 print("event 1->2->1 track: "+str(graph[parent[0]][0])+"-"+str(parent)+"-"+str(child)+" frame "+str(pos[0]) ) 1401 self.add_event(pos, parent[0], "track-1-2-*", refresh=False) 1402 onetwoone = True 1403 1404 if not onetwoone: 1405 pos = self.epicure.tracking.get_mean_position(child, only_first=True) 1406 if pos is not None: 1407 if self.epicure.verbose > 2: 1408 print("event 2->1 track: "+str(parent)+"-"+str(child)+" frame "+str(int(pos[0])) ) 1409 self.add_event(pos, parent[0], "track-2->1", refresh=False) 1410 else: 1411 if self.epicure.verbose > 1: 1412 print("Something weird, "+str(child)+" mean position") 1413 1414 self.refresh_events() 1415 1416 def get_outside_cells( self ): 1417 """ Get list of cells on tissu boundaries and/or on border of the movie """ 1418 self.boundary_cells = dict() 1419 self.border_cells = dict() 1420 check_border = self.ignore_borders.isChecked() 1421 check_bound = self.ignore_boundaries.isChecked() 1422 def get_cells( img ): 1423 """ For parallel processing, task of one thread (one frame) """ 1424 bounds, borders = None, None 1425 if check_bound: 1426 bounds = ut.get_boundary_cells( img ) 1427 if check_border: 1428 borders = ut.get_border_cells( img ) 1429 return (bounds, borders) 1430 1431 if self.epicure.process_parallel: 1432 # Process in parallel, putting all in temp list and then filling the local dict 1433 cell_list = Parallel(n_jobs=self.epicure.nparallel)( 1434 delayed(get_cells)(frame) for frame in self.epicure.seg 1435 ) 1436 for tframe in range(self.epicure.nframes): 1437 if check_bound: 1438 self.boundary_cells[tframe] = cell_list[tframe][0] 1439 if check_border: 1440 self.border_cells[tframe] = cell_list[tframe][1] 1441 else: 1442 ## simple sequential processing 1443 for tframe in range(self.epicure.nframes): 1444 img = self.epicure.seg[tframe] 1445 if check_bound: 1446 self.boundary_cells[tframe] = ut.get_boundary_cells( img ) 1447 if check_border: 1448 self.border_cells[tframe] = ut.get_border_cells( img ) 1449 1450 def get_boundaries_cells(self, pbar=None): 1451 """ Return list of cells that are at the tissu boundaries (touching background) """ 1452 self.boundary_cells = dict() 1453 for tframe in range(self.epicure.nframes): 1454 if pbar is not None: 1455 pbar.update( tframe) 1456 self.boundary_cells[tframe] = ut.get_boundary_cells( self.epicure.seg[tframe] ) 1457 1458 def get_border_cells(self, pbar=None): 1459 """ Return list of cells that are at the border of the movie """ 1460 self.border_cells = dict() 1461 for tframe in range(self.epicure.nframes): 1462 if pbar is not None: 1463 pbar.update( tframe) 1464 img = self.epicure.seg[tframe] 1465 self.border_cells[tframe] = ut.get_border_cells(img) 1466 1467 def get_divisions( self ): 1468 """ Get and add divisions from the tracking graph """ 1469 self.reset_event_type( "division", frame=None ) 1470 graph = self.epicure.tracking.graph 1471 divisions = {} 1472 ## Go through the graph and fill all division by parents 1473 if graph is not None: 1474 for child, parent in graph.items(): 1475 ## 1 parent, potential division 1476 if (isinstance(parent, int)) or (len(parent) == 1): 1477 if isinstance( parent, list ): 1478 par = parent[0] 1479 else: 1480 par = parent 1481 if par not in divisions: 1482 divisions[par] = [child] 1483 else: 1484 divisions[par].append(child) 1485 1486 ## Add all the divisions in the event list 1487 for parent, childs in divisions.items(): 1488 indexes = self.epicure.tracking.get_track_indexes(childs) 1489 if len(indexes) <= 0: 1490 ## something wrong in the graph or in the tracks, ignore for now 1491 continue 1492 ## get the average first position of the childs just after division 1493 pos = self.epicure.tracking.mean_position(indexes, only_first=True) 1494 self.add_event(pos, parent, "division", symb="o", color="#0055ffff", force=True, refresh=False) 1495 ## Update display to show/hide the divisions 1496 self.show_hide_divisions() 1497 self.refresh_events() 1498 1499 def show_hide_events( self, i=None, eclass=None ): 1500 """ Update which type of events to show or hide """ 1501 if i is None: 1502 ## update all events display 1503 tmp_size = int(self.event_size.value()) 1504 self.events.size = tmp_size 1505 hide_events = [] 1506 for i, eclass in enumerate( self.event_class ): 1507 if not self.show_class[i].isChecked(): 1508 hide_events.append( eclass ) 1509 self.show_subset_event( hide_events, True ) 1510 else: 1511 ## update only the triggered one 1512 self.show_subset_event( eclass, self.show_class[i].isChecked() ) 1513 1514 def show_hide_divisions( self ): 1515 """ Show or hide division events """ 1516 self.show_subset_event( "division", self.show_class[0].isChecked() ) 1517 1518 def show_hide_suspects( self ): 1519 """ Show or hide suspect events """ 1520 self.show_subset_event( "suspect", self.show_class[2].isChecked() ) 1521 1522 def add_extrusion( self, label, frame ): 1523 """ Mark given label at specified frame as an extrusion """ 1524 pos = self.epicure.tracking.get_full_position( label, frame ) 1525 self.events.selected_data = {} 1526 if self.show_class[1].isChecked(): 1527 self.events.current_size = int(self.event_size.value()) 1528 else: 1529 self.events.current_size = 0.1 1530 self.add_event( pos, label, "extrusion", symb="diamond", color="red", force=True ) 1531 self.events.selected_data = {} 1532 self.events.current_size = int(self.event_size.value()) 1533 self.update_nevents_display() 1534 1535 def add_division_event( self, labela, labelb, parent, frame ): 1536 """ Add a division event given the two daughter labels, the parent one and frame of division """ 1537 indexes = self.epicure.tracking.get_index( [labela, labelb], frame ) 1538 indexes = indexes.flatten() 1539 pos = self.epicure.tracking.mean_position( indexes ) 1540 self.events.selected_data = {} 1541 if self.show_class[0].isChecked(): 1542 self.events.current_size = int(self.event_size.value()) 1543 else: 1544 self.events.current_size = 0.1 1545 self.add_event( pos, parent, "division", symb="o", color="#0055ffff", force=True ) 1546 self.events.selected_data = {} 1547 self.events.current_size = int(self.event_size.value()) 1548 ## check if there are suspect events to remove, cleared by the division 1549 if parent is not None: 1550 ## check eventual parent event 1551 num, sid = self.find_event( pos[0]-1, parent ) 1552 if num is not None: 1553 if self.is_end_event( sid ): 1554 ## the parent event correspond to a potential end of track, remove it 1555 ind = self.index_from_id( sid ) 1556 self.exonerate_one( ind, remove_division=False ) 1557 if self.epicure.verbose > 0: 1558 print( "Removed suspect event of parent cell "+str(parent)+" cleared by the division flag" ) 1559 ## check each child suspect if cleared by the new division 1560 for child in [labela, labelb]: 1561 num, sid = self.find_event( pos[0], child ) 1562 if num is not None: 1563 if self.is_begin_event( sid ): 1564 ## the child event correspond to a potential begin of track, remove it 1565 ind = self.index_from_id( sid ) 1566 self.exonerate_one( ind, remove_division=False ) 1567 if self.epicure.verbose > 0: 1568 print( "Removed suspect event of daughter cell "+str(child)+" cleared by the division flag" ) 1569 self.update_nevents_display() 1570 1571 def get_event_class( self, ind ): 1572 """ Return the class of event of index ind """ 1573 if self.is_division( ind ): 1574 return 0 1575 if self.is_extrusion( ind ): 1576 return 1 1577 return 2 1578 1579 def is_extrusion( self, ind ): 1580 """ Return if the event of current index is a division """ 1581 return ("extrusion" in self.event_types) and (self.id_from_index(ind) in self.event_types["extrusion"]) 1582 1583 1584 def is_division( self, ind ): 1585 """ Return if the event of current index is a division """ 1586 return ("division" in self.event_types) and (self.id_from_index(ind) in self.event_types["division"]) 1587 1588 def is_suspect( self, ind ): 1589 """ Return if the event of current index is a suspect event """ 1590 return not self.is_division( ind ) 1591 1592 def is_begin_event( self, sid ): 1593 """ Return True if the event has a type corresponding to begin of a track (too small or appearing) """ 1594 beg_events = ["track-apparition", "track-length"] 1595 for event in beg_events: 1596 if event in self.event_types: 1597 if sid in self.event_types[event]: 1598 return True 1599 return False 1600 1601 def is_end_event( self, sid ): 1602 """ Return True if the event has a type corresponding to end of a track (too small or disappearing) """ 1603 end_events = ["track-disparition", "track-length"] 1604 for event in end_events: 1605 if event in self.event_types: 1606 if sid in self.event_types[event]: 1607 return True 1608 return False 1609 1610 def track_position_jump( self, track_ids, progress_bar ): 1611 """ Look at jump in the track position """ 1612 factor = float( self.jump_factor.text() ) 1613 if progress_bar is not None: 1614 sub_bar = progress( total = len( track_ids ), desc="Check position jump in tracks", nest_under = progress_bar ) 1615 for i, tid in enumerate(track_ids): 1616 if progress_bar is not None: 1617 sub_bar.update(i) 1618 track_indexes = self.epicure.tracking.get_track_indexes( tid ) 1619 ## track should be long enough to make sense to look for outlier 1620 if len(track_indexes) > 3: 1621 track_velo = self.epicure.tracking.measure_speed( tid ) 1622 jumps = self.find_jump( track_velo, factor=factor, min_value=5 ) 1623 for tind in jumps: 1624 tdata = self.epicure.tracking.get_frame_data( tid, tind ) 1625 if self.epicure.verbose > 1: 1626 print("event track jump: "+str(tdata[0])+" "+" frame "+str(tdata[1]) ) 1627 self.add_event( tdata[1:4], tid, "track-jump", refresh=False ) 1628 if progress_bar is not None: 1629 sub_bar.close() 1630 self.refresh_events() 1631 1632 1633 def track_features(self): 1634 """ Look at outliers in track features """ 1635 track_ids = self.epicure.tracking.get_track_list() 1636 features = [] 1637 featType = {} 1638 if self.check_size.isChecked(): 1639 features = features + ["Area", "Perimeter"] 1640 featType["Area"] = "size" 1641 featType["Perimeter"] = "size" 1642 size_factor = float(self.size_variability.text()) 1643 if self.check_shape.isChecked(): 1644 features = features + ["Eccentricity", "Solidity"] 1645 featType["Eccentricity"] = "shape" 1646 featType["Solidity"] = "shape" 1647 shape_factor = float(self.shape_variability.text()) 1648 for tid in track_ids: 1649 track_indexes = self.epicure.tracking.get_track_indexes( tid ) 1650 ## track should be long enough to make sense to look for outlier 1651 if len(track_indexes) > 3: 1652 track_feats = self.epicure.tracking.measure_features( tid, features ) 1653 for feature, values in track_feats.items(): 1654 if featType[feature] == "size": 1655 factor = size_factor 1656 if featType[feature] == "shape": 1657 factor = shape_factor 1658 outliers = self.find_jump( values, factor=factor ) 1659 for out in outliers: 1660 tdata = self.epicure.tracking.get_frame_data( tid, out ) 1661 if self.epicure.verbose > 1: 1662 print("event track "+feature+": "+str(tdata[0])+" "+" frame "+str(tdata[1]) ) 1663 self.add_event(tdata[1:4], tid, "track_"+featType[feature], refresh=False) 1664 self.refresh_events() 1665 1666 def find_jump( self, tab, factor=1, min_value=None ): 1667 """ Detect brutal jump in the values """ 1668 jumps = [] 1669 tab = np.array(tab) 1670 diff = np.diff( tab, n=2, prepend=tab[0], append=tab[-1] ) 1671 ## get local average 1672 if len(tab) <= 10: 1673 avg = np.mean( tab ) 1674 else: 1675 kernel = np.repeat (1.0/10.0, 10 ) 1676 avg = np.convolve( tab, kernel, mode="same") 1677 ## normalize the difference by the average value 1678 eps = 0.0000001 1679 diff = np.array(diff, dtype=np.float32) 1680 avg = np.array(avg, dtype=np.float32) 1681 diff = abs(diff+eps)/(avg+eps) 1682 ## keep only local max above threshold 1683 local_max = (np.diff( np.sign(np.diff(diff)) )<0).nonzero()[0] + 1 1684 if min_value is None: 1685 jumps = [i for i in local_max if diff[i] > factor] 1686 else: 1687 jumps = [ i for i in local_max if (diff[i] > factor) and (tab[i] > min_value) ] 1688 return jumps 1689 1690 def find_outliers_tuk( self, tab, factor=3, below=True, above=True ): 1691 """ Returns index of outliers from Tukey's like test """ 1692 q1 = np.quantile(tab, 0.2) 1693 q3 = np.quantile(tab, 0.8) 1694 qtuk = factor * (q3-q1) 1695 outliers = [] 1696 if below: 1697 outliers = outliers + (np.where((tab-q1+qtuk)<0)[0]).tolist() 1698 if above: 1699 outliers = outliers + (np.where((tab-q3-qtuk)>0)[0]).tolist() 1700 return outliers 1701 1702 def weirdo_area(self): 1703 """ look at area trajectory for outliers """ 1704 track_df = self.epicure.tracking.track_df 1705 for tid in np.unique(track_df["track_id"]): 1706 rows = track_df[track_df["track_id"]==tid].copy() 1707 if len(rows) >= 3: 1708 rows["smooth"] = rows.area.rolling(self.win_size, min_periods=1).mean() 1709 rows["diff"] = (rows["area"] - rows["smooth"]).abs() 1710 rows["diff"] = rows["diff"].div(rows["smooth"]) 1711 if self.epicure.verbose > 2: 1712 print(rows)
26class Inspecting(QWidget): 27 28 def __init__(self, napari_viewer, epic): 29 """ 30 Generate the graphical interface for the inspection panel, and initialize the events layer. 31 """ 32 super().__init__() 33 self.viewer = napari_viewer 34 self.epicure = epic 35 self.seglayer = self.viewer.layers["Segmentation"] 36 self.border_cells = None ## list of cells that are on the image border 37 self.boundary_cells = None ## list of cells that are on the boundary (touch the background) 38 self.eventlayer_name = "Events" 39 self.events = None 40 self.win_size = 10 41 self.event_class = self.epicure.event_class 42 43 ## Print the current number of events 44 self.nevents_print = QLabel("") 45 self.update_nevents_display() 46 47 self.create_eventlayer() 48 layout = QVBoxLayout() 49 layout.addWidget( self.nevents_print ) 50 51 ## Reset or update some events 52 update_events_choice = wid.add_button( btn="Reset/Update some events...", btn_func=self.reset_events_choice, descr="Pops up an interface to choose which event(s) to remove or update" ) 53 layout.addWidget( update_events_choice ) 54 layout.addWidget( wid.separation() ) 55 56 57 ## choose events to display 58 show_label = wid.label_line( "Show events:" ) 59 layout.addWidget( show_label ) 60 show_line = wid.hlayout() 61 self.show_class = [] 62 for i, eclass in enumerate(self.event_class) : 63 check = wid.add_check_tolayout( show_line, eclass, True, None, "Show/hide the "+eclass ) 64 check.stateChanged.connect( lambda state, i=i, eclass=eclass: self.show_hide_events(i, eclass) ) 65 self.show_class.append( check ) 66 layout.addLayout( show_line ) 67 68 ## Visualisation options 69 disp_line, self.event_disp, self.displayevent = wid.checkgroup_help( "Display options", False, "Show/hide event display options panel", "event#visualisation", self.epicure.display_colors, "group3" ) 70 self.create_displayeventBlock() 71 layout.addLayout( disp_line ) 72 layout.addWidget(self.displayevent) 73 74 layout.addWidget( wid.separation() ) 75 ## Error suggestions based on cell features 76 outlier_line, self.outlier_vis, self.featOutliers = wid.checkgroup_help( "Outlier options", False, "Show/Hide outlier options panel", "event#frame-based-events", self.epicure.display_colors, "group" ) 77 layout.addLayout( outlier_line ) 78 self.create_outliersBlock() 79 layout.addWidget(self.featOutliers) 80 81 ## Error suggestions based on tracks 82 track_line, self.track_vis, self.eventTrack = wid.checkgroup_help( "Track options", True, "Show/hide track options", "event#track-based-events", self.epicure.display_colors, "group2" ) 83 self.create_tracksBlock() 84 layout.addLayout( track_line ) 85 layout.addWidget(self.eventTrack) 86 87 self.setLayout(layout) 88 self.key_binding() 89 90 def key_binding(self): 91 """ active key bindings (keyboard and mouse shortcuts) for events options """ 92 sevents = self.epicure.shortcuts["Events"] 93 self.epicure.overtext["events"] = "---- Events editing ---- \n" 94 self.epicure.overtext["events"] += ut.print_shortcuts( sevents ) 95 96 @self.epicure.seglayer.mouse_drag_callbacks.append 97 def handle_event(seglayer, event): 98 if event.type == "mouse_press": 99 ## remove a event 100 if ut.shortcut_click_match( sevents["delete"], event ): 101 ind = ut.getCellValue( self.events, event ) 102 if self.epicure.verbose > 1: 103 print("Removing clicked event, at index "+str(ind)) 104 if ind is None: 105 ## click was not on a event 106 return 107 sid = self.events.properties["id"][ind] 108 if sid is not None: 109 self.exonerate_one(ind, remove_division=True) 110 self.update_nevents_display() 111 else: 112 if self.epicure.verbose > 1: 113 print("event with id "+str(sid)+" not found") 114 self.events.refresh() 115 return 116 117 ## zoom on a event 118 if ut.shortcut_click_match( sevents["zoom"], event ): 119 ind = ut.getCellValue( self.events, event ) 120 if "id" not in self.events.properties.keys(): 121 print("No event under click") 122 return 123 sid = self.events.properties["id"][ind] 124 if self.epicure.verbose > 1: 125 print("Zoom on event with id "+str(sid)+"") 126 self.zoom_on_event( event.position, sid ) 127 return 128 129 @self.epicure.seglayer.bind_key( sevents["next"]["key"], overwrite=True ) 130 def go_next(seglayer): 131 """ Select next suspect event and zoom on it """ 132 num_event = int(self.event_num.value()) 133 nevents = self.nb_events() 134 if num_event < 0: 135 if self.nb_events( only_suspect=True ) == 0: 136 if self.epicure.verbose > 0: 137 print("No more suspect event") 138 return 139 else: 140 self.event_num.setValue(0) 141 else: 142 self.event_num.setValue( (num_event+1)%nevents ) 143 self.skip_nonselected_event( nevents, min(nevents,3000) ) 144 self.go_to_event() 145 146 def skip_nonselected_event( self, nevents, left ): 147 """ Skip next event if not a selected one (show event is not checked) """ 148 if left < 0: 149 return 0 150 151 index = int(self.event_num.value()) 152 nothing_showed = True 153 for i, curclass in enumerate(self.show_class): 154 if curclass.isChecked(): 155 nothing_showed = False 156 break 157 if nothing_showed: 158 ## nothing is shown, then go through all events 159 self.event_num.setValue( index ) 160 return index 161 162 event_class = self.get_event_class( index ) 163 ## Show only if show event class is selected 164 if self.show_class[ event_class ].isChecked(): 165 self.event_num.setValue( index ) 166 return index 167 ## else go to next event 168 index = (index + 1)%nevents 169 self.event_num.setValue( index ) 170 return self.skip_nonselected_event( nevents, left-1 ) 171 172 173 def create_eventlayer(self): 174 """ Create a point layer that contains the events """ 175 features = {} 176 pts = [] 177 self.events = self.viewer.add_points( np.array(pts), properties=features, face_color="red", size = 10, symbol='x', name=self.eventlayer_name, scale=self.viewer.layers["Segmentation"].scale ) 178 self.event_types = {} 179 self.update_nevents_display() 180 self.epicure.finish_update() 181 182 def load_events(self, pts, features, event_types): 183 """ Load events data from file and reinitialize layer with it""" 184 ut.remove_layer(self.viewer, self.eventlayer_name) 185 symbols = np.repeat("x", len(pts)) 186 colors = np.repeat("white", len(pts)) 187 self.events = self.viewer.add_points( np.array(pts), properties=features, face_color=colors, size = 10, symbol=symbols, name=self.eventlayer_name, scale=self.viewer.layers["Segmentation"].scale ) 188 self.event_types = event_types 189 190 ## set the display of division events 191 self.events.selected_data = {} 192 self.select_feature_event( "division" ) 193 self.events.current_symbol = "o" 194 self.events.current_face_color = "#0055ffff" 195 self.events.selected_data = {} 196 self.select_feature_event( "extrusion" ) 197 self.events.current_symbol = "diamond" 198 self.events.current_face_color = "red" 199 self.events.refresh() 200 self.update_nevents_display() 201 self.show_hide_events() 202 self.epicure.finish_update() 203 204 205 ############### Display event options 206 def get_event_types( self ): 207 """ Returns the list of possible event types """ 208 return list( self.event_types.keys() ) 209 210 def update_nevents_display( self ): 211 """ Update the display of number of event""" 212 text = str(self.nb_events(only_suspect=True))+" suspects | " 213 text += str(self.nb_type("division"))+" divisions | " 214 text += str(self.nb_type("extrusion"))+" extrusions" 215 self.nevents_print.setText( text ) 216 217 def nb_events( self, only_suspect=False ): 218 """ Returns current number of events """ 219 if self.events is None: 220 return 0 221 if self.events.properties is None: 222 return 0 223 if "score" not in self.events.properties: 224 return 0 225 if not only_suspect: 226 return len(self.events.properties["score"]) 227 return ( len(self.events.properties["score"]) - self.nb_type("division") - self.nb_type("extrusion") ) 228 229 def get_events_from_type( self, feature ): 230 """ Return the list of events of a given type """ 231 if feature == "suspect": 232 sub_features = self.suspect_subtypes() 233 evts_id = [] 234 for feat in sub_features: 235 evts_id.extend( eid for eid in self.event_types[ feat ] if eid not in evts_id ) 236 return list( evts_id ) 237 if feature in self.event_types: 238 return self.event_types[ feature ] 239 return [] 240 241 def nb_type( self, feature ): 242 """ Return nb of event of given type """ 243 if self.events is None: 244 return 0 245 if (self.event_types is None) or (feature not in self.event_types): 246 return 0 247 return len(self.event_types[feature]) 248 249 def create_displayeventBlock(self): 250 ''' Block interface of displaying event layer options ''' 251 disp_layout = QVBoxLayout() 252 253 ## Color mode 254 colorlay, self.color_choice = wid.list_line( "Color by:", "Choose color to display the events", self.color_events ) 255 self.color_choice.addItem("None") 256 self.color_choice.addItem("score") 257 self.color_choice.addItem("track-2->1") 258 self.color_choice.addItem("track-1-2-*") 259 self.color_choice.addItem("track-length") 260 self.color_choice.addItem("track-gap") 261 self.color_choice.addItem("track-jump") 262 self.color_choice.addItem("division") 263 self.color_choice.addItem("area") 264 self.color_choice.addItem("solidity") 265 self.color_choice.addItem("intensity") 266 self.color_choice.addItem("tubeness") 267 disp_layout.addLayout(colorlay) 268 269 esize = int(self.epicure.reference_size/70+10) 270 msize = 100 271 if esize > 70: 272 msize = 200 273 esize = min( esize, 100 ) 274 sizelay, self.event_size = wid.slider_line( "Point size:", minval=0, maxval=msize, step=1, value=esize, show_value=True, slidefunc=self.display_event_size, descr="Choose the current point size display" ) 275 disp_layout.addLayout(sizelay) 276 277 ### Interface to select a event and zoom on it 278 chooselay, self.event_num = wid.ranged_value_line( label="event n°", minval=0, maxval=1000000, step=1, val=0, descr="Choose current event to display/remove" ) 279 disp_layout.addLayout(chooselay) 280 go_event_btn = wid.add_button( "Go to event", self.go_to_event, "Zoom and display current event" ) 281 disp_layout.addWidget(go_event_btn) 282 clear_event_btn = wid.add_button( "Remove current event", self.clear_event, "Delete current event from the list of events" ) 283 disp_layout.addWidget(clear_event_btn) 284 285 ## all features 286 self.displayevent.setLayout(disp_layout) 287 self.displayevent.setVisible( self.event_disp.isChecked() ) 288 289 ##### 290 def reset_event_range(self): 291 """ Reset the max num of event """ 292 nsus = len(self.events.data)-1 293 if self.event_num.value() > nsus: 294 self.event_num.setValue(0) 295 self.event_num.setMaximum(nsus) 296 297 def go_to_event(self): 298 """ Zoom on the currently selected event """ 299 num_event = int(self.event_num.value()) 300 ## if reached the end of possible events 301 if num_event >= self.nb_events(): 302 num_event = 0 303 self.event_num.setValue(0) 304 if num_event < 0: 305 if self.nb_events() == 0: 306 if self.epicure.verbose > 0: 307 print("No more event") 308 return 309 else: 310 self.event_num.setValue(0) 311 num_event = 0 312 pos = self.events.data[num_event] 313 event_id = self.events.properties["id"][num_event] 314 self.zoom_on_event( pos, event_id ) 315 316 def get_event_infos( self, sid ): 317 """ Get the properties of the event of given id """ 318 index = self.index_from_id( sid ) 319 pos = self.events.data[ index ] 320 label = self.events.properties[ "label" ][index] 321 return pos, label 322 323 def zoom_on_event( self, event_pos, event_id ): 324 """ Zoom on chose event at given position """ 325 evt_lay = self.viewer.layers[self.eventlayer_name] 326 epos = evt_lay.data_to_world(event_pos) 327 #pos = event_pos 328 #print(epos) 329 self.viewer.camera.center = tuple(epos) 330 self.viewer.camera.zoom = 5/self.epicure.epi_metadata["ScaleXY"] 331 ut.set_frame( self.viewer, int(epos[0]) ) 332 crimes = self.get_crimes(event_id) 333 if self.epicure.verbose > 0: 334 print("Suspected because of: "+str(crimes)) 335 336 def color_events(self): 337 """ Color points by the selected mode """ 338 color_mode = self.color_choice.currentText() 339 self.events.refresh_colors() 340 if color_mode == "None": 341 self.events.face_color = "white" 342 elif color_mode == "score": 343 self.set_colors_from_properties("score") 344 else: 345 self.set_colors_from_event_type(color_mode) 346 self.events.refresh_colors() 347 348 def suspect_subtypes( self ): 349 """ Return the list of suspect-related event types """ 350 features = list( self.event_types.keys() ) 351 if "division" in features: 352 features.remove( "division" ) 353 if "extrusion" in features: 354 features.remove( "extrusion" ) 355 return features 356 357 def show_subset_event( self, feature, show=True ): 358 """ Show/hide a subset (type) of event """ 359 tmp_size = int(self.event_size.value()) 360 size = 0.1 361 if show: 362 size = tmp_size 363 ## select the events of corresponding type 364 self.events.selected_data = {} 365 if not isinstance( feature, list ): 366 features = [feature] 367 else: 368 features = feature 369 if "suspect" in features: 370 ## take all possible features except non-suspect ones (division, extrusion..) 371 features.remove( "suspect" ) 372 features = features + self.suspect_subtypes() 373 374 posids = [] 375 for feat in features: 376 if feat in self.event_types: 377 posid = self.event_types[feat] 378 posids = posids + posid 379 nfound = len(posids) 380 if nfound <= 0: 381 return 382 for ind, cid in enumerate( self.events.properties["id"] ): 383 if cid in posids: 384 self.events._size[ind] = size 385 nfound = nfound - 1 386 ## finished, all updated 387 if nfound == 0: 388 break 389 ## reset selection and default size 390 self.events.selected_data = {} 391 self.events.current_size = tmp_size 392 self.events.refresh() 393 394 def select_feature_event( self, feature ): 395 """ Add all event of given feature to currently selected data """ 396 if feature not in self.event_types: 397 return 398 posid = self.event_types[feature] 399 nfound = len(posid) 400 for ind, cid in enumerate(self.events.properties["id"]): 401 if cid in posid: 402 self.events.selected_data.add( ind ) 403 nfound = nfound - 1 404 ## stop if found all of them 405 if nfound == 0: 406 return 407 408 def set_colors_from_event_type(self, feature): 409 """ Set colors from given event_type feature (eg area, tracking..) """ 410 if self.event_types.get(feature) is None: 411 self.events.face_color="white" 412 return 413 posid = self.event_types[feature] 414 colors = ["white"]*len(self.events.data) 415 ## change the color of all the positive events for the chosen feature 416 for sid in posid: 417 ind = self.index_from_id(sid) 418 if ind is not None: 419 colors[ind] = (0.8,0.1,0.1) 420 self.events.face_color = colors 421 422 def set_colors_from_properties(self, feature): 423 """ Set colors from given propertie (eg score, label) """ 424 ncols = (np.max(self.events.properties[feature])) 425 color_cycle = [] 426 for i in range(ncols): 427 color_cycle.append( (0.25+float(i/ncols*0.75), float(i/ncols*0.85), float(i/ncols*0.75)) ) 428 self.events.face_color_cycle = color_cycle 429 self.events.face_color = feature 430 431 def update_display(self): 432 """ Update the display of the events layer """ 433 self.events.refresh() 434 self.color_events() 435 436 def get_current_settings(self): 437 """ Returns current event widget parameters """ 438 disp = {} 439 disp["Point size"] = int(self.event_size.value()) 440 disp["Outliers ON"] = self.outlier_vis.isChecked() 441 disp["Track ON"] = self.track_vis.isChecked() 442 disp["EventDisp ON"] = self.event_disp.isChecked() 443 for i, eclass in enumerate(self.event_class): 444 disp["Show "+eclass] = self.show_class[i].isChecked() 445 disp["Ignore border"] = self.ignore_borders.isChecked() 446 disp["Ignore boundaries"] = self.ignore_boundaries.isChecked() 447 disp["Flag length"] = self.check_length.isChecked() 448 disp["Flag jump"] = self.check_jump.isChecked() 449 disp["length"] = self.min_length.text() 450 disp["Check size"] = self.check_size.isChecked() 451 disp["Check shape"] = self.check_shape.isChecked() 452 disp["Get merging"] = self.get_merge.isChecked() 453 disp["Get apparitions"] = self.get_apparition.isChecked() 454 disp["Get divisions"] = self.get_division.isChecked() 455 disp["Get disparitions"] = self.get_disparition.isChecked() 456 disp["Get extrusions"] = self.get_extrusions.isChecked() 457 disp["Get gaps"] = self.get_gaps.isChecked() 458 disp["threshold disparition"] = self.threshold_disparition.text() 459 disp["Min gap"] = self.min_gaps.text() 460 disp["Min area"] = self.min_area.text() 461 disp["Max area"] = self.max_area.text() 462 disp["Current frame"] = self.feat_onframe.isChecked() 463 return disp 464 465 def apply_settings( self, settings ): 466 """ Set the current state (display, widget) from preferences if any """ 467 for setting, val in settings.items(): 468 if setting == "Outliers ON": 469 self.outlier_vis.setChecked( val ) 470 if setting == "Track ON": 471 self.track_vis.setChecked( val ) 472 if setting =="EventDisp ON": 473 self.event_disp.setChecked( val ) 474 if setting == "Point size": 475 self.event_size.setValue( int(val) ) 476 #self.display_event_size() 477 for i, eclass in enumerate(self.event_class): 478 if setting == "Show "+eclass: 479 self.show_class[i].setChecked( val ) 480 #self.show_hide_events() 481 if setting == "Ignore border": 482 self.ignore_borders.setChecked( val ) 483 if setting == "Ignore boundaries": 484 self.ignore_boundaries.setChecked( val ) 485 if setting == "Flag length": 486 self.check_length.setChecked( val ) 487 if setting == "Flag jump": 488 self.check_jump.setChecked( val ) 489 if setting == "length": 490 self.min_length.setText( val ) 491 if setting == "Check size": 492 self.check_size.setChecked( val ) 493 if setting == "Check shape": 494 self.check_shape.setChecked( val ) 495 if setting == "Get merging": 496 self.get_merge.setChecked( val ) 497 if setting == "Get apparitions": 498 self.get_apparition.setChecked( val ) 499 if setting == "Get divisions": 500 self.get_division.setChecked( val ) 501 if setting == "Get disparitions": 502 self.get_disparition.setChecked( val ) 503 if setting == "Get extrusions": 504 self.get_extrusions.setChecked( val ) 505 if setting == "Get gaps": 506 self.get_gaps.setChecked( val ) 507 if setting == "Threshold disparition": 508 self.threshold_disparition.setText( val ) 509 if setting == "Min gap": 510 self.min_gaps.setText( val ) 511 if setting == "Min area": 512 self.min_area.setText( val ) 513 if setting == "Max area": 514 self.max_area.setText( val ) 515 if setting == "Current frame": 516 self.feat_onframe.setChecked( val ) 517 518 519 def display_event_size(self): 520 """ Change the size of the point display """ 521 size = int(self.event_size.value()) 522 self.events.size = size 523 self.events.refresh() 524 #### Depend on event type, to update 525 526 ############### eventing functions 527 def get_crimes(self, sid): 528 """ For a given event, get its event_type(s) """ 529 crimes = [] 530 for feat in self.event_types.keys(): 531 if sid in self.event_types.get(feat): 532 crimes.append(feat) 533 return crimes 534 535 def add_event_type(self, ind, sid, feature): 536 """ Add 1 to the event_type score for given feature """ 537 #print(self.event_types) 538 if self.event_types.get(feature) is None: 539 self.event_types[feature] = [] 540 self.event_types[feature].append(sid) 541 score = self.events.properties["score"].copy() 542 score[ind] = score[ind] + 1 543 self.events.properties["score"] = score 544 self.events.properties["score"].flags.writeable = True 545 #self.events.properties() 546 547 def first_event(self, pos, label, featurename): 548 """ Addition of the first event (initialize all) """ 549 ut.remove_layer(self.viewer, "Events") 550 features = {} 551 sid = self.new_event_id() 552 features["id"] = np.array([sid], dtype="uint16") 553 features["label"] = np.array([label], dtype=self.epicure.dtype) 554 features["score"] = np.array([0], dtype="uint8") 555 pts = [pos] 556 self.events = self.viewer.add_points( np.array(pts), properties=features, face_color="score", size = int( self.event_size.value() ), symbol="x", name="Events", scale=self.viewer.layers["Segmentation"].scale ) 557 props = self.events.properties 558 props["label"].flags.writeable = True 559 props["score"].flags.writeable = True 560 props["id"].flags.writeable = True 561 self.add_event_type(0, sid, featurename) 562 self.events.refresh() 563 self.update_nevents_display() 564 565 def add_event(self, pos, label, reason, symb="x", color="white", force=False, refresh=True): 566 """ Add a event to the list, evented by a feature """ 567 if (not force) and (self.ignore_borders.isChecked()) and (self.border_cells is not None): 568 tframe = int(pos[0]) 569 if label in self.border_cells[tframe]: 570 return 571 572 if (not force) and (self.ignore_boundaries.isChecked()) and (self.boundary_cells is not None): 573 tframe = int(pos[0]) 574 if label in self.boundary_cells[tframe]: 575 return 576 577 ## initialise if necessary 578 if len(self.events.data) <= 0: 579 self.first_event(pos, label, reason) 580 return 581 582 self.events.selected_data = [] 583 584 ## look if already evented, then add the charge 585 num, sid = self.find_event(pos[0], label) 586 if num is not None: 587 ## event already in the list. For same crime ? 588 if self.event_types.get(reason) is not None: 589 if sid not in self.event_types[reason]: 590 self.add_event_type(num, sid, reason) 591 else: 592 self.add_event_type(num, sid, reason) 593 else: 594 ## new event, add to the Point layer 595 ind = len(self.events.data) 596 sid = self.new_event_id() 597 self.events.add(pos) 598 props = self.events.properties 599 props["label"].flags.writeable = True 600 props["score"].flags.writeable = True 601 props["id"].flags.writeable = True 602 props["label"][ind] = label 603 props["id"][ind] = sid 604 props["score"][ind] = 0 605 self.add_event_type(ind, sid, reason) 606 607 self.events.symbol.flags.writeable = True 608 self.events.current_symbol = symb 609 self.events.current_face_color = color 610 if refresh: 611 self.refresh_events() 612 613 def refresh_events( self ): 614 """ Refresh event view and text """ 615 self.events.refresh() 616 self.update_nevents_display() 617 self.reset_event_range() 618 self.epicure.finish_update() 619 620 def new_event_id(self): 621 """ Find the first unused id """ 622 sid = 0 623 if self.events.properties.get("id") is None: 624 return 0 625 while sid in self.events.properties["id"]: 626 sid = sid + 1 627 return sid 628 629 def reset_events_choice( self ): 630 """ Interface to choose event(s) to reset/update """ 631 632 class ResetChoice( QWidget ): 633 """ Choices of event(s) and update or reset """ 634 def __init__( self, insp ): 635 super().__init__() 636 self.insp = insp 637 poplayout = wid.vlayout() 638 639 ## Handle division events 640 update_div_btn = wid.add_button( btn="Update divisions from graph", btn_func=self.insp.get_divisions, descr="Update the list of division events from the track graph" ) 641 poplayout.addWidget(update_div_btn) 642 poplayout.addWidget( wid.separation() ) 643 644 ### Reset: delete all events 645 reset_color = self.insp.epicure.get_resetbtn_color() 646 reset_event_btn = wid.add_button( btn="Reset all events", btn_func=self.insp.reset_all_events, descr="Delete all current events", color=reset_color ) 647 poplayout.addWidget( reset_event_btn ) 648 649 ## Reset: specific events 650 reset_line = wid.hlayout() 651 for i, eclass in enumerate( self.insp.event_class ): 652 go_btn = wid.add_button( btn="Reset "+eclass, btn_func=None, descr="Reset "+eclass+" events only", color=reset_color ) 653 go_btn.clicked.connect( lambda i=i, eclass=eclass: self.insp.reset_event_type( eclass, frame=None ) ) 654 reset_line.addWidget( go_btn ) 655 poplayout.addLayout( reset_line ) 656 657 poplayout.addWidget( wid.separation() ) 658 ## Remove events on border 659 bord_lab = wid.label_line( "Remove if on BORDER:") 660 bord_line = wid.hlayout() 661 for i, eclass in enumerate( self.insp.event_class ): 662 if eclass != "suspect": 663 go_btn = wid.add_button( btn=""+eclass, btn_func=None, descr="Remove "+eclass+" events if they are on border" ) 664 go_btn.clicked.connect( lambda i=i, eclass=eclass: self.insp.remove_event_border( eclass ) ) 665 bord_line.addWidget( go_btn ) 666 poplayout.addWidget( bord_lab ) 667 poplayout.addLayout( bord_line ) 668 669 ## Remove events on boundaries 670 bound_lab = wid.label_line( "Remove if on BOUNDARY:") 671 bound_line = wid.hlayout() 672 for i, eclass in enumerate( self.insp.event_class ): 673 if eclass != "suspect": 674 go_btn = wid.add_button( btn=""+eclass, btn_func=None, descr="Remove "+eclass+" events if they are on boundary" ) 675 go_btn.clicked.connect( lambda i=i, eclass=eclass: self.insp.remove_event_boundary( eclass ) ) 676 bound_line.addWidget( go_btn ) 677 poplayout.addWidget( bound_lab ) 678 poplayout.addLayout( bound_line ) 679 poplayout.addWidget( wid.separation() ) 680 681 682 self.setLayout( poplayout ) 683 684 #def close( self ): 685 # """ Close the pop-up window """ 686 # self.hide() 687 rc = ResetChoice( self ) 688 rc.show() 689 690 691 def remove_event_border( self, evt_type ): 692 """ Remove events of given types if they are on border cells """ 693 if self.event_types.get( evt_type ) is None: 694 return 695 696 pbar = ut.start_progress( self.viewer, total=self.epicure.nframes+1, descr="Detecting border cells" ) 697 ## get/update the list of border cells 698 self.get_border_cells() 699 700 ## check all event_type events if they are on border cells 701 idlist = self.event_types[ evt_type ].copy() 702 for sid in idlist: 703 ind = self.index_from_id(sid) 704 if ind is not None: 705 ## get the event cell label and frame 706 lab = self.events.properties["label"][ind] 707 frame = self.events.data[ind][0] 708 if evt_type == "division": 709 frame = frame - 1 710 if frame is not None: 711 if lab in self.border_cells[ frame ]: 712 ## event is on border, remove it 713 self.event_types[ evt_type ].remove( sid ) 714 self.decrease_score( ind ) 715 716 ## update displays 717 ut.close_progress( self.viewer, pbar ) 718 self.events.refresh() 719 self.update_nevents_display() 720 721 def remove_event_boundary( self, evt_type ): 722 """ Remove events of given types if they are on boundary cells """ 723 if self.event_types.get( evt_type ) is None: 724 return 725 726 pbar = ut.start_progress( self.viewer, total=self.epicure.nframes+1, descr="Detecting boundary cells" ) 727 ## get/update the list of border cells 728 self.get_boundaries_cells( pbar ) 729 730 ## check all event_type events if they are on border cells 731 idlist = self.event_types[ evt_type ].copy() 732 for sid in idlist: 733 ind = self.index_from_id(sid) 734 if ind is not None: 735 ## get the event cell label and frame 736 lab = self.events.properties["label"][ind] 737 frame = self.events.data[ind][0] 738 if evt_type == "division": 739 frame = frame - 1 740 if frame is not None: 741 if lab in self.boundary_cells[ frame ]: 742 ## event is on border, remove it 743 self.event_types[ evt_type ].remove( sid ) 744 self.decrease_score( ind ) 745 746 ## update displays 747 ut.close_progress( self.viewer, pbar ) 748 self.events.refresh() 749 self.update_nevents_display() 750 751 def reset_all_events(self): 752 """ Remove all event_types """ 753 features = {} 754 pts = [] 755 ut.remove_layer(self.viewer, "Events") 756 self.events = self.viewer.add_points( np.array(pts), properties=features, face_color="red", size = 10, symbol='x', name="Events", scale=self.viewer.layers["Segmentation"].scale ) 757 self.event_types = {} 758 self.update_nevents_display() 759 #self.update_nevents_display() 760 self.epicure.finish_update() 761 762 def reset_event_type(self, feature, frame ): 763 """ Remove all event_types of given feature, for current frame or all if frame is None """ 764 if self.event_types.get(feature) is None: 765 return 766 idlist = self.event_types[feature].copy() 767 for sid in idlist: 768 ind = self.index_from_id(sid) 769 if ind is not None: 770 if frame is not None: 771 if int(self.events.data[ind][0]) == frame: 772 self.event_types[feature].remove(sid) 773 self.decrease_score(ind) 774 else: 775 self.event_types[feature].remove(sid) 776 self.decrease_score(ind) 777 self.events.refresh() 778 self.update_nevents_display() 779 780 def remove_event_types(self, sid): 781 """ Remove all event_types of given event id """ 782 for listval in self.event_types.values(): 783 if sid in listval: 784 listval.remove(sid) 785 786 def decrease_score(self, ind): 787 """ Decrease by one score of event at index ind. Delete it if reach 0""" 788 score = self.events.properties["score"] 789 score.flags.writeable = True 790 score[ind] = score[ind] - 1 791 if self.events.properties["score"][ind] == 0: 792 self.exonerate_one( ind, remove_division=False ) 793 self.update_nevents_display() 794 795 def index_from_id(self, sid): 796 """ From event id, find the corresponding index in the properties array """ 797 for ind, cid in enumerate(self.events.properties["id"]): 798 if cid == sid: 799 return ind 800 return None 801 802 def id_from_index( self, ind ): 803 """ From event index, returns it id """ 804 return self.events.properties["id"][ind] 805 806 def find_event(self, frame, label): 807 """ Find if there is already a event at given frame and label """ 808 events = self.events.data 809 events_lab = self.events.properties["label"] 810 for i, lab in enumerate(events_lab): 811 if lab == label: 812 if events[i][0] == frame: 813 return i, self.events.properties["id"][i] 814 return None, None 815 816 def init_suggestion(self): 817 """ Initialize the layer that will contains propostion of tracks/segmentations """ 818 suggestion = np.zeros(self.seglayer.data.shape, dtype="uint16") 819 self.suggestion = self.viewer.add_labels(suggestion, blending="additive", name="Suggestion") 820 821 @self.seglayer.mouse_drag_callbacks.append 822 def click(layer, event): 823 if event.type == "mouse_press": 824 if 'Alt' in event.modifiers: 825 if event.button == 1: 826 pos = event.position 827 # alt+left click accept suggestion under the mouse pointer (in all frames) 828 self.accept_suggestion(pos) 829 830 def accept_suggestion(self, pos): 831 """ Accept the modifications of the label at position pos (all the label) """ 832 seglayer = self.viewer.layers["Segmentation"] 833 label = self.suggestion.data[tuple(map(int, pos))] 834 found = self.suggestion.data==label 835 self.exonerate( found, seglayer ) 836 indices = np.argwhere( found ) 837 ut.setNewLabel( seglayer, indices, label, add_frame=None ) 838 self.suggestion.data[self.suggestion.data==label] = 0 839 self.suggestion.refresh() 840 self.update_nevents_display() 841 842 def remove_one_event( self, event_id ): 843 """ Remove the given event from its id """ 844 if self.events is None: 845 return 846 ind = self.index_from_id(event_id) 847 if ind is not None: 848 self.exonerate_one( ind ) 849 self.update_nevents_display() 850 self.events.refresh() 851 852 def exonerate_one(self, ind, remove_division=True): 853 """ Remove one event at index ind """ 854 self.events.selected_data = [ind] 855 sid = self.events.properties["id"][ind] 856 if (remove_division) and ("division" in self.event_types.keys()) and (sid in self.event_types["division"]): 857 self.epicure.tracking.remove_division( self.events.properties["label"][ind] ) 858 self.events.remove_selected() 859 self.remove_event_types(sid) 860 861 def clear_event(self): 862 """ Remove the current event """ 863 num_event = int(self.event_num.value()) 864 self.exonerate_one( num_event, remove_division=True ) 865 self.update_nevents_display() 866 867 def exonerate_from_event(self, event): 868 """ Remove all events in the corresponding cell of position """ 869 label = ut.getCellValue( self.seglayer, event ) 870 if len(self.events.data) > 0: 871 for ind, lab in enumerate(self.events.properties["label"]): 872 if lab == label: 873 if self.events.data[ind][0] == event.position[0]: 874 self.exonerate_one(ind, remove_division=True) 875 self.update_nevents_display() 876 877 def exonerate(self, indices, seglayer): 878 """ Remove events that have been corrected/cleared """ 879 seglabels = np.unique(seglayer.data[indices]) 880 selected = [] 881 if self.events.properties.get("label") is None: 882 return 883 for ind, lab in enumerate(self.events.properties["label"]): 884 if lab in seglabels: 885 ## label to remove from event list 886 selected.append(ind) 887 if len(selected) > 0: 888 self.events.selected_data = selected 889 self.events.remove_selected() 890 self.update_nevents_display() 891 892 893 #######################################" 894 ## Outliers suggestion functions 895 def show_outlierBlock(self): 896 self.featOutliers.setVisible( self.outlier_vis.isChecked() ) 897 898 def create_outliersBlock(self): 899 ''' Block interface of functions for error suggestions based on cell features ''' 900 feat_layout = QVBoxLayout() 901 902 self.feat_onframe = wid.add_check( check="Only current frame", checked=True, check_func=None, descr="Search for outliers only in current frame" ) 903 feat_layout.addWidget(self.feat_onframe) 904 905 ## area widget 906 tarea_layout, self.min_area, self.max_area = wid.min_button_max( btn="< Area (pix^2) <", btn_func=self.event_area_threshold, min_val="0", max_val="2000", descr="Look for cell which size is outside the given area range" ) 907 feat_layout.addLayout( tarea_layout ) 908 909 ## solid widget 910 feat_solid_line, self.fsolid_out = wid.button_parameter_line( btn="Solidity outliers", btn_func=self.event_solidity, value="3.0", descr_btn="Search for outliers in solidity value", descr_value="Inter-quartiles range factor to consider outlier" ) 911 feat_layout.addLayout( feat_solid_line ) 912 913 ## intensity widget 914 feat_inten_line, self.fintensity_out = wid.button_parameter_line( btn="Intensity cytoplasm/junction", btn_func=self.event_intensity, value="1.0", descr_btn="Search for outliers in intensity ratio", descr_value="Ratio of intensity above which the cell looks suspect" ) 915 feat_layout.addLayout( feat_inten_line ) 916 917 ## tubeness widget 918 feat_tub_line, self.ftub_out = wid.button_parameter_line( btn="Tubeness cytoplasm/junction", btn_func=self.event_tubeness, value="1.0", descr_btn="Search for outliers in tubeness ratio", descr_value="Ratio of tubeness above which the cell looks suspect" ) 919 feat_layout.addLayout( feat_tub_line ) 920 921 ## all features 922 self.featOutliers.setLayout(feat_layout) 923 self.featOutliers.setVisible( self.outlier_vis.isChecked() ) 924 925 def event_feature(self, featname, funcname ): 926 """ event in one frame or all frames the given feature """ 927 onframe = self.feat_onframe.isChecked() 928 if onframe: 929 tframe = ut.current_frame(self.viewer) 930 self.reset_event_type(featname, tframe) 931 funcname(tframe) 932 else: 933 self.reset_event_type(featname, None) 934 for frame in range(self.seglayer.data.shape[0]): 935 funcname(frame) 936 self.update_display() 937 ut.set_active_layer( self.viewer, "Segmentation" ) 938 939 def inspect_outliers(self, tab, props, tuk, frame, feature): 940 q1 = np.quantile(tab, 0.25) 941 q3 = np.quantile(tab, 0.75) 942 qtuk = tuk * (q3-q1) 943 for sign in [1, -1]: 944 #thresh = np.mean(tab) + sign * np.std(tab)*tuk 945 if sign > 0: 946 thresh = q3 + qtuk 947 else: 948 thresh = q1 - qtuk 949 for i in np.where((tab-thresh)*sign>0)[0]: 950 position = ut.prop_to_pos( props[i], frame ) 951 self.add_event( position, props[i].label, feature ) 952 953 def event_area_threshold(self): 954 """ Look for cell's area below/above a threshold """ 955 self.event_feature( "area", self.event_area_threshold_oneframe ) 956 957 def event_area_threshold_oneframe( self, tframe ): 958 """ Check if area is above/below given threshold """ 959 minarea = int(self.min_area.text()) 960 maxarea = int(self.max_area.text()) 961 frame_props = self.epicure.get_frame_features( tframe ) 962 for prop in frame_props: 963 if (prop.area < minarea) or (prop.area > maxarea): 964 position = ut.prop_to_pos( prop, tframe ) 965 self.add_event( position, prop.label, "area" ) 966 967 968 def event_area(self, state): 969 """ Look for outliers in term of cell area """ 970 self.event_feature( "area", self.event_area_oneframe ) 971 972 def event_area_oneframe(self, frame): 973 seglayer = self.seglayer.data[frame] 974 props = regionprops(seglayer) 975 ncell = len(props) 976 areas = np.zeros((ncell,1), dtype="float") 977 for i, prop in enumerate(props): 978 if prop.label > 0: 979 areas[i] = prop.area 980 tuk = self.farea_out.value() 981 self.inspect_outliers(areas, props, tuk, frame, "area") 982 983 def event_solidity(self, state): 984 """ Look for outliers in term ofz cell solidity """ 985 self.event_feature( "solidity", self.event_solidity_oneframe ) 986 987 def event_solidity_oneframe(self, frame): 988 seglayer = self.seglayer.data[frame] 989 props = regionprops(seglayer) 990 ncell = len(props) 991 sols = np.zeros((ncell,1), dtype="float") 992 for i, prop in enumerate(props): 993 if prop.label > 0: 994 sols[i] = prop.solidity 995 tuk = float(self.fsolid_out.text()) 996 self.inspect_outliers(sols, props, tuk, frame, "solidity") 997 998 def event_intensity(self, state): 999 """ Look for abnormal intensity inside/periph ratio """ 1000 self.event_feature( "intensity", self.event_intensity_oneframe ) 1001 1002 def event_intensity_oneframe(self, frame): 1003 seglayer = self.seglayer.data[frame] 1004 intlayer = self.viewer.layers["Movie"].data[frame] 1005 props = regionprops(seglayer) 1006 for i, prop in enumerate(props): 1007 if prop.label > 0: 1008 self.test_intensity( intlayer, prop, frame ) 1009 1010 def test_intensity(self, inten, prop, frame): 1011 """ Test if intensity inside is much smaller than at periphery """ 1012 bbox = prop.bbox 1013 intbb = inten[bbox[0]:bbox[2], bbox[1]:bbox[3]] 1014 footprint = disk(radius=self.epicure.thickness) 1015 inside = binary_erosion(prop.image, footprint) 1016 ininten = np.mean(intbb*inside) 1017 dil_img = binary_dilation(prop.image, footprint) 1018 periph = dil_img^inside 1019 periphint = np.mean(intbb*periph) 1020 if (periphint<=0) or (ininten/periphint > float(self.fintensity_out.text())): 1021 position = ( frame, int(prop.centroid[0]), int(prop.centroid[1]) ) 1022 self.add_event( position, prop.label, "intensity" ) 1023 1024 def event_tubeness(self, state): 1025 """ Look for abnormal tubeness inside vs periph """ 1026 self.event_feature( "tubeness", self.event_tubeness_oneframe ) 1027 1028 def event_tubeness_oneframe(self, frame): 1029 seglayer = self.seglayer.data[frame] 1030 mov = self.viewer.layers["Movie"].data[frame] 1031 sated = np.copy(mov) 1032 sated = filters.sato(sated, black_ridges=False) 1033 props = regionprops(seglayer) 1034 for i, prop in enumerate(props): 1035 if prop.label > 0: 1036 self.test_tubeness( sated, prop, frame ) 1037 1038 def test_tubeness(self, sated, prop, frame): 1039 """ Test if tubeness inside is much smaller than tubeness on periph """ 1040 bbox = prop.bbox 1041 satbb = sated[bbox[0]:bbox[2], bbox[1]:bbox[3]] 1042 footprint = disk(radius=self.epicure.thickness) 1043 inside = binary_erosion(prop.image, footprint) 1044 intub = np.mean(satbb*inside) 1045 periph = prop.image^inside 1046 periphtub = np.mean(satbb*periph) 1047 if periphtub <= 0: 1048 position = ( frame, int(prop.centroid[0]), int(prop.centroid[1]) ) 1049 self.add_event( position, prop.label, "tubeness" ) 1050 else: 1051 if intub/periphtub > float(self.ftub_out.text()): 1052 position = ( frame, int(prop.centroid[0]), int(prop.centroid[1]) ) 1053 self.add_event( position, prop.label, "tubeness" ) 1054 1055 1056############# event based on track 1057 1058 def show_tracksBlock(self): 1059 self.eventTrack.setVisible( self.track_vis.isChecked() ) 1060 1061 def create_tracksBlock(self): 1062 ''' Block interface of functions for error suggestions based on tracks ''' 1063 track_layout = QVBoxLayout() 1064 1065 hign = wid.hlayout() 1066 ignore_label = wid.label_line( "Ignore cells on:") 1067 hign.addWidget( ignore_label ) 1068 vign = wid.vlayout() 1069 self.ignore_borders = wid.add_check( "border (of image)", False, None, "When adding suspect, don't add it if the cell is touching the border of the image" ) 1070 vign.addWidget(self.ignore_borders) 1071 1072 self.ignore_boundaries = wid.add_check( "tissue boundaries", False, None, "When adding suspect, don't add it if the cell is on the tissu boundaries (no neighbor in one side)" ) 1073 vign.addWidget(self.ignore_boundaries) 1074 hign.addLayout( vign ) 1075 track_layout.addLayout( hign ) 1076 1077 ## Look for merging tracks 1078 self.get_merge = wid.add_check( "Flag track merging", True, None, "Add a suspect if two track merge in one" ) 1079 track_layout.addWidget(self.get_merge) 1080 1081 ## Look for sudden appearance of tracks 1082 self.get_apparition = wid.add_check( "Flag track apparition", True, None, "Add a suspect if a track appears in the middle of the movie (not on border)" ) 1083 track_layout.addWidget(self.get_apparition) 1084 1085 self.get_division = wid.add_check( "Get divisions", False, None, "Add a division if two touching track appears while a potential parent track disappear" ) 1086 track_layout.addWidget(self.get_division) 1087 1088 ## Look for sudden disappearance of tracks 1089 dsp_layout = wid.hlayout() 1090 self.get_disparition = wid.add_check( check="Flag track disparition", checked=True, check_func=None, descr="Add a suspect if a track disappears (not last frame, not border)" ) 1091 disp_line, self.threshold_disparition = wid.value_line( label="cell area threshold", default_value="200", descr="Flag cell if cell area is above threshold" ) 1092 self.get_extrusions = wid.add_check( "Get extrusions", True, None, "Add extrusions events when a track is disappearing normally (below cell area threshold)" ) 1093 vlay = wid.vlayout() 1094 vlay.addWidget( self.get_disparition ) 1095 vlay.addWidget( self.get_extrusions ) 1096 dsp_layout.addLayout( vlay ) 1097 dsp_layout.addLayout( disp_line ) 1098 track_layout.addLayout( dsp_layout ) 1099 1100 ## Look for temporal gaps in tracks 1101 gap_line, self.get_gaps, self.min_gaps = wid.check_value( check="Flag track gaps", checkfunc=None, checked=True, value="1", descr="Add a suspect if a track has gaps longer than threshold (in nb of frames)", label="if gap above" ) 1102 track_layout.addLayout( gap_line ) 1103 1104 ## track length event_types 1105 ilengthlay, self.check_length, self.min_length = wid.check_value( check="Flag tracks smaller than", checkfunc=None, checked=True, value="1", descr="Add a suspect event for each track smaller than chosen value (in number of frames)" ) 1106 track_layout.addLayout(ilengthlay) 1107 1108 ## track sudden jump in position 1109 ijumplay, self.check_jump, self.jump_factor = wid.check_value( check="Flag jump in track position", checkfunc=None, checked=True, value="3.0", descr="Add a suspect event for when the position of cell centroid moves suddenly a lot compared to the rest of the track" ) 1110 track_layout.addLayout(ijumplay) 1111 1112 ## Variability in feature event_type 1113 sizevar_line, self.check_size, self.size_variability = wid.check_value( check="Size variation", checkfunc=None, checked=False, value="3", descr="Add a suspect if the size of the cell varies suddenly in the track" ) 1114 track_layout.addLayout( sizevar_line ) 1115 shapevar_line, self.check_shape, self.shape_variability = wid.check_value( check="Shape variation", checkfunc=None, checked=False, value="3.0", descr="Add a suspect if the shape of the cell varies suddenly in the track" ) 1116 track_layout.addLayout( shapevar_line ) 1117 1118 ## merge/split combinaisons 1119 track_btn = wid.add_button( btn="Inspect track", btn_func=self.inspect_tracks, descr="Start track analysis to look for suspects based on selected features" ) 1120 track_layout.addWidget(track_btn) 1121 1122 ## all features 1123 self.eventTrack.setLayout(track_layout) 1124 self.eventTrack.setVisible( self.track_vis.isChecked() ) 1125 1126 def reset_tracking_event(self): 1127 """ Remove events from tracking """ 1128 self.reset_event_type("track-1-2-*", None) 1129 self.reset_event_type("track-2->1", None) 1130 self.reset_event_type("track-length", None) 1131 self.reset_event_type("track-jump", None) 1132 self.reset_event_type("track-size", None) 1133 self.reset_event_type("track-shape", None) 1134 self.reset_event_type("track-apparition", None) 1135 self.reset_event_type("track-disparition", None) 1136 self.reset_event_type("track-gap", None) 1137 if self.get_extrusions.isChecked(): 1138 self.reset_event_type("extrusion", None) 1139 self.reset_event_range() 1140 1141 def track_length(self): 1142 """ Find all cells that are only in one frame """ 1143 max_len = int(self.min_length.text()) 1144 labels, lengths, positions = self.epicure.tracking.get_small_tracks( max_len ) 1145 ## remove track from first and last frame 1146 first_tracks = self.epicure.tracking.get_tracks_on_frame( 0 ) 1147 last_tracks = self.epicure.tracking.get_tracks_on_frame( self.epicure.nframes-1 ) 1148 for label, nframe, pos in zip(labels, lengths, positions): 1149 if label in first_tracks or label in last_tracks: 1150 ## present in the first or last track, don't check it 1151 continue 1152 if self.epicure.verbose > 2: 1153 print("event track length "+str(nframe)+": "+str(label)+" frame "+str(pos[0]) ) 1154 self.add_event(pos, label, "track-length", refresh=False) 1155 self.refresh_events() 1156 1157 def inspect_tracks( self, subprogress=True ): 1158 """ Look for suspicious tracks """ 1159 ut.set_visibility( self.viewer, "Events", True ) 1160 progress_bar = ut.start_progress( self.viewer, total=10 ) 1161 if subprogress: 1162 ## show subprogress bars in sub functions (doesn't work on notebook without interface) 1163 pb = progress_bar 1164 else: 1165 pb= None 1166 progress_bar.update(0) 1167 self.reset_tracking_event() 1168 progress_bar.update(1) 1169 if self.ignore_borders.isChecked() or self.ignore_boundaries.isChecked(): 1170 progress_bar.set_description("Identifying border and/or boundaries cells") 1171 self.get_outside_cells() 1172 progress_bar.update(2) 1173 tracks = self.epicure.tracking.get_track_list() 1174 if self.check_length.isChecked(): 1175 progress_bar.set_description("Identifying too small tracks") 1176 self.track_length() 1177 progress_bar.update(3) 1178 if self.get_merge.isChecked(): 1179 progress_bar.set_description("Inspect tracks 2->1") 1180 self.track_21() 1181 progress_bar.update(4) 1182 if (self.check_size.isChecked()) or self.check_shape.isChecked(): 1183 progress_bar.set_description("Inspect track features") 1184 self.track_features() 1185 progress_bar.update(5) 1186 if self.get_apparition.isChecked() or self.get_division.isChecked(): 1187 progress_bar.set_description("Check new track apparition and/or division") 1188 self.track_apparition( tracks ) 1189 progress_bar.update(6) 1190 if self.get_disparition.isChecked() or self.get_extrusions.isChecked(): 1191 progress_bar.set_description("Check track disparition and/or extrusion") 1192 self.track_disparition( tracks, pb ) 1193 progress_bar.update(7) 1194 if self.get_gaps.isChecked(): 1195 progress_bar.set_description("Check temporal gaps in tracks") 1196 self.track_gaps( tracks, pb ) 1197 progress_bar.update(8) 1198 if self.check_jump.isChecked(): 1199 progress_bar.set_description("Check position jump in tracks") 1200 self.track_position_jump( tracks, pb ) 1201 progress_bar.update(9) 1202 ut.close_progress( self.viewer, progress_bar ) 1203 ut.set_active_layer( self.viewer, "Segmentation" ) 1204 1205 def track_apparition( self, tracks ): 1206 """ Check if some track appears suddenly (in the middle of the movie and not by division) """ 1207 start_time = time.time() 1208 ## remove track on first frame 1209 ctracks = list( set(tracks) - set( self.epicure.tracking.get_tracks_on_frame( 0 ) ) ) 1210 graph = self.epicure.tracking.graph 1211 do_divisions = self.get_division.isChecked() 1212 do_apparition = self.get_apparition.isChecked() 1213 apparitions = {} 1214 for i, track_id in enumerate( ctracks) : 1215 fframe = self.epicure.tracking.get_first_frame( track_id ) 1216 ## If on the border, ignore 1217 #outside = self.epicure.cell_on_border( track_id, fframe ) 1218 #if outside: 1219 # continue 1220 ## Not on border, check if potential division 1221 if (graph is not None) and (track_id in graph.keys()): 1222 continue 1223 ## event apparition 1224 if (not do_divisions) and do_apparition: 1225 self.add_apparition( fframe, track_id ) 1226 else: 1227 if fframe not in apparitions: 1228 apparitions[fframe] = [track_id] 1229 else: 1230 apparitions[fframe].append(track_id) 1231 if do_divisions: 1232 self.apparition_or_division( apparitions, do_apparition ) 1233 self.refresh_events() 1234 if self.epicure.verbose > 1: 1235 ut.show_duration( start_time, "Tracks apparition took " ) 1236 1237 def add_apparition( self, frame, trackid ): 1238 """ Add a suspect apparition to events """ 1239 posxy = self.epicure.tracking.get_position( trackid, frame ) 1240 if posxy is not None: 1241 pos = [ frame, posxy[0], posxy[1] ] 1242 if self.epicure.verbose > 2: 1243 print("Appearing track: "+str(trackid)+" at frame "+str(frame) ) 1244 self.add_event(pos, trackid, "track-apparition", refresh=False) 1245 1246 def apparition_or_division( self, apevents, do_apparition ): 1247 """ Check if detected events are apparitions or divisions """ 1248 for frame, tracks in apevents.items(): 1249 if len(tracks) == 1: 1250 ## only one event, apparition 1251 if do_apparition: 1252 self.add_apparition( frame, tracks[0] ) 1253 else: 1254 # look for potential neighbors for each apparition at this frame 1255 ind = 0 1256 while ind < len(tracks): 1257 ctrack = tracks[ind] 1258 ## already treated 1259 if ctrack < 0: 1260 ind = ind + 1 1261 continue 1262 dind = ind + 1 1263 found = False 1264 while dind < len(tracks): 1265 ## skip if already done 1266 if tracks[dind] < 0: 1267 dind = dind + 1 1268 continue 1269 ## check if labels are touching at the appearing frame 1270 bbox, merged = ut.getBBox2DMerge( self.epicure.seg[frame], ctrack, tracks[dind] ) 1271 bbox = ut.extendBBox2D( bbox, 1.05, self.epicure.imgshape2D ) 1272 segt_crop = ut.cropBBox2D( self.epicure.seg[frame], bbox ) 1273 touched = ut.checkTouchingLabels( segt_crop, ctrack, tracks[dind] ) 1274 if touched: 1275 ## found neighbor, potential division 1276 found = True 1277 if self.epicure.editing.add_division( ctrack, tracks[dind], frame ): 1278 ## division added successfully 1279 tracks[dind] = -1 ## track done 1280 break 1281 else: 1282 ## failed to add a division, so mark it as apparition 1283 if do_apparition: 1284 self.add_apparition( frame, ctrack ) 1285 break 1286 else: 1287 dind = dind + 1 1288 # no neighbor found, so mark this as an apparition 1289 if (not found) and do_apparition: 1290 if ctrack > 0: 1291 self.add_apparition( frame, ctrack ) 1292 ind = ind + 1 1293 #print(self.epicure.tracking.graph) 1294 1295 1296 1297 def track_disparition( self, tracks, progress_bar ): 1298 """ Check if some track disappears suddenly (in the middle of the movie and not by division) """ 1299 start_time = ut.start_time() 1300 ## Track disappears in the movie, not last frame 1301 ctracks = list( set(tracks) - set( self.epicure.tracking.get_tracks_on_frame( self.epicure.nframes-1 ) ) ) 1302 threshold_area = float(self.threshold_disparition.text()) 1303 if progress_bar is not None: 1304 sub_bar = progress( total = len( ctracks ), desc="Check non last frame tracks", nest_under = progress_bar ) 1305 for i, track_id in enumerate( ctracks ): 1306 if progress_bar is not None: 1307 sub_bar.update( i ) 1308 lframe = self.epicure.tracking.get_last_frame( track_id ) 1309 ## If on the border, ignore 1310 #outside = self.epicure.cell_on_border( track_id, lframe ) 1311 #if outside: 1312 # continue 1313 ## Not on border, check if potential division 1314 if self.epicure.tracking.is_single_parent( track_id ): 1315 continue 1316 1317 ## check if the cell area is below the threshold, then considered as ok (likely extrusion) 1318 if (threshold_area > 0): 1319 cell_area = self.epicure.cell_area( track_id, lframe ) 1320 if cell_area < threshold_area: 1321 if self.get_extrusions.isChecked(): 1322 fframe = self.epicure.tracking.get_first_frame( track_id ) 1323 if fframe == lframe: 1324 ## track is only one frame, don't flag as extrusion 1325 continue 1326 ## event extrusion 1327 posxy = self.epicure.tracking.get_position( track_id, lframe ) 1328 if posxy is not None: 1329 pos = [ lframe, posxy[0], posxy[1] ] 1330 if self.epicure.verbose > 2: 1331 print("Add extrusion: "+str(track_id)+" at frame "+str(lframe) ) 1332 self.add_event( pos, track_id, "extrusion", symb="diamond", color="red", refresh=False ) 1333 continue 1334 if not self.get_disparition.isChecked(): 1335 continue 1336 1337 ## event disparition 1338 posxy = self.epicure.tracking.get_position( track_id, lframe ) 1339 if posxy is not None: 1340 pos = [ lframe, posxy[0], posxy[1] ] 1341 if self.epicure.verbose > 2: 1342 print("Disappearing track: "+str(track_id)+" at frame "+str(lframe) ) 1343 self.add_event(pos, track_id, "track-disparition", refresh=False) 1344 if progress_bar is not None: 1345 sub_bar.close() 1346 self.refresh_events() 1347 if self.epicure.verbose > 1: 1348 ut.show_duration( start_time, "Tracks disparition took " ) 1349 1350 1351 def track_gaps( self, tracks, progress_bar ): 1352 """ Check if some track have temporal gaps above a given threshold of frames """ 1353 start_time = time.time() 1354 ## Track disappears in the movie, not last frame 1355 ctracks = tracks 1356 min_gaps = int(self.min_gaps.text()) 1357 if progress_bar is not None: 1358 sub_bar = progress( total = len( ctracks ), desc="Check gaps in tracks", nest_under = progress_bar ) 1359 gaped = self.epicure.tracking.check_gap( ctracks, verbose=0 ) 1360 if len( gaped ) > 0: 1361 for i, track_id in enumerate( gaped ): 1362 if progress_bar is not None: 1363 sub_bar.update( i ) 1364 gap_frames = self.epicure.tracking.gap_frames( track_id ) 1365 if len( gap_frames ) > 0: 1366 gaps = ut.get_consecutives( gap_frames ) 1367 if self.epicure.verbose > 1: 1368 print("Found gaps in track "+str(track_id)+" : "+str(gaps) ) 1369 for gapy in gaps: 1370 if (gapy[1]-gapy[0]+1) >= min_gaps: 1371 ## flag gap as it's long enough 1372 poszxy = self.epicure.tracking.get_middle_position( track_id, gapy[0]-1, gapy[1]+1 ) 1373 if poszxy is not None: 1374 if self.epicure.verbose > 2: 1375 print("Gap in track: "+str(track_id)+" at frame "+str(poszxy[0]) ) 1376 self.add_event(poszxy, track_id, "track-gap", refresh=False) 1377 if progress_bar is not None: 1378 sub_bar.close() 1379 self.refresh_events() 1380 if self.epicure.verbose > 1: 1381 ut.show_duration( start_time, "Tracks gaps took " ) 1382 1383 def track_21(self): 1384 """ Look for event track: 2->1 """ 1385 if self.epicure.tracking.tracklayer is None: 1386 ut.show_error("No tracking done yet!") 1387 return 1388 1389 graph = self.epicure.tracking.graph 1390 if graph is not None: 1391 for child, parent in graph.items(): 1392 ## 2->1, merge, event 1393 if isinstance(parent, list) and len(parent) == 2: 1394 onetwoone = False 1395 ## was it only one before ? 1396 if (parent[0] in graph.keys()) and (parent[1] in graph.keys()): 1397 if graph[parent[0]][0] == graph[parent[1]][0]: 1398 pos = self.epicure.tracking.get_mean_position([parent[0], parent[1]]) 1399 if pos is not None: 1400 if self.epicure.verbose > 1: 1401 print("event 1->2->1 track: "+str(graph[parent[0]][0])+"-"+str(parent)+"-"+str(child)+" frame "+str(pos[0]) ) 1402 self.add_event(pos, parent[0], "track-1-2-*", refresh=False) 1403 onetwoone = True 1404 1405 if not onetwoone: 1406 pos = self.epicure.tracking.get_mean_position(child, only_first=True) 1407 if pos is not None: 1408 if self.epicure.verbose > 2: 1409 print("event 2->1 track: "+str(parent)+"-"+str(child)+" frame "+str(int(pos[0])) ) 1410 self.add_event(pos, parent[0], "track-2->1", refresh=False) 1411 else: 1412 if self.epicure.verbose > 1: 1413 print("Something weird, "+str(child)+" mean position") 1414 1415 self.refresh_events() 1416 1417 def get_outside_cells( self ): 1418 """ Get list of cells on tissu boundaries and/or on border of the movie """ 1419 self.boundary_cells = dict() 1420 self.border_cells = dict() 1421 check_border = self.ignore_borders.isChecked() 1422 check_bound = self.ignore_boundaries.isChecked() 1423 def get_cells( img ): 1424 """ For parallel processing, task of one thread (one frame) """ 1425 bounds, borders = None, None 1426 if check_bound: 1427 bounds = ut.get_boundary_cells( img ) 1428 if check_border: 1429 borders = ut.get_border_cells( img ) 1430 return (bounds, borders) 1431 1432 if self.epicure.process_parallel: 1433 # Process in parallel, putting all in temp list and then filling the local dict 1434 cell_list = Parallel(n_jobs=self.epicure.nparallel)( 1435 delayed(get_cells)(frame) for frame in self.epicure.seg 1436 ) 1437 for tframe in range(self.epicure.nframes): 1438 if check_bound: 1439 self.boundary_cells[tframe] = cell_list[tframe][0] 1440 if check_border: 1441 self.border_cells[tframe] = cell_list[tframe][1] 1442 else: 1443 ## simple sequential processing 1444 for tframe in range(self.epicure.nframes): 1445 img = self.epicure.seg[tframe] 1446 if check_bound: 1447 self.boundary_cells[tframe] = ut.get_boundary_cells( img ) 1448 if check_border: 1449 self.border_cells[tframe] = ut.get_border_cells( img ) 1450 1451 def get_boundaries_cells(self, pbar=None): 1452 """ Return list of cells that are at the tissu boundaries (touching background) """ 1453 self.boundary_cells = dict() 1454 for tframe in range(self.epicure.nframes): 1455 if pbar is not None: 1456 pbar.update( tframe) 1457 self.boundary_cells[tframe] = ut.get_boundary_cells( self.epicure.seg[tframe] ) 1458 1459 def get_border_cells(self, pbar=None): 1460 """ Return list of cells that are at the border of the movie """ 1461 self.border_cells = dict() 1462 for tframe in range(self.epicure.nframes): 1463 if pbar is not None: 1464 pbar.update( tframe) 1465 img = self.epicure.seg[tframe] 1466 self.border_cells[tframe] = ut.get_border_cells(img) 1467 1468 def get_divisions( self ): 1469 """ Get and add divisions from the tracking graph """ 1470 self.reset_event_type( "division", frame=None ) 1471 graph = self.epicure.tracking.graph 1472 divisions = {} 1473 ## Go through the graph and fill all division by parents 1474 if graph is not None: 1475 for child, parent in graph.items(): 1476 ## 1 parent, potential division 1477 if (isinstance(parent, int)) or (len(parent) == 1): 1478 if isinstance( parent, list ): 1479 par = parent[0] 1480 else: 1481 par = parent 1482 if par not in divisions: 1483 divisions[par] = [child] 1484 else: 1485 divisions[par].append(child) 1486 1487 ## Add all the divisions in the event list 1488 for parent, childs in divisions.items(): 1489 indexes = self.epicure.tracking.get_track_indexes(childs) 1490 if len(indexes) <= 0: 1491 ## something wrong in the graph or in the tracks, ignore for now 1492 continue 1493 ## get the average first position of the childs just after division 1494 pos = self.epicure.tracking.mean_position(indexes, only_first=True) 1495 self.add_event(pos, parent, "division", symb="o", color="#0055ffff", force=True, refresh=False) 1496 ## Update display to show/hide the divisions 1497 self.show_hide_divisions() 1498 self.refresh_events() 1499 1500 def show_hide_events( self, i=None, eclass=None ): 1501 """ Update which type of events to show or hide """ 1502 if i is None: 1503 ## update all events display 1504 tmp_size = int(self.event_size.value()) 1505 self.events.size = tmp_size 1506 hide_events = [] 1507 for i, eclass in enumerate( self.event_class ): 1508 if not self.show_class[i].isChecked(): 1509 hide_events.append( eclass ) 1510 self.show_subset_event( hide_events, True ) 1511 else: 1512 ## update only the triggered one 1513 self.show_subset_event( eclass, self.show_class[i].isChecked() ) 1514 1515 def show_hide_divisions( self ): 1516 """ Show or hide division events """ 1517 self.show_subset_event( "division", self.show_class[0].isChecked() ) 1518 1519 def show_hide_suspects( self ): 1520 """ Show or hide suspect events """ 1521 self.show_subset_event( "suspect", self.show_class[2].isChecked() ) 1522 1523 def add_extrusion( self, label, frame ): 1524 """ Mark given label at specified frame as an extrusion """ 1525 pos = self.epicure.tracking.get_full_position( label, frame ) 1526 self.events.selected_data = {} 1527 if self.show_class[1].isChecked(): 1528 self.events.current_size = int(self.event_size.value()) 1529 else: 1530 self.events.current_size = 0.1 1531 self.add_event( pos, label, "extrusion", symb="diamond", color="red", force=True ) 1532 self.events.selected_data = {} 1533 self.events.current_size = int(self.event_size.value()) 1534 self.update_nevents_display() 1535 1536 def add_division_event( self, labela, labelb, parent, frame ): 1537 """ Add a division event given the two daughter labels, the parent one and frame of division """ 1538 indexes = self.epicure.tracking.get_index( [labela, labelb], frame ) 1539 indexes = indexes.flatten() 1540 pos = self.epicure.tracking.mean_position( indexes ) 1541 self.events.selected_data = {} 1542 if self.show_class[0].isChecked(): 1543 self.events.current_size = int(self.event_size.value()) 1544 else: 1545 self.events.current_size = 0.1 1546 self.add_event( pos, parent, "division", symb="o", color="#0055ffff", force=True ) 1547 self.events.selected_data = {} 1548 self.events.current_size = int(self.event_size.value()) 1549 ## check if there are suspect events to remove, cleared by the division 1550 if parent is not None: 1551 ## check eventual parent event 1552 num, sid = self.find_event( pos[0]-1, parent ) 1553 if num is not None: 1554 if self.is_end_event( sid ): 1555 ## the parent event correspond to a potential end of track, remove it 1556 ind = self.index_from_id( sid ) 1557 self.exonerate_one( ind, remove_division=False ) 1558 if self.epicure.verbose > 0: 1559 print( "Removed suspect event of parent cell "+str(parent)+" cleared by the division flag" ) 1560 ## check each child suspect if cleared by the new division 1561 for child in [labela, labelb]: 1562 num, sid = self.find_event( pos[0], child ) 1563 if num is not None: 1564 if self.is_begin_event( sid ): 1565 ## the child event correspond to a potential begin of track, remove it 1566 ind = self.index_from_id( sid ) 1567 self.exonerate_one( ind, remove_division=False ) 1568 if self.epicure.verbose > 0: 1569 print( "Removed suspect event of daughter cell "+str(child)+" cleared by the division flag" ) 1570 self.update_nevents_display() 1571 1572 def get_event_class( self, ind ): 1573 """ Return the class of event of index ind """ 1574 if self.is_division( ind ): 1575 return 0 1576 if self.is_extrusion( ind ): 1577 return 1 1578 return 2 1579 1580 def is_extrusion( self, ind ): 1581 """ Return if the event of current index is a division """ 1582 return ("extrusion" in self.event_types) and (self.id_from_index(ind) in self.event_types["extrusion"]) 1583 1584 1585 def is_division( self, ind ): 1586 """ Return if the event of current index is a division """ 1587 return ("division" in self.event_types) and (self.id_from_index(ind) in self.event_types["division"]) 1588 1589 def is_suspect( self, ind ): 1590 """ Return if the event of current index is a suspect event """ 1591 return not self.is_division( ind ) 1592 1593 def is_begin_event( self, sid ): 1594 """ Return True if the event has a type corresponding to begin of a track (too small or appearing) """ 1595 beg_events = ["track-apparition", "track-length"] 1596 for event in beg_events: 1597 if event in self.event_types: 1598 if sid in self.event_types[event]: 1599 return True 1600 return False 1601 1602 def is_end_event( self, sid ): 1603 """ Return True if the event has a type corresponding to end of a track (too small or disappearing) """ 1604 end_events = ["track-disparition", "track-length"] 1605 for event in end_events: 1606 if event in self.event_types: 1607 if sid in self.event_types[event]: 1608 return True 1609 return False 1610 1611 def track_position_jump( self, track_ids, progress_bar ): 1612 """ Look at jump in the track position """ 1613 factor = float( self.jump_factor.text() ) 1614 if progress_bar is not None: 1615 sub_bar = progress( total = len( track_ids ), desc="Check position jump in tracks", nest_under = progress_bar ) 1616 for i, tid in enumerate(track_ids): 1617 if progress_bar is not None: 1618 sub_bar.update(i) 1619 track_indexes = self.epicure.tracking.get_track_indexes( tid ) 1620 ## track should be long enough to make sense to look for outlier 1621 if len(track_indexes) > 3: 1622 track_velo = self.epicure.tracking.measure_speed( tid ) 1623 jumps = self.find_jump( track_velo, factor=factor, min_value=5 ) 1624 for tind in jumps: 1625 tdata = self.epicure.tracking.get_frame_data( tid, tind ) 1626 if self.epicure.verbose > 1: 1627 print("event track jump: "+str(tdata[0])+" "+" frame "+str(tdata[1]) ) 1628 self.add_event( tdata[1:4], tid, "track-jump", refresh=False ) 1629 if progress_bar is not None: 1630 sub_bar.close() 1631 self.refresh_events() 1632 1633 1634 def track_features(self): 1635 """ Look at outliers in track features """ 1636 track_ids = self.epicure.tracking.get_track_list() 1637 features = [] 1638 featType = {} 1639 if self.check_size.isChecked(): 1640 features = features + ["Area", "Perimeter"] 1641 featType["Area"] = "size" 1642 featType["Perimeter"] = "size" 1643 size_factor = float(self.size_variability.text()) 1644 if self.check_shape.isChecked(): 1645 features = features + ["Eccentricity", "Solidity"] 1646 featType["Eccentricity"] = "shape" 1647 featType["Solidity"] = "shape" 1648 shape_factor = float(self.shape_variability.text()) 1649 for tid in track_ids: 1650 track_indexes = self.epicure.tracking.get_track_indexes( tid ) 1651 ## track should be long enough to make sense to look for outlier 1652 if len(track_indexes) > 3: 1653 track_feats = self.epicure.tracking.measure_features( tid, features ) 1654 for feature, values in track_feats.items(): 1655 if featType[feature] == "size": 1656 factor = size_factor 1657 if featType[feature] == "shape": 1658 factor = shape_factor 1659 outliers = self.find_jump( values, factor=factor ) 1660 for out in outliers: 1661 tdata = self.epicure.tracking.get_frame_data( tid, out ) 1662 if self.epicure.verbose > 1: 1663 print("event track "+feature+": "+str(tdata[0])+" "+" frame "+str(tdata[1]) ) 1664 self.add_event(tdata[1:4], tid, "track_"+featType[feature], refresh=False) 1665 self.refresh_events() 1666 1667 def find_jump( self, tab, factor=1, min_value=None ): 1668 """ Detect brutal jump in the values """ 1669 jumps = [] 1670 tab = np.array(tab) 1671 diff = np.diff( tab, n=2, prepend=tab[0], append=tab[-1] ) 1672 ## get local average 1673 if len(tab) <= 10: 1674 avg = np.mean( tab ) 1675 else: 1676 kernel = np.repeat (1.0/10.0, 10 ) 1677 avg = np.convolve( tab, kernel, mode="same") 1678 ## normalize the difference by the average value 1679 eps = 0.0000001 1680 diff = np.array(diff, dtype=np.float32) 1681 avg = np.array(avg, dtype=np.float32) 1682 diff = abs(diff+eps)/(avg+eps) 1683 ## keep only local max above threshold 1684 local_max = (np.diff( np.sign(np.diff(diff)) )<0).nonzero()[0] + 1 1685 if min_value is None: 1686 jumps = [i for i in local_max if diff[i] > factor] 1687 else: 1688 jumps = [ i for i in local_max if (diff[i] > factor) and (tab[i] > min_value) ] 1689 return jumps 1690 1691 def find_outliers_tuk( self, tab, factor=3, below=True, above=True ): 1692 """ Returns index of outliers from Tukey's like test """ 1693 q1 = np.quantile(tab, 0.2) 1694 q3 = np.quantile(tab, 0.8) 1695 qtuk = factor * (q3-q1) 1696 outliers = [] 1697 if below: 1698 outliers = outliers + (np.where((tab-q1+qtuk)<0)[0]).tolist() 1699 if above: 1700 outliers = outliers + (np.where((tab-q3-qtuk)>0)[0]).tolist() 1701 return outliers 1702 1703 def weirdo_area(self): 1704 """ look at area trajectory for outliers """ 1705 track_df = self.epicure.tracking.track_df 1706 for tid in np.unique(track_df["track_id"]): 1707 rows = track_df[track_df["track_id"]==tid].copy() 1708 if len(rows) >= 3: 1709 rows["smooth"] = rows.area.rolling(self.win_size, min_periods=1).mean() 1710 rows["diff"] = (rows["area"] - rows["smooth"]).abs() 1711 rows["diff"] = rows["diff"].div(rows["smooth"]) 1712 if self.epicure.verbose > 2: 1713 print(rows)
QWidget(parent: QWidget|None = None, flags: Qt.WindowType = Qt.WindowFlags())
28 def __init__(self, napari_viewer, epic): 29 """ 30 Generate the graphical interface for the inspection panel, and initialize the events layer. 31 """ 32 super().__init__() 33 self.viewer = napari_viewer 34 self.epicure = epic 35 self.seglayer = self.viewer.layers["Segmentation"] 36 self.border_cells = None ## list of cells that are on the image border 37 self.boundary_cells = None ## list of cells that are on the boundary (touch the background) 38 self.eventlayer_name = "Events" 39 self.events = None 40 self.win_size = 10 41 self.event_class = self.epicure.event_class 42 43 ## Print the current number of events 44 self.nevents_print = QLabel("") 45 self.update_nevents_display() 46 47 self.create_eventlayer() 48 layout = QVBoxLayout() 49 layout.addWidget( self.nevents_print ) 50 51 ## Reset or update some events 52 update_events_choice = wid.add_button( btn="Reset/Update some events...", btn_func=self.reset_events_choice, descr="Pops up an interface to choose which event(s) to remove or update" ) 53 layout.addWidget( update_events_choice ) 54 layout.addWidget( wid.separation() ) 55 56 57 ## choose events to display 58 show_label = wid.label_line( "Show events:" ) 59 layout.addWidget( show_label ) 60 show_line = wid.hlayout() 61 self.show_class = [] 62 for i, eclass in enumerate(self.event_class) : 63 check = wid.add_check_tolayout( show_line, eclass, True, None, "Show/hide the "+eclass ) 64 check.stateChanged.connect( lambda state, i=i, eclass=eclass: self.show_hide_events(i, eclass) ) 65 self.show_class.append( check ) 66 layout.addLayout( show_line ) 67 68 ## Visualisation options 69 disp_line, self.event_disp, self.displayevent = wid.checkgroup_help( "Display options", False, "Show/hide event display options panel", "event#visualisation", self.epicure.display_colors, "group3" ) 70 self.create_displayeventBlock() 71 layout.addLayout( disp_line ) 72 layout.addWidget(self.displayevent) 73 74 layout.addWidget( wid.separation() ) 75 ## Error suggestions based on cell features 76 outlier_line, self.outlier_vis, self.featOutliers = wid.checkgroup_help( "Outlier options", False, "Show/Hide outlier options panel", "event#frame-based-events", self.epicure.display_colors, "group" ) 77 layout.addLayout( outlier_line ) 78 self.create_outliersBlock() 79 layout.addWidget(self.featOutliers) 80 81 ## Error suggestions based on tracks 82 track_line, self.track_vis, self.eventTrack = wid.checkgroup_help( "Track options", True, "Show/hide track options", "event#track-based-events", self.epicure.display_colors, "group2" ) 83 self.create_tracksBlock() 84 layout.addLayout( track_line ) 85 layout.addWidget(self.eventTrack) 86 87 self.setLayout(layout) 88 self.key_binding()
Generate the graphical interface for the inspection panel, and initialize the events layer.
90 def key_binding(self): 91 """ active key bindings (keyboard and mouse shortcuts) for events options """ 92 sevents = self.epicure.shortcuts["Events"] 93 self.epicure.overtext["events"] = "---- Events editing ---- \n" 94 self.epicure.overtext["events"] += ut.print_shortcuts( sevents ) 95 96 @self.epicure.seglayer.mouse_drag_callbacks.append 97 def handle_event(seglayer, event): 98 if event.type == "mouse_press": 99 ## remove a event 100 if ut.shortcut_click_match( sevents["delete"], event ): 101 ind = ut.getCellValue( self.events, event ) 102 if self.epicure.verbose > 1: 103 print("Removing clicked event, at index "+str(ind)) 104 if ind is None: 105 ## click was not on a event 106 return 107 sid = self.events.properties["id"][ind] 108 if sid is not None: 109 self.exonerate_one(ind, remove_division=True) 110 self.update_nevents_display() 111 else: 112 if self.epicure.verbose > 1: 113 print("event with id "+str(sid)+" not found") 114 self.events.refresh() 115 return 116 117 ## zoom on a event 118 if ut.shortcut_click_match( sevents["zoom"], event ): 119 ind = ut.getCellValue( self.events, event ) 120 if "id" not in self.events.properties.keys(): 121 print("No event under click") 122 return 123 sid = self.events.properties["id"][ind] 124 if self.epicure.verbose > 1: 125 print("Zoom on event with id "+str(sid)+"") 126 self.zoom_on_event( event.position, sid ) 127 return 128 129 @self.epicure.seglayer.bind_key( sevents["next"]["key"], overwrite=True ) 130 def go_next(seglayer): 131 """ Select next suspect event and zoom on it """ 132 num_event = int(self.event_num.value()) 133 nevents = self.nb_events() 134 if num_event < 0: 135 if self.nb_events( only_suspect=True ) == 0: 136 if self.epicure.verbose > 0: 137 print("No more suspect event") 138 return 139 else: 140 self.event_num.setValue(0) 141 else: 142 self.event_num.setValue( (num_event+1)%nevents ) 143 self.skip_nonselected_event( nevents, min(nevents,3000) ) 144 self.go_to_event()
active key bindings (keyboard and mouse shortcuts) for events options
146 def skip_nonselected_event( self, nevents, left ): 147 """ Skip next event if not a selected one (show event is not checked) """ 148 if left < 0: 149 return 0 150 151 index = int(self.event_num.value()) 152 nothing_showed = True 153 for i, curclass in enumerate(self.show_class): 154 if curclass.isChecked(): 155 nothing_showed = False 156 break 157 if nothing_showed: 158 ## nothing is shown, then go through all events 159 self.event_num.setValue( index ) 160 return index 161 162 event_class = self.get_event_class( index ) 163 ## Show only if show event class is selected 164 if self.show_class[ event_class ].isChecked(): 165 self.event_num.setValue( index ) 166 return index 167 ## else go to next event 168 index = (index + 1)%nevents 169 self.event_num.setValue( index ) 170 return self.skip_nonselected_event( nevents, left-1 )
Skip next event if not a selected one (show event is not checked)
173 def create_eventlayer(self): 174 """ Create a point layer that contains the events """ 175 features = {} 176 pts = [] 177 self.events = self.viewer.add_points( np.array(pts), properties=features, face_color="red", size = 10, symbol='x', name=self.eventlayer_name, scale=self.viewer.layers["Segmentation"].scale ) 178 self.event_types = {} 179 self.update_nevents_display() 180 self.epicure.finish_update()
Create a point layer that contains the events
182 def load_events(self, pts, features, event_types): 183 """ Load events data from file and reinitialize layer with it""" 184 ut.remove_layer(self.viewer, self.eventlayer_name) 185 symbols = np.repeat("x", len(pts)) 186 colors = np.repeat("white", len(pts)) 187 self.events = self.viewer.add_points( np.array(pts), properties=features, face_color=colors, size = 10, symbol=symbols, name=self.eventlayer_name, scale=self.viewer.layers["Segmentation"].scale ) 188 self.event_types = event_types 189 190 ## set the display of division events 191 self.events.selected_data = {} 192 self.select_feature_event( "division" ) 193 self.events.current_symbol = "o" 194 self.events.current_face_color = "#0055ffff" 195 self.events.selected_data = {} 196 self.select_feature_event( "extrusion" ) 197 self.events.current_symbol = "diamond" 198 self.events.current_face_color = "red" 199 self.events.refresh() 200 self.update_nevents_display() 201 self.show_hide_events() 202 self.epicure.finish_update()
Load events data from file and reinitialize layer with it
206 def get_event_types( self ): 207 """ Returns the list of possible event types """ 208 return list( self.event_types.keys() )
Returns the list of possible event types
210 def update_nevents_display( self ): 211 """ Update the display of number of event""" 212 text = str(self.nb_events(only_suspect=True))+" suspects | " 213 text += str(self.nb_type("division"))+" divisions | " 214 text += str(self.nb_type("extrusion"))+" extrusions" 215 self.nevents_print.setText( text )
Update the display of number of event
217 def nb_events( self, only_suspect=False ): 218 """ Returns current number of events """ 219 if self.events is None: 220 return 0 221 if self.events.properties is None: 222 return 0 223 if "score" not in self.events.properties: 224 return 0 225 if not only_suspect: 226 return len(self.events.properties["score"]) 227 return ( len(self.events.properties["score"]) - self.nb_type("division") - self.nb_type("extrusion") )
Returns current number of events
229 def get_events_from_type( self, feature ): 230 """ Return the list of events of a given type """ 231 if feature == "suspect": 232 sub_features = self.suspect_subtypes() 233 evts_id = [] 234 for feat in sub_features: 235 evts_id.extend( eid for eid in self.event_types[ feat ] if eid not in evts_id ) 236 return list( evts_id ) 237 if feature in self.event_types: 238 return self.event_types[ feature ] 239 return []
Return the list of events of a given type
241 def nb_type( self, feature ): 242 """ Return nb of event of given type """ 243 if self.events is None: 244 return 0 245 if (self.event_types is None) or (feature not in self.event_types): 246 return 0 247 return len(self.event_types[feature])
Return nb of event of given type
249 def create_displayeventBlock(self): 250 ''' Block interface of displaying event layer options ''' 251 disp_layout = QVBoxLayout() 252 253 ## Color mode 254 colorlay, self.color_choice = wid.list_line( "Color by:", "Choose color to display the events", self.color_events ) 255 self.color_choice.addItem("None") 256 self.color_choice.addItem("score") 257 self.color_choice.addItem("track-2->1") 258 self.color_choice.addItem("track-1-2-*") 259 self.color_choice.addItem("track-length") 260 self.color_choice.addItem("track-gap") 261 self.color_choice.addItem("track-jump") 262 self.color_choice.addItem("division") 263 self.color_choice.addItem("area") 264 self.color_choice.addItem("solidity") 265 self.color_choice.addItem("intensity") 266 self.color_choice.addItem("tubeness") 267 disp_layout.addLayout(colorlay) 268 269 esize = int(self.epicure.reference_size/70+10) 270 msize = 100 271 if esize > 70: 272 msize = 200 273 esize = min( esize, 100 ) 274 sizelay, self.event_size = wid.slider_line( "Point size:", minval=0, maxval=msize, step=1, value=esize, show_value=True, slidefunc=self.display_event_size, descr="Choose the current point size display" ) 275 disp_layout.addLayout(sizelay) 276 277 ### Interface to select a event and zoom on it 278 chooselay, self.event_num = wid.ranged_value_line( label="event n°", minval=0, maxval=1000000, step=1, val=0, descr="Choose current event to display/remove" ) 279 disp_layout.addLayout(chooselay) 280 go_event_btn = wid.add_button( "Go to event", self.go_to_event, "Zoom and display current event" ) 281 disp_layout.addWidget(go_event_btn) 282 clear_event_btn = wid.add_button( "Remove current event", self.clear_event, "Delete current event from the list of events" ) 283 disp_layout.addWidget(clear_event_btn) 284 285 ## all features 286 self.displayevent.setLayout(disp_layout) 287 self.displayevent.setVisible( self.event_disp.isChecked() )
Block interface of displaying event layer options
290 def reset_event_range(self): 291 """ Reset the max num of event """ 292 nsus = len(self.events.data)-1 293 if self.event_num.value() > nsus: 294 self.event_num.setValue(0) 295 self.event_num.setMaximum(nsus)
Reset the max num of event
297 def go_to_event(self): 298 """ Zoom on the currently selected event """ 299 num_event = int(self.event_num.value()) 300 ## if reached the end of possible events 301 if num_event >= self.nb_events(): 302 num_event = 0 303 self.event_num.setValue(0) 304 if num_event < 0: 305 if self.nb_events() == 0: 306 if self.epicure.verbose > 0: 307 print("No more event") 308 return 309 else: 310 self.event_num.setValue(0) 311 num_event = 0 312 pos = self.events.data[num_event] 313 event_id = self.events.properties["id"][num_event] 314 self.zoom_on_event( pos, event_id )
Zoom on the currently selected event
316 def get_event_infos( self, sid ): 317 """ Get the properties of the event of given id """ 318 index = self.index_from_id( sid ) 319 pos = self.events.data[ index ] 320 label = self.events.properties[ "label" ][index] 321 return pos, label
Get the properties of the event of given id
323 def zoom_on_event( self, event_pos, event_id ): 324 """ Zoom on chose event at given position """ 325 evt_lay = self.viewer.layers[self.eventlayer_name] 326 epos = evt_lay.data_to_world(event_pos) 327 #pos = event_pos 328 #print(epos) 329 self.viewer.camera.center = tuple(epos) 330 self.viewer.camera.zoom = 5/self.epicure.epi_metadata["ScaleXY"] 331 ut.set_frame( self.viewer, int(epos[0]) ) 332 crimes = self.get_crimes(event_id) 333 if self.epicure.verbose > 0: 334 print("Suspected because of: "+str(crimes))
Zoom on chose event at given position
336 def color_events(self): 337 """ Color points by the selected mode """ 338 color_mode = self.color_choice.currentText() 339 self.events.refresh_colors() 340 if color_mode == "None": 341 self.events.face_color = "white" 342 elif color_mode == "score": 343 self.set_colors_from_properties("score") 344 else: 345 self.set_colors_from_event_type(color_mode) 346 self.events.refresh_colors()
Color points by the selected mode
348 def suspect_subtypes( self ): 349 """ Return the list of suspect-related event types """ 350 features = list( self.event_types.keys() ) 351 if "division" in features: 352 features.remove( "division" ) 353 if "extrusion" in features: 354 features.remove( "extrusion" ) 355 return features
Return the list of suspect-related event types
357 def show_subset_event( self, feature, show=True ): 358 """ Show/hide a subset (type) of event """ 359 tmp_size = int(self.event_size.value()) 360 size = 0.1 361 if show: 362 size = tmp_size 363 ## select the events of corresponding type 364 self.events.selected_data = {} 365 if not isinstance( feature, list ): 366 features = [feature] 367 else: 368 features = feature 369 if "suspect" in features: 370 ## take all possible features except non-suspect ones (division, extrusion..) 371 features.remove( "suspect" ) 372 features = features + self.suspect_subtypes() 373 374 posids = [] 375 for feat in features: 376 if feat in self.event_types: 377 posid = self.event_types[feat] 378 posids = posids + posid 379 nfound = len(posids) 380 if nfound <= 0: 381 return 382 for ind, cid in enumerate( self.events.properties["id"] ): 383 if cid in posids: 384 self.events._size[ind] = size 385 nfound = nfound - 1 386 ## finished, all updated 387 if nfound == 0: 388 break 389 ## reset selection and default size 390 self.events.selected_data = {} 391 self.events.current_size = tmp_size 392 self.events.refresh()
Show/hide a subset (type) of event
394 def select_feature_event( self, feature ): 395 """ Add all event of given feature to currently selected data """ 396 if feature not in self.event_types: 397 return 398 posid = self.event_types[feature] 399 nfound = len(posid) 400 for ind, cid in enumerate(self.events.properties["id"]): 401 if cid in posid: 402 self.events.selected_data.add( ind ) 403 nfound = nfound - 1 404 ## stop if found all of them 405 if nfound == 0: 406 return
Add all event of given feature to currently selected data
408 def set_colors_from_event_type(self, feature): 409 """ Set colors from given event_type feature (eg area, tracking..) """ 410 if self.event_types.get(feature) is None: 411 self.events.face_color="white" 412 return 413 posid = self.event_types[feature] 414 colors = ["white"]*len(self.events.data) 415 ## change the color of all the positive events for the chosen feature 416 for sid in posid: 417 ind = self.index_from_id(sid) 418 if ind is not None: 419 colors[ind] = (0.8,0.1,0.1) 420 self.events.face_color = colors
Set colors from given event_type feature (eg area, tracking..)
422 def set_colors_from_properties(self, feature): 423 """ Set colors from given propertie (eg score, label) """ 424 ncols = (np.max(self.events.properties[feature])) 425 color_cycle = [] 426 for i in range(ncols): 427 color_cycle.append( (0.25+float(i/ncols*0.75), float(i/ncols*0.85), float(i/ncols*0.75)) ) 428 self.events.face_color_cycle = color_cycle 429 self.events.face_color = feature
Set colors from given propertie (eg score, label)
431 def update_display(self): 432 """ Update the display of the events layer """ 433 self.events.refresh() 434 self.color_events()
Update the display of the events layer
436 def get_current_settings(self): 437 """ Returns current event widget parameters """ 438 disp = {} 439 disp["Point size"] = int(self.event_size.value()) 440 disp["Outliers ON"] = self.outlier_vis.isChecked() 441 disp["Track ON"] = self.track_vis.isChecked() 442 disp["EventDisp ON"] = self.event_disp.isChecked() 443 for i, eclass in enumerate(self.event_class): 444 disp["Show "+eclass] = self.show_class[i].isChecked() 445 disp["Ignore border"] = self.ignore_borders.isChecked() 446 disp["Ignore boundaries"] = self.ignore_boundaries.isChecked() 447 disp["Flag length"] = self.check_length.isChecked() 448 disp["Flag jump"] = self.check_jump.isChecked() 449 disp["length"] = self.min_length.text() 450 disp["Check size"] = self.check_size.isChecked() 451 disp["Check shape"] = self.check_shape.isChecked() 452 disp["Get merging"] = self.get_merge.isChecked() 453 disp["Get apparitions"] = self.get_apparition.isChecked() 454 disp["Get divisions"] = self.get_division.isChecked() 455 disp["Get disparitions"] = self.get_disparition.isChecked() 456 disp["Get extrusions"] = self.get_extrusions.isChecked() 457 disp["Get gaps"] = self.get_gaps.isChecked() 458 disp["threshold disparition"] = self.threshold_disparition.text() 459 disp["Min gap"] = self.min_gaps.text() 460 disp["Min area"] = self.min_area.text() 461 disp["Max area"] = self.max_area.text() 462 disp["Current frame"] = self.feat_onframe.isChecked() 463 return disp
Returns current event widget parameters
465 def apply_settings( self, settings ): 466 """ Set the current state (display, widget) from preferences if any """ 467 for setting, val in settings.items(): 468 if setting == "Outliers ON": 469 self.outlier_vis.setChecked( val ) 470 if setting == "Track ON": 471 self.track_vis.setChecked( val ) 472 if setting =="EventDisp ON": 473 self.event_disp.setChecked( val ) 474 if setting == "Point size": 475 self.event_size.setValue( int(val) ) 476 #self.display_event_size() 477 for i, eclass in enumerate(self.event_class): 478 if setting == "Show "+eclass: 479 self.show_class[i].setChecked( val ) 480 #self.show_hide_events() 481 if setting == "Ignore border": 482 self.ignore_borders.setChecked( val ) 483 if setting == "Ignore boundaries": 484 self.ignore_boundaries.setChecked( val ) 485 if setting == "Flag length": 486 self.check_length.setChecked( val ) 487 if setting == "Flag jump": 488 self.check_jump.setChecked( val ) 489 if setting == "length": 490 self.min_length.setText( val ) 491 if setting == "Check size": 492 self.check_size.setChecked( val ) 493 if setting == "Check shape": 494 self.check_shape.setChecked( val ) 495 if setting == "Get merging": 496 self.get_merge.setChecked( val ) 497 if setting == "Get apparitions": 498 self.get_apparition.setChecked( val ) 499 if setting == "Get divisions": 500 self.get_division.setChecked( val ) 501 if setting == "Get disparitions": 502 self.get_disparition.setChecked( val ) 503 if setting == "Get extrusions": 504 self.get_extrusions.setChecked( val ) 505 if setting == "Get gaps": 506 self.get_gaps.setChecked( val ) 507 if setting == "Threshold disparition": 508 self.threshold_disparition.setText( val ) 509 if setting == "Min gap": 510 self.min_gaps.setText( val ) 511 if setting == "Min area": 512 self.min_area.setText( val ) 513 if setting == "Max area": 514 self.max_area.setText( val ) 515 if setting == "Current frame": 516 self.feat_onframe.setChecked( val )
Set the current state (display, widget) from preferences if any
519 def display_event_size(self): 520 """ Change the size of the point display """ 521 size = int(self.event_size.value()) 522 self.events.size = size 523 self.events.refresh() 524 #### Depend on event type, to update
Change the size of the point display
527 def get_crimes(self, sid): 528 """ For a given event, get its event_type(s) """ 529 crimes = [] 530 for feat in self.event_types.keys(): 531 if sid in self.event_types.get(feat): 532 crimes.append(feat) 533 return crimes
For a given event, get its event_type(s)
535 def add_event_type(self, ind, sid, feature): 536 """ Add 1 to the event_type score for given feature """ 537 #print(self.event_types) 538 if self.event_types.get(feature) is None: 539 self.event_types[feature] = [] 540 self.event_types[feature].append(sid) 541 score = self.events.properties["score"].copy() 542 score[ind] = score[ind] + 1 543 self.events.properties["score"] = score 544 self.events.properties["score"].flags.writeable = True 545 #self.events.properties()
Add 1 to the event_type score for given feature
547 def first_event(self, pos, label, featurename): 548 """ Addition of the first event (initialize all) """ 549 ut.remove_layer(self.viewer, "Events") 550 features = {} 551 sid = self.new_event_id() 552 features["id"] = np.array([sid], dtype="uint16") 553 features["label"] = np.array([label], dtype=self.epicure.dtype) 554 features["score"] = np.array([0], dtype="uint8") 555 pts = [pos] 556 self.events = self.viewer.add_points( np.array(pts), properties=features, face_color="score", size = int( self.event_size.value() ), symbol="x", name="Events", scale=self.viewer.layers["Segmentation"].scale ) 557 props = self.events.properties 558 props["label"].flags.writeable = True 559 props["score"].flags.writeable = True 560 props["id"].flags.writeable = True 561 self.add_event_type(0, sid, featurename) 562 self.events.refresh() 563 self.update_nevents_display()
Addition of the first event (initialize all)
565 def add_event(self, pos, label, reason, symb="x", color="white", force=False, refresh=True): 566 """ Add a event to the list, evented by a feature """ 567 if (not force) and (self.ignore_borders.isChecked()) and (self.border_cells is not None): 568 tframe = int(pos[0]) 569 if label in self.border_cells[tframe]: 570 return 571 572 if (not force) and (self.ignore_boundaries.isChecked()) and (self.boundary_cells is not None): 573 tframe = int(pos[0]) 574 if label in self.boundary_cells[tframe]: 575 return 576 577 ## initialise if necessary 578 if len(self.events.data) <= 0: 579 self.first_event(pos, label, reason) 580 return 581 582 self.events.selected_data = [] 583 584 ## look if already evented, then add the charge 585 num, sid = self.find_event(pos[0], label) 586 if num is not None: 587 ## event already in the list. For same crime ? 588 if self.event_types.get(reason) is not None: 589 if sid not in self.event_types[reason]: 590 self.add_event_type(num, sid, reason) 591 else: 592 self.add_event_type(num, sid, reason) 593 else: 594 ## new event, add to the Point layer 595 ind = len(self.events.data) 596 sid = self.new_event_id() 597 self.events.add(pos) 598 props = self.events.properties 599 props["label"].flags.writeable = True 600 props["score"].flags.writeable = True 601 props["id"].flags.writeable = True 602 props["label"][ind] = label 603 props["id"][ind] = sid 604 props["score"][ind] = 0 605 self.add_event_type(ind, sid, reason) 606 607 self.events.symbol.flags.writeable = True 608 self.events.current_symbol = symb 609 self.events.current_face_color = color 610 if refresh: 611 self.refresh_events()
Add a event to the list, evented by a feature
613 def refresh_events( self ): 614 """ Refresh event view and text """ 615 self.events.refresh() 616 self.update_nevents_display() 617 self.reset_event_range() 618 self.epicure.finish_update()
Refresh event view and text
620 def new_event_id(self): 621 """ Find the first unused id """ 622 sid = 0 623 if self.events.properties.get("id") is None: 624 return 0 625 while sid in self.events.properties["id"]: 626 sid = sid + 1 627 return sid
Find the first unused id
629 def reset_events_choice( self ): 630 """ Interface to choose event(s) to reset/update """ 631 632 class ResetChoice( QWidget ): 633 """ Choices of event(s) and update or reset """ 634 def __init__( self, insp ): 635 super().__init__() 636 self.insp = insp 637 poplayout = wid.vlayout() 638 639 ## Handle division events 640 update_div_btn = wid.add_button( btn="Update divisions from graph", btn_func=self.insp.get_divisions, descr="Update the list of division events from the track graph" ) 641 poplayout.addWidget(update_div_btn) 642 poplayout.addWidget( wid.separation() ) 643 644 ### Reset: delete all events 645 reset_color = self.insp.epicure.get_resetbtn_color() 646 reset_event_btn = wid.add_button( btn="Reset all events", btn_func=self.insp.reset_all_events, descr="Delete all current events", color=reset_color ) 647 poplayout.addWidget( reset_event_btn ) 648 649 ## Reset: specific events 650 reset_line = wid.hlayout() 651 for i, eclass in enumerate( self.insp.event_class ): 652 go_btn = wid.add_button( btn="Reset "+eclass, btn_func=None, descr="Reset "+eclass+" events only", color=reset_color ) 653 go_btn.clicked.connect( lambda i=i, eclass=eclass: self.insp.reset_event_type( eclass, frame=None ) ) 654 reset_line.addWidget( go_btn ) 655 poplayout.addLayout( reset_line ) 656 657 poplayout.addWidget( wid.separation() ) 658 ## Remove events on border 659 bord_lab = wid.label_line( "Remove if on BORDER:") 660 bord_line = wid.hlayout() 661 for i, eclass in enumerate( self.insp.event_class ): 662 if eclass != "suspect": 663 go_btn = wid.add_button( btn=""+eclass, btn_func=None, descr="Remove "+eclass+" events if they are on border" ) 664 go_btn.clicked.connect( lambda i=i, eclass=eclass: self.insp.remove_event_border( eclass ) ) 665 bord_line.addWidget( go_btn ) 666 poplayout.addWidget( bord_lab ) 667 poplayout.addLayout( bord_line ) 668 669 ## Remove events on boundaries 670 bound_lab = wid.label_line( "Remove if on BOUNDARY:") 671 bound_line = wid.hlayout() 672 for i, eclass in enumerate( self.insp.event_class ): 673 if eclass != "suspect": 674 go_btn = wid.add_button( btn=""+eclass, btn_func=None, descr="Remove "+eclass+" events if they are on boundary" ) 675 go_btn.clicked.connect( lambda i=i, eclass=eclass: self.insp.remove_event_boundary( eclass ) ) 676 bound_line.addWidget( go_btn ) 677 poplayout.addWidget( bound_lab ) 678 poplayout.addLayout( bound_line ) 679 poplayout.addWidget( wid.separation() ) 680 681 682 self.setLayout( poplayout ) 683 684 #def close( self ): 685 # """ Close the pop-up window """ 686 # self.hide() 687 rc = ResetChoice( self ) 688 rc.show()
Interface to choose event(s) to reset/update
691 def remove_event_border( self, evt_type ): 692 """ Remove events of given types if they are on border cells """ 693 if self.event_types.get( evt_type ) is None: 694 return 695 696 pbar = ut.start_progress( self.viewer, total=self.epicure.nframes+1, descr="Detecting border cells" ) 697 ## get/update the list of border cells 698 self.get_border_cells() 699 700 ## check all event_type events if they are on border cells 701 idlist = self.event_types[ evt_type ].copy() 702 for sid in idlist: 703 ind = self.index_from_id(sid) 704 if ind is not None: 705 ## get the event cell label and frame 706 lab = self.events.properties["label"][ind] 707 frame = self.events.data[ind][0] 708 if evt_type == "division": 709 frame = frame - 1 710 if frame is not None: 711 if lab in self.border_cells[ frame ]: 712 ## event is on border, remove it 713 self.event_types[ evt_type ].remove( sid ) 714 self.decrease_score( ind ) 715 716 ## update displays 717 ut.close_progress( self.viewer, pbar ) 718 self.events.refresh() 719 self.update_nevents_display()
Remove events of given types if they are on border cells
721 def remove_event_boundary( self, evt_type ): 722 """ Remove events of given types if they are on boundary cells """ 723 if self.event_types.get( evt_type ) is None: 724 return 725 726 pbar = ut.start_progress( self.viewer, total=self.epicure.nframes+1, descr="Detecting boundary cells" ) 727 ## get/update the list of border cells 728 self.get_boundaries_cells( pbar ) 729 730 ## check all event_type events if they are on border cells 731 idlist = self.event_types[ evt_type ].copy() 732 for sid in idlist: 733 ind = self.index_from_id(sid) 734 if ind is not None: 735 ## get the event cell label and frame 736 lab = self.events.properties["label"][ind] 737 frame = self.events.data[ind][0] 738 if evt_type == "division": 739 frame = frame - 1 740 if frame is not None: 741 if lab in self.boundary_cells[ frame ]: 742 ## event is on border, remove it 743 self.event_types[ evt_type ].remove( sid ) 744 self.decrease_score( ind ) 745 746 ## update displays 747 ut.close_progress( self.viewer, pbar ) 748 self.events.refresh() 749 self.update_nevents_display()
Remove events of given types if they are on boundary cells
751 def reset_all_events(self): 752 """ Remove all event_types """ 753 features = {} 754 pts = [] 755 ut.remove_layer(self.viewer, "Events") 756 self.events = self.viewer.add_points( np.array(pts), properties=features, face_color="red", size = 10, symbol='x', name="Events", scale=self.viewer.layers["Segmentation"].scale ) 757 self.event_types = {} 758 self.update_nevents_display() 759 #self.update_nevents_display() 760 self.epicure.finish_update()
Remove all event_types
762 def reset_event_type(self, feature, frame ): 763 """ Remove all event_types of given feature, for current frame or all if frame is None """ 764 if self.event_types.get(feature) is None: 765 return 766 idlist = self.event_types[feature].copy() 767 for sid in idlist: 768 ind = self.index_from_id(sid) 769 if ind is not None: 770 if frame is not None: 771 if int(self.events.data[ind][0]) == frame: 772 self.event_types[feature].remove(sid) 773 self.decrease_score(ind) 774 else: 775 self.event_types[feature].remove(sid) 776 self.decrease_score(ind) 777 self.events.refresh() 778 self.update_nevents_display()
Remove all event_types of given feature, for current frame or all if frame is None
780 def remove_event_types(self, sid): 781 """ Remove all event_types of given event id """ 782 for listval in self.event_types.values(): 783 if sid in listval: 784 listval.remove(sid)
Remove all event_types of given event id
786 def decrease_score(self, ind): 787 """ Decrease by one score of event at index ind. Delete it if reach 0""" 788 score = self.events.properties["score"] 789 score.flags.writeable = True 790 score[ind] = score[ind] - 1 791 if self.events.properties["score"][ind] == 0: 792 self.exonerate_one( ind, remove_division=False ) 793 self.update_nevents_display()
Decrease by one score of event at index ind. Delete it if reach 0
795 def index_from_id(self, sid): 796 """ From event id, find the corresponding index in the properties array """ 797 for ind, cid in enumerate(self.events.properties["id"]): 798 if cid == sid: 799 return ind 800 return None
From event id, find the corresponding index in the properties array
802 def id_from_index( self, ind ): 803 """ From event index, returns it id """ 804 return self.events.properties["id"][ind]
From event index, returns it id
806 def find_event(self, frame, label): 807 """ Find if there is already a event at given frame and label """ 808 events = self.events.data 809 events_lab = self.events.properties["label"] 810 for i, lab in enumerate(events_lab): 811 if lab == label: 812 if events[i][0] == frame: 813 return i, self.events.properties["id"][i] 814 return None, None
Find if there is already a event at given frame and label
816 def init_suggestion(self): 817 """ Initialize the layer that will contains propostion of tracks/segmentations """ 818 suggestion = np.zeros(self.seglayer.data.shape, dtype="uint16") 819 self.suggestion = self.viewer.add_labels(suggestion, blending="additive", name="Suggestion") 820 821 @self.seglayer.mouse_drag_callbacks.append 822 def click(layer, event): 823 if event.type == "mouse_press": 824 if 'Alt' in event.modifiers: 825 if event.button == 1: 826 pos = event.position 827 # alt+left click accept suggestion under the mouse pointer (in all frames) 828 self.accept_suggestion(pos)
Initialize the layer that will contains propostion of tracks/segmentations
830 def accept_suggestion(self, pos): 831 """ Accept the modifications of the label at position pos (all the label) """ 832 seglayer = self.viewer.layers["Segmentation"] 833 label = self.suggestion.data[tuple(map(int, pos))] 834 found = self.suggestion.data==label 835 self.exonerate( found, seglayer ) 836 indices = np.argwhere( found ) 837 ut.setNewLabel( seglayer, indices, label, add_frame=None ) 838 self.suggestion.data[self.suggestion.data==label] = 0 839 self.suggestion.refresh() 840 self.update_nevents_display()
Accept the modifications of the label at position pos (all the label)
842 def remove_one_event( self, event_id ): 843 """ Remove the given event from its id """ 844 if self.events is None: 845 return 846 ind = self.index_from_id(event_id) 847 if ind is not None: 848 self.exonerate_one( ind ) 849 self.update_nevents_display() 850 self.events.refresh()
Remove the given event from its id
852 def exonerate_one(self, ind, remove_division=True): 853 """ Remove one event at index ind """ 854 self.events.selected_data = [ind] 855 sid = self.events.properties["id"][ind] 856 if (remove_division) and ("division" in self.event_types.keys()) and (sid in self.event_types["division"]): 857 self.epicure.tracking.remove_division( self.events.properties["label"][ind] ) 858 self.events.remove_selected() 859 self.remove_event_types(sid)
Remove one event at index ind
861 def clear_event(self): 862 """ Remove the current event """ 863 num_event = int(self.event_num.value()) 864 self.exonerate_one( num_event, remove_division=True ) 865 self.update_nevents_display()
Remove the current event
867 def exonerate_from_event(self, event): 868 """ Remove all events in the corresponding cell of position """ 869 label = ut.getCellValue( self.seglayer, event ) 870 if len(self.events.data) > 0: 871 for ind, lab in enumerate(self.events.properties["label"]): 872 if lab == label: 873 if self.events.data[ind][0] == event.position[0]: 874 self.exonerate_one(ind, remove_division=True) 875 self.update_nevents_display()
Remove all events in the corresponding cell of position
877 def exonerate(self, indices, seglayer): 878 """ Remove events that have been corrected/cleared """ 879 seglabels = np.unique(seglayer.data[indices]) 880 selected = [] 881 if self.events.properties.get("label") is None: 882 return 883 for ind, lab in enumerate(self.events.properties["label"]): 884 if lab in seglabels: 885 ## label to remove from event list 886 selected.append(ind) 887 if len(selected) > 0: 888 self.events.selected_data = selected 889 self.events.remove_selected() 890 self.update_nevents_display()
Remove events that have been corrected/cleared
898 def create_outliersBlock(self): 899 ''' Block interface of functions for error suggestions based on cell features ''' 900 feat_layout = QVBoxLayout() 901 902 self.feat_onframe = wid.add_check( check="Only current frame", checked=True, check_func=None, descr="Search for outliers only in current frame" ) 903 feat_layout.addWidget(self.feat_onframe) 904 905 ## area widget 906 tarea_layout, self.min_area, self.max_area = wid.min_button_max( btn="< Area (pix^2) <", btn_func=self.event_area_threshold, min_val="0", max_val="2000", descr="Look for cell which size is outside the given area range" ) 907 feat_layout.addLayout( tarea_layout ) 908 909 ## solid widget 910 feat_solid_line, self.fsolid_out = wid.button_parameter_line( btn="Solidity outliers", btn_func=self.event_solidity, value="3.0", descr_btn="Search for outliers in solidity value", descr_value="Inter-quartiles range factor to consider outlier" ) 911 feat_layout.addLayout( feat_solid_line ) 912 913 ## intensity widget 914 feat_inten_line, self.fintensity_out = wid.button_parameter_line( btn="Intensity cytoplasm/junction", btn_func=self.event_intensity, value="1.0", descr_btn="Search for outliers in intensity ratio", descr_value="Ratio of intensity above which the cell looks suspect" ) 915 feat_layout.addLayout( feat_inten_line ) 916 917 ## tubeness widget 918 feat_tub_line, self.ftub_out = wid.button_parameter_line( btn="Tubeness cytoplasm/junction", btn_func=self.event_tubeness, value="1.0", descr_btn="Search for outliers in tubeness ratio", descr_value="Ratio of tubeness above which the cell looks suspect" ) 919 feat_layout.addLayout( feat_tub_line ) 920 921 ## all features 922 self.featOutliers.setLayout(feat_layout) 923 self.featOutliers.setVisible( self.outlier_vis.isChecked() )
Block interface of functions for error suggestions based on cell features
925 def event_feature(self, featname, funcname ): 926 """ event in one frame or all frames the given feature """ 927 onframe = self.feat_onframe.isChecked() 928 if onframe: 929 tframe = ut.current_frame(self.viewer) 930 self.reset_event_type(featname, tframe) 931 funcname(tframe) 932 else: 933 self.reset_event_type(featname, None) 934 for frame in range(self.seglayer.data.shape[0]): 935 funcname(frame) 936 self.update_display() 937 ut.set_active_layer( self.viewer, "Segmentation" )
event in one frame or all frames the given feature
939 def inspect_outliers(self, tab, props, tuk, frame, feature): 940 q1 = np.quantile(tab, 0.25) 941 q3 = np.quantile(tab, 0.75) 942 qtuk = tuk * (q3-q1) 943 for sign in [1, -1]: 944 #thresh = np.mean(tab) + sign * np.std(tab)*tuk 945 if sign > 0: 946 thresh = q3 + qtuk 947 else: 948 thresh = q1 - qtuk 949 for i in np.where((tab-thresh)*sign>0)[0]: 950 position = ut.prop_to_pos( props[i], frame ) 951 self.add_event( position, props[i].label, feature )
953 def event_area_threshold(self): 954 """ Look for cell's area below/above a threshold """ 955 self.event_feature( "area", self.event_area_threshold_oneframe )
Look for cell's area below/above a threshold
957 def event_area_threshold_oneframe( self, tframe ): 958 """ Check if area is above/below given threshold """ 959 minarea = int(self.min_area.text()) 960 maxarea = int(self.max_area.text()) 961 frame_props = self.epicure.get_frame_features( tframe ) 962 for prop in frame_props: 963 if (prop.area < minarea) or (prop.area > maxarea): 964 position = ut.prop_to_pos( prop, tframe ) 965 self.add_event( position, prop.label, "area" )
Check if area is above/below given threshold
968 def event_area(self, state): 969 """ Look for outliers in term of cell area """ 970 self.event_feature( "area", self.event_area_oneframe )
Look for outliers in term of cell area
972 def event_area_oneframe(self, frame): 973 seglayer = self.seglayer.data[frame] 974 props = regionprops(seglayer) 975 ncell = len(props) 976 areas = np.zeros((ncell,1), dtype="float") 977 for i, prop in enumerate(props): 978 if prop.label > 0: 979 areas[i] = prop.area 980 tuk = self.farea_out.value() 981 self.inspect_outliers(areas, props, tuk, frame, "area")
983 def event_solidity(self, state): 984 """ Look for outliers in term ofz cell solidity """ 985 self.event_feature( "solidity", self.event_solidity_oneframe )
Look for outliers in term ofz cell solidity
987 def event_solidity_oneframe(self, frame): 988 seglayer = self.seglayer.data[frame] 989 props = regionprops(seglayer) 990 ncell = len(props) 991 sols = np.zeros((ncell,1), dtype="float") 992 for i, prop in enumerate(props): 993 if prop.label > 0: 994 sols[i] = prop.solidity 995 tuk = float(self.fsolid_out.text()) 996 self.inspect_outliers(sols, props, tuk, frame, "solidity")
998 def event_intensity(self, state): 999 """ Look for abnormal intensity inside/periph ratio """ 1000 self.event_feature( "intensity", self.event_intensity_oneframe )
Look for abnormal intensity inside/periph ratio
1010 def test_intensity(self, inten, prop, frame): 1011 """ Test if intensity inside is much smaller than at periphery """ 1012 bbox = prop.bbox 1013 intbb = inten[bbox[0]:bbox[2], bbox[1]:bbox[3]] 1014 footprint = disk(radius=self.epicure.thickness) 1015 inside = binary_erosion(prop.image, footprint) 1016 ininten = np.mean(intbb*inside) 1017 dil_img = binary_dilation(prop.image, footprint) 1018 periph = dil_img^inside 1019 periphint = np.mean(intbb*periph) 1020 if (periphint<=0) or (ininten/periphint > float(self.fintensity_out.text())): 1021 position = ( frame, int(prop.centroid[0]), int(prop.centroid[1]) ) 1022 self.add_event( position, prop.label, "intensity" )
Test if intensity inside is much smaller than at periphery
1024 def event_tubeness(self, state): 1025 """ Look for abnormal tubeness inside vs periph """ 1026 self.event_feature( "tubeness", self.event_tubeness_oneframe )
Look for abnormal tubeness inside vs periph
1028 def event_tubeness_oneframe(self, frame): 1029 seglayer = self.seglayer.data[frame] 1030 mov = self.viewer.layers["Movie"].data[frame] 1031 sated = np.copy(mov) 1032 sated = filters.sato(sated, black_ridges=False) 1033 props = regionprops(seglayer) 1034 for i, prop in enumerate(props): 1035 if prop.label > 0: 1036 self.test_tubeness( sated, prop, frame )
1038 def test_tubeness(self, sated, prop, frame): 1039 """ Test if tubeness inside is much smaller than tubeness on periph """ 1040 bbox = prop.bbox 1041 satbb = sated[bbox[0]:bbox[2], bbox[1]:bbox[3]] 1042 footprint = disk(radius=self.epicure.thickness) 1043 inside = binary_erosion(prop.image, footprint) 1044 intub = np.mean(satbb*inside) 1045 periph = prop.image^inside 1046 periphtub = np.mean(satbb*periph) 1047 if periphtub <= 0: 1048 position = ( frame, int(prop.centroid[0]), int(prop.centroid[1]) ) 1049 self.add_event( position, prop.label, "tubeness" ) 1050 else: 1051 if intub/periphtub > float(self.ftub_out.text()): 1052 position = ( frame, int(prop.centroid[0]), int(prop.centroid[1]) ) 1053 self.add_event( position, prop.label, "tubeness" )
Test if tubeness inside is much smaller than tubeness on periph
1061 def create_tracksBlock(self): 1062 ''' Block interface of functions for error suggestions based on tracks ''' 1063 track_layout = QVBoxLayout() 1064 1065 hign = wid.hlayout() 1066 ignore_label = wid.label_line( "Ignore cells on:") 1067 hign.addWidget( ignore_label ) 1068 vign = wid.vlayout() 1069 self.ignore_borders = wid.add_check( "border (of image)", False, None, "When adding suspect, don't add it if the cell is touching the border of the image" ) 1070 vign.addWidget(self.ignore_borders) 1071 1072 self.ignore_boundaries = wid.add_check( "tissue boundaries", False, None, "When adding suspect, don't add it if the cell is on the tissu boundaries (no neighbor in one side)" ) 1073 vign.addWidget(self.ignore_boundaries) 1074 hign.addLayout( vign ) 1075 track_layout.addLayout( hign ) 1076 1077 ## Look for merging tracks 1078 self.get_merge = wid.add_check( "Flag track merging", True, None, "Add a suspect if two track merge in one" ) 1079 track_layout.addWidget(self.get_merge) 1080 1081 ## Look for sudden appearance of tracks 1082 self.get_apparition = wid.add_check( "Flag track apparition", True, None, "Add a suspect if a track appears in the middle of the movie (not on border)" ) 1083 track_layout.addWidget(self.get_apparition) 1084 1085 self.get_division = wid.add_check( "Get divisions", False, None, "Add a division if two touching track appears while a potential parent track disappear" ) 1086 track_layout.addWidget(self.get_division) 1087 1088 ## Look for sudden disappearance of tracks 1089 dsp_layout = wid.hlayout() 1090 self.get_disparition = wid.add_check( check="Flag track disparition", checked=True, check_func=None, descr="Add a suspect if a track disappears (not last frame, not border)" ) 1091 disp_line, self.threshold_disparition = wid.value_line( label="cell area threshold", default_value="200", descr="Flag cell if cell area is above threshold" ) 1092 self.get_extrusions = wid.add_check( "Get extrusions", True, None, "Add extrusions events when a track is disappearing normally (below cell area threshold)" ) 1093 vlay = wid.vlayout() 1094 vlay.addWidget( self.get_disparition ) 1095 vlay.addWidget( self.get_extrusions ) 1096 dsp_layout.addLayout( vlay ) 1097 dsp_layout.addLayout( disp_line ) 1098 track_layout.addLayout( dsp_layout ) 1099 1100 ## Look for temporal gaps in tracks 1101 gap_line, self.get_gaps, self.min_gaps = wid.check_value( check="Flag track gaps", checkfunc=None, checked=True, value="1", descr="Add a suspect if a track has gaps longer than threshold (in nb of frames)", label="if gap above" ) 1102 track_layout.addLayout( gap_line ) 1103 1104 ## track length event_types 1105 ilengthlay, self.check_length, self.min_length = wid.check_value( check="Flag tracks smaller than", checkfunc=None, checked=True, value="1", descr="Add a suspect event for each track smaller than chosen value (in number of frames)" ) 1106 track_layout.addLayout(ilengthlay) 1107 1108 ## track sudden jump in position 1109 ijumplay, self.check_jump, self.jump_factor = wid.check_value( check="Flag jump in track position", checkfunc=None, checked=True, value="3.0", descr="Add a suspect event for when the position of cell centroid moves suddenly a lot compared to the rest of the track" ) 1110 track_layout.addLayout(ijumplay) 1111 1112 ## Variability in feature event_type 1113 sizevar_line, self.check_size, self.size_variability = wid.check_value( check="Size variation", checkfunc=None, checked=False, value="3", descr="Add a suspect if the size of the cell varies suddenly in the track" ) 1114 track_layout.addLayout( sizevar_line ) 1115 shapevar_line, self.check_shape, self.shape_variability = wid.check_value( check="Shape variation", checkfunc=None, checked=False, value="3.0", descr="Add a suspect if the shape of the cell varies suddenly in the track" ) 1116 track_layout.addLayout( shapevar_line ) 1117 1118 ## merge/split combinaisons 1119 track_btn = wid.add_button( btn="Inspect track", btn_func=self.inspect_tracks, descr="Start track analysis to look for suspects based on selected features" ) 1120 track_layout.addWidget(track_btn) 1121 1122 ## all features 1123 self.eventTrack.setLayout(track_layout) 1124 self.eventTrack.setVisible( self.track_vis.isChecked() )
Block interface of functions for error suggestions based on tracks
1126 def reset_tracking_event(self): 1127 """ Remove events from tracking """ 1128 self.reset_event_type("track-1-2-*", None) 1129 self.reset_event_type("track-2->1", None) 1130 self.reset_event_type("track-length", None) 1131 self.reset_event_type("track-jump", None) 1132 self.reset_event_type("track-size", None) 1133 self.reset_event_type("track-shape", None) 1134 self.reset_event_type("track-apparition", None) 1135 self.reset_event_type("track-disparition", None) 1136 self.reset_event_type("track-gap", None) 1137 if self.get_extrusions.isChecked(): 1138 self.reset_event_type("extrusion", None) 1139 self.reset_event_range()
Remove events from tracking
1141 def track_length(self): 1142 """ Find all cells that are only in one frame """ 1143 max_len = int(self.min_length.text()) 1144 labels, lengths, positions = self.epicure.tracking.get_small_tracks( max_len ) 1145 ## remove track from first and last frame 1146 first_tracks = self.epicure.tracking.get_tracks_on_frame( 0 ) 1147 last_tracks = self.epicure.tracking.get_tracks_on_frame( self.epicure.nframes-1 ) 1148 for label, nframe, pos in zip(labels, lengths, positions): 1149 if label in first_tracks or label in last_tracks: 1150 ## present in the first or last track, don't check it 1151 continue 1152 if self.epicure.verbose > 2: 1153 print("event track length "+str(nframe)+": "+str(label)+" frame "+str(pos[0]) ) 1154 self.add_event(pos, label, "track-length", refresh=False) 1155 self.refresh_events()
Find all cells that are only in one frame
1157 def inspect_tracks( self, subprogress=True ): 1158 """ Look for suspicious tracks """ 1159 ut.set_visibility( self.viewer, "Events", True ) 1160 progress_bar = ut.start_progress( self.viewer, total=10 ) 1161 if subprogress: 1162 ## show subprogress bars in sub functions (doesn't work on notebook without interface) 1163 pb = progress_bar 1164 else: 1165 pb= None 1166 progress_bar.update(0) 1167 self.reset_tracking_event() 1168 progress_bar.update(1) 1169 if self.ignore_borders.isChecked() or self.ignore_boundaries.isChecked(): 1170 progress_bar.set_description("Identifying border and/or boundaries cells") 1171 self.get_outside_cells() 1172 progress_bar.update(2) 1173 tracks = self.epicure.tracking.get_track_list() 1174 if self.check_length.isChecked(): 1175 progress_bar.set_description("Identifying too small tracks") 1176 self.track_length() 1177 progress_bar.update(3) 1178 if self.get_merge.isChecked(): 1179 progress_bar.set_description("Inspect tracks 2->1") 1180 self.track_21() 1181 progress_bar.update(4) 1182 if (self.check_size.isChecked()) or self.check_shape.isChecked(): 1183 progress_bar.set_description("Inspect track features") 1184 self.track_features() 1185 progress_bar.update(5) 1186 if self.get_apparition.isChecked() or self.get_division.isChecked(): 1187 progress_bar.set_description("Check new track apparition and/or division") 1188 self.track_apparition( tracks ) 1189 progress_bar.update(6) 1190 if self.get_disparition.isChecked() or self.get_extrusions.isChecked(): 1191 progress_bar.set_description("Check track disparition and/or extrusion") 1192 self.track_disparition( tracks, pb ) 1193 progress_bar.update(7) 1194 if self.get_gaps.isChecked(): 1195 progress_bar.set_description("Check temporal gaps in tracks") 1196 self.track_gaps( tracks, pb ) 1197 progress_bar.update(8) 1198 if self.check_jump.isChecked(): 1199 progress_bar.set_description("Check position jump in tracks") 1200 self.track_position_jump( tracks, pb ) 1201 progress_bar.update(9) 1202 ut.close_progress( self.viewer, progress_bar ) 1203 ut.set_active_layer( self.viewer, "Segmentation" )
Look for suspicious tracks
1205 def track_apparition( self, tracks ): 1206 """ Check if some track appears suddenly (in the middle of the movie and not by division) """ 1207 start_time = time.time() 1208 ## remove track on first frame 1209 ctracks = list( set(tracks) - set( self.epicure.tracking.get_tracks_on_frame( 0 ) ) ) 1210 graph = self.epicure.tracking.graph 1211 do_divisions = self.get_division.isChecked() 1212 do_apparition = self.get_apparition.isChecked() 1213 apparitions = {} 1214 for i, track_id in enumerate( ctracks) : 1215 fframe = self.epicure.tracking.get_first_frame( track_id ) 1216 ## If on the border, ignore 1217 #outside = self.epicure.cell_on_border( track_id, fframe ) 1218 #if outside: 1219 # continue 1220 ## Not on border, check if potential division 1221 if (graph is not None) and (track_id in graph.keys()): 1222 continue 1223 ## event apparition 1224 if (not do_divisions) and do_apparition: 1225 self.add_apparition( fframe, track_id ) 1226 else: 1227 if fframe not in apparitions: 1228 apparitions[fframe] = [track_id] 1229 else: 1230 apparitions[fframe].append(track_id) 1231 if do_divisions: 1232 self.apparition_or_division( apparitions, do_apparition ) 1233 self.refresh_events() 1234 if self.epicure.verbose > 1: 1235 ut.show_duration( start_time, "Tracks apparition took " )
Check if some track appears suddenly (in the middle of the movie and not by division)
1237 def add_apparition( self, frame, trackid ): 1238 """ Add a suspect apparition to events """ 1239 posxy = self.epicure.tracking.get_position( trackid, frame ) 1240 if posxy is not None: 1241 pos = [ frame, posxy[0], posxy[1] ] 1242 if self.epicure.verbose > 2: 1243 print("Appearing track: "+str(trackid)+" at frame "+str(frame) ) 1244 self.add_event(pos, trackid, "track-apparition", refresh=False)
Add a suspect apparition to events
1246 def apparition_or_division( self, apevents, do_apparition ): 1247 """ Check if detected events are apparitions or divisions """ 1248 for frame, tracks in apevents.items(): 1249 if len(tracks) == 1: 1250 ## only one event, apparition 1251 if do_apparition: 1252 self.add_apparition( frame, tracks[0] ) 1253 else: 1254 # look for potential neighbors for each apparition at this frame 1255 ind = 0 1256 while ind < len(tracks): 1257 ctrack = tracks[ind] 1258 ## already treated 1259 if ctrack < 0: 1260 ind = ind + 1 1261 continue 1262 dind = ind + 1 1263 found = False 1264 while dind < len(tracks): 1265 ## skip if already done 1266 if tracks[dind] < 0: 1267 dind = dind + 1 1268 continue 1269 ## check if labels are touching at the appearing frame 1270 bbox, merged = ut.getBBox2DMerge( self.epicure.seg[frame], ctrack, tracks[dind] ) 1271 bbox = ut.extendBBox2D( bbox, 1.05, self.epicure.imgshape2D ) 1272 segt_crop = ut.cropBBox2D( self.epicure.seg[frame], bbox ) 1273 touched = ut.checkTouchingLabels( segt_crop, ctrack, tracks[dind] ) 1274 if touched: 1275 ## found neighbor, potential division 1276 found = True 1277 if self.epicure.editing.add_division( ctrack, tracks[dind], frame ): 1278 ## division added successfully 1279 tracks[dind] = -1 ## track done 1280 break 1281 else: 1282 ## failed to add a division, so mark it as apparition 1283 if do_apparition: 1284 self.add_apparition( frame, ctrack ) 1285 break 1286 else: 1287 dind = dind + 1 1288 # no neighbor found, so mark this as an apparition 1289 if (not found) and do_apparition: 1290 if ctrack > 0: 1291 self.add_apparition( frame, ctrack ) 1292 ind = ind + 1 1293 #print(self.epicure.tracking.graph)
Check if detected events are apparitions or divisions
1297 def track_disparition( self, tracks, progress_bar ): 1298 """ Check if some track disappears suddenly (in the middle of the movie and not by division) """ 1299 start_time = ut.start_time() 1300 ## Track disappears in the movie, not last frame 1301 ctracks = list( set(tracks) - set( self.epicure.tracking.get_tracks_on_frame( self.epicure.nframes-1 ) ) ) 1302 threshold_area = float(self.threshold_disparition.text()) 1303 if progress_bar is not None: 1304 sub_bar = progress( total = len( ctracks ), desc="Check non last frame tracks", nest_under = progress_bar ) 1305 for i, track_id in enumerate( ctracks ): 1306 if progress_bar is not None: 1307 sub_bar.update( i ) 1308 lframe = self.epicure.tracking.get_last_frame( track_id ) 1309 ## If on the border, ignore 1310 #outside = self.epicure.cell_on_border( track_id, lframe ) 1311 #if outside: 1312 # continue 1313 ## Not on border, check if potential division 1314 if self.epicure.tracking.is_single_parent( track_id ): 1315 continue 1316 1317 ## check if the cell area is below the threshold, then considered as ok (likely extrusion) 1318 if (threshold_area > 0): 1319 cell_area = self.epicure.cell_area( track_id, lframe ) 1320 if cell_area < threshold_area: 1321 if self.get_extrusions.isChecked(): 1322 fframe = self.epicure.tracking.get_first_frame( track_id ) 1323 if fframe == lframe: 1324 ## track is only one frame, don't flag as extrusion 1325 continue 1326 ## event extrusion 1327 posxy = self.epicure.tracking.get_position( track_id, lframe ) 1328 if posxy is not None: 1329 pos = [ lframe, posxy[0], posxy[1] ] 1330 if self.epicure.verbose > 2: 1331 print("Add extrusion: "+str(track_id)+" at frame "+str(lframe) ) 1332 self.add_event( pos, track_id, "extrusion", symb="diamond", color="red", refresh=False ) 1333 continue 1334 if not self.get_disparition.isChecked(): 1335 continue 1336 1337 ## event disparition 1338 posxy = self.epicure.tracking.get_position( track_id, lframe ) 1339 if posxy is not None: 1340 pos = [ lframe, posxy[0], posxy[1] ] 1341 if self.epicure.verbose > 2: 1342 print("Disappearing track: "+str(track_id)+" at frame "+str(lframe) ) 1343 self.add_event(pos, track_id, "track-disparition", refresh=False) 1344 if progress_bar is not None: 1345 sub_bar.close() 1346 self.refresh_events() 1347 if self.epicure.verbose > 1: 1348 ut.show_duration( start_time, "Tracks disparition took " )
Check if some track disappears suddenly (in the middle of the movie and not by division)
1351 def track_gaps( self, tracks, progress_bar ): 1352 """ Check if some track have temporal gaps above a given threshold of frames """ 1353 start_time = time.time() 1354 ## Track disappears in the movie, not last frame 1355 ctracks = tracks 1356 min_gaps = int(self.min_gaps.text()) 1357 if progress_bar is not None: 1358 sub_bar = progress( total = len( ctracks ), desc="Check gaps in tracks", nest_under = progress_bar ) 1359 gaped = self.epicure.tracking.check_gap( ctracks, verbose=0 ) 1360 if len( gaped ) > 0: 1361 for i, track_id in enumerate( gaped ): 1362 if progress_bar is not None: 1363 sub_bar.update( i ) 1364 gap_frames = self.epicure.tracking.gap_frames( track_id ) 1365 if len( gap_frames ) > 0: 1366 gaps = ut.get_consecutives( gap_frames ) 1367 if self.epicure.verbose > 1: 1368 print("Found gaps in track "+str(track_id)+" : "+str(gaps) ) 1369 for gapy in gaps: 1370 if (gapy[1]-gapy[0]+1) >= min_gaps: 1371 ## flag gap as it's long enough 1372 poszxy = self.epicure.tracking.get_middle_position( track_id, gapy[0]-1, gapy[1]+1 ) 1373 if poszxy is not None: 1374 if self.epicure.verbose > 2: 1375 print("Gap in track: "+str(track_id)+" at frame "+str(poszxy[0]) ) 1376 self.add_event(poszxy, track_id, "track-gap", refresh=False) 1377 if progress_bar is not None: 1378 sub_bar.close() 1379 self.refresh_events() 1380 if self.epicure.verbose > 1: 1381 ut.show_duration( start_time, "Tracks gaps took " )
Check if some track have temporal gaps above a given threshold of frames
1383 def track_21(self): 1384 """ Look for event track: 2->1 """ 1385 if self.epicure.tracking.tracklayer is None: 1386 ut.show_error("No tracking done yet!") 1387 return 1388 1389 graph = self.epicure.tracking.graph 1390 if graph is not None: 1391 for child, parent in graph.items(): 1392 ## 2->1, merge, event 1393 if isinstance(parent, list) and len(parent) == 2: 1394 onetwoone = False 1395 ## was it only one before ? 1396 if (parent[0] in graph.keys()) and (parent[1] in graph.keys()): 1397 if graph[parent[0]][0] == graph[parent[1]][0]: 1398 pos = self.epicure.tracking.get_mean_position([parent[0], parent[1]]) 1399 if pos is not None: 1400 if self.epicure.verbose > 1: 1401 print("event 1->2->1 track: "+str(graph[parent[0]][0])+"-"+str(parent)+"-"+str(child)+" frame "+str(pos[0]) ) 1402 self.add_event(pos, parent[0], "track-1-2-*", refresh=False) 1403 onetwoone = True 1404 1405 if not onetwoone: 1406 pos = self.epicure.tracking.get_mean_position(child, only_first=True) 1407 if pos is not None: 1408 if self.epicure.verbose > 2: 1409 print("event 2->1 track: "+str(parent)+"-"+str(child)+" frame "+str(int(pos[0])) ) 1410 self.add_event(pos, parent[0], "track-2->1", refresh=False) 1411 else: 1412 if self.epicure.verbose > 1: 1413 print("Something weird, "+str(child)+" mean position") 1414 1415 self.refresh_events()
Look for event track: 2->1
1417 def get_outside_cells( self ): 1418 """ Get list of cells on tissu boundaries and/or on border of the movie """ 1419 self.boundary_cells = dict() 1420 self.border_cells = dict() 1421 check_border = self.ignore_borders.isChecked() 1422 check_bound = self.ignore_boundaries.isChecked() 1423 def get_cells( img ): 1424 """ For parallel processing, task of one thread (one frame) """ 1425 bounds, borders = None, None 1426 if check_bound: 1427 bounds = ut.get_boundary_cells( img ) 1428 if check_border: 1429 borders = ut.get_border_cells( img ) 1430 return (bounds, borders) 1431 1432 if self.epicure.process_parallel: 1433 # Process in parallel, putting all in temp list and then filling the local dict 1434 cell_list = Parallel(n_jobs=self.epicure.nparallel)( 1435 delayed(get_cells)(frame) for frame in self.epicure.seg 1436 ) 1437 for tframe in range(self.epicure.nframes): 1438 if check_bound: 1439 self.boundary_cells[tframe] = cell_list[tframe][0] 1440 if check_border: 1441 self.border_cells[tframe] = cell_list[tframe][1] 1442 else: 1443 ## simple sequential processing 1444 for tframe in range(self.epicure.nframes): 1445 img = self.epicure.seg[tframe] 1446 if check_bound: 1447 self.boundary_cells[tframe] = ut.get_boundary_cells( img ) 1448 if check_border: 1449 self.border_cells[tframe] = ut.get_border_cells( img )
Get list of cells on tissu boundaries and/or on border of the movie
1451 def get_boundaries_cells(self, pbar=None): 1452 """ Return list of cells that are at the tissu boundaries (touching background) """ 1453 self.boundary_cells = dict() 1454 for tframe in range(self.epicure.nframes): 1455 if pbar is not None: 1456 pbar.update( tframe) 1457 self.boundary_cells[tframe] = ut.get_boundary_cells( self.epicure.seg[tframe] )
Return list of cells that are at the tissu boundaries (touching background)
1459 def get_border_cells(self, pbar=None): 1460 """ Return list of cells that are at the border of the movie """ 1461 self.border_cells = dict() 1462 for tframe in range(self.epicure.nframes): 1463 if pbar is not None: 1464 pbar.update( tframe) 1465 img = self.epicure.seg[tframe] 1466 self.border_cells[tframe] = ut.get_border_cells(img)
Return list of cells that are at the border of the movie
1468 def get_divisions( self ): 1469 """ Get and add divisions from the tracking graph """ 1470 self.reset_event_type( "division", frame=None ) 1471 graph = self.epicure.tracking.graph 1472 divisions = {} 1473 ## Go through the graph and fill all division by parents 1474 if graph is not None: 1475 for child, parent in graph.items(): 1476 ## 1 parent, potential division 1477 if (isinstance(parent, int)) or (len(parent) == 1): 1478 if isinstance( parent, list ): 1479 par = parent[0] 1480 else: 1481 par = parent 1482 if par not in divisions: 1483 divisions[par] = [child] 1484 else: 1485 divisions[par].append(child) 1486 1487 ## Add all the divisions in the event list 1488 for parent, childs in divisions.items(): 1489 indexes = self.epicure.tracking.get_track_indexes(childs) 1490 if len(indexes) <= 0: 1491 ## something wrong in the graph or in the tracks, ignore for now 1492 continue 1493 ## get the average first position of the childs just after division 1494 pos = self.epicure.tracking.mean_position(indexes, only_first=True) 1495 self.add_event(pos, parent, "division", symb="o", color="#0055ffff", force=True, refresh=False) 1496 ## Update display to show/hide the divisions 1497 self.show_hide_divisions() 1498 self.refresh_events()
Get and add divisions from the tracking graph
1500 def show_hide_events( self, i=None, eclass=None ): 1501 """ Update which type of events to show or hide """ 1502 if i is None: 1503 ## update all events display 1504 tmp_size = int(self.event_size.value()) 1505 self.events.size = tmp_size 1506 hide_events = [] 1507 for i, eclass in enumerate( self.event_class ): 1508 if not self.show_class[i].isChecked(): 1509 hide_events.append( eclass ) 1510 self.show_subset_event( hide_events, True ) 1511 else: 1512 ## update only the triggered one 1513 self.show_subset_event( eclass, self.show_class[i].isChecked() )
Update which type of events to show or hide
1515 def show_hide_divisions( self ): 1516 """ Show or hide division events """ 1517 self.show_subset_event( "division", self.show_class[0].isChecked() )
Show or hide division events
1519 def show_hide_suspects( self ): 1520 """ Show or hide suspect events """ 1521 self.show_subset_event( "suspect", self.show_class[2].isChecked() )
Show or hide suspect events
1523 def add_extrusion( self, label, frame ): 1524 """ Mark given label at specified frame as an extrusion """ 1525 pos = self.epicure.tracking.get_full_position( label, frame ) 1526 self.events.selected_data = {} 1527 if self.show_class[1].isChecked(): 1528 self.events.current_size = int(self.event_size.value()) 1529 else: 1530 self.events.current_size = 0.1 1531 self.add_event( pos, label, "extrusion", symb="diamond", color="red", force=True ) 1532 self.events.selected_data = {} 1533 self.events.current_size = int(self.event_size.value()) 1534 self.update_nevents_display()
Mark given label at specified frame as an extrusion
1536 def add_division_event( self, labela, labelb, parent, frame ): 1537 """ Add a division event given the two daughter labels, the parent one and frame of division """ 1538 indexes = self.epicure.tracking.get_index( [labela, labelb], frame ) 1539 indexes = indexes.flatten() 1540 pos = self.epicure.tracking.mean_position( indexes ) 1541 self.events.selected_data = {} 1542 if self.show_class[0].isChecked(): 1543 self.events.current_size = int(self.event_size.value()) 1544 else: 1545 self.events.current_size = 0.1 1546 self.add_event( pos, parent, "division", symb="o", color="#0055ffff", force=True ) 1547 self.events.selected_data = {} 1548 self.events.current_size = int(self.event_size.value()) 1549 ## check if there are suspect events to remove, cleared by the division 1550 if parent is not None: 1551 ## check eventual parent event 1552 num, sid = self.find_event( pos[0]-1, parent ) 1553 if num is not None: 1554 if self.is_end_event( sid ): 1555 ## the parent event correspond to a potential end of track, remove it 1556 ind = self.index_from_id( sid ) 1557 self.exonerate_one( ind, remove_division=False ) 1558 if self.epicure.verbose > 0: 1559 print( "Removed suspect event of parent cell "+str(parent)+" cleared by the division flag" ) 1560 ## check each child suspect if cleared by the new division 1561 for child in [labela, labelb]: 1562 num, sid = self.find_event( pos[0], child ) 1563 if num is not None: 1564 if self.is_begin_event( sid ): 1565 ## the child event correspond to a potential begin of track, remove it 1566 ind = self.index_from_id( sid ) 1567 self.exonerate_one( ind, remove_division=False ) 1568 if self.epicure.verbose > 0: 1569 print( "Removed suspect event of daughter cell "+str(child)+" cleared by the division flag" ) 1570 self.update_nevents_display()
Add a division event given the two daughter labels, the parent one and frame of division
1572 def get_event_class( self, ind ): 1573 """ Return the class of event of index ind """ 1574 if self.is_division( ind ): 1575 return 0 1576 if self.is_extrusion( ind ): 1577 return 1 1578 return 2
Return the class of event of index ind
1580 def is_extrusion( self, ind ): 1581 """ Return if the event of current index is a division """ 1582 return ("extrusion" in self.event_types) and (self.id_from_index(ind) in self.event_types["extrusion"])
Return if the event of current index is a division
1585 def is_division( self, ind ): 1586 """ Return if the event of current index is a division """ 1587 return ("division" in self.event_types) and (self.id_from_index(ind) in self.event_types["division"])
Return if the event of current index is a division
1589 def is_suspect( self, ind ): 1590 """ Return if the event of current index is a suspect event """ 1591 return not self.is_division( ind )
Return if the event of current index is a suspect event
1593 def is_begin_event( self, sid ): 1594 """ Return True if the event has a type corresponding to begin of a track (too small or appearing) """ 1595 beg_events = ["track-apparition", "track-length"] 1596 for event in beg_events: 1597 if event in self.event_types: 1598 if sid in self.event_types[event]: 1599 return True 1600 return False
Return True if the event has a type corresponding to begin of a track (too small or appearing)
1602 def is_end_event( self, sid ): 1603 """ Return True if the event has a type corresponding to end of a track (too small or disappearing) """ 1604 end_events = ["track-disparition", "track-length"] 1605 for event in end_events: 1606 if event in self.event_types: 1607 if sid in self.event_types[event]: 1608 return True 1609 return False
Return True if the event has a type corresponding to end of a track (too small or disappearing)
1611 def track_position_jump( self, track_ids, progress_bar ): 1612 """ Look at jump in the track position """ 1613 factor = float( self.jump_factor.text() ) 1614 if progress_bar is not None: 1615 sub_bar = progress( total = len( track_ids ), desc="Check position jump in tracks", nest_under = progress_bar ) 1616 for i, tid in enumerate(track_ids): 1617 if progress_bar is not None: 1618 sub_bar.update(i) 1619 track_indexes = self.epicure.tracking.get_track_indexes( tid ) 1620 ## track should be long enough to make sense to look for outlier 1621 if len(track_indexes) > 3: 1622 track_velo = self.epicure.tracking.measure_speed( tid ) 1623 jumps = self.find_jump( track_velo, factor=factor, min_value=5 ) 1624 for tind in jumps: 1625 tdata = self.epicure.tracking.get_frame_data( tid, tind ) 1626 if self.epicure.verbose > 1: 1627 print("event track jump: "+str(tdata[0])+" "+" frame "+str(tdata[1]) ) 1628 self.add_event( tdata[1:4], tid, "track-jump", refresh=False ) 1629 if progress_bar is not None: 1630 sub_bar.close() 1631 self.refresh_events()
Look at jump in the track position
1634 def track_features(self): 1635 """ Look at outliers in track features """ 1636 track_ids = self.epicure.tracking.get_track_list() 1637 features = [] 1638 featType = {} 1639 if self.check_size.isChecked(): 1640 features = features + ["Area", "Perimeter"] 1641 featType["Area"] = "size" 1642 featType["Perimeter"] = "size" 1643 size_factor = float(self.size_variability.text()) 1644 if self.check_shape.isChecked(): 1645 features = features + ["Eccentricity", "Solidity"] 1646 featType["Eccentricity"] = "shape" 1647 featType["Solidity"] = "shape" 1648 shape_factor = float(self.shape_variability.text()) 1649 for tid in track_ids: 1650 track_indexes = self.epicure.tracking.get_track_indexes( tid ) 1651 ## track should be long enough to make sense to look for outlier 1652 if len(track_indexes) > 3: 1653 track_feats = self.epicure.tracking.measure_features( tid, features ) 1654 for feature, values in track_feats.items(): 1655 if featType[feature] == "size": 1656 factor = size_factor 1657 if featType[feature] == "shape": 1658 factor = shape_factor 1659 outliers = self.find_jump( values, factor=factor ) 1660 for out in outliers: 1661 tdata = self.epicure.tracking.get_frame_data( tid, out ) 1662 if self.epicure.verbose > 1: 1663 print("event track "+feature+": "+str(tdata[0])+" "+" frame "+str(tdata[1]) ) 1664 self.add_event(tdata[1:4], tid, "track_"+featType[feature], refresh=False) 1665 self.refresh_events()
Look at outliers in track features
1667 def find_jump( self, tab, factor=1, min_value=None ): 1668 """ Detect brutal jump in the values """ 1669 jumps = [] 1670 tab = np.array(tab) 1671 diff = np.diff( tab, n=2, prepend=tab[0], append=tab[-1] ) 1672 ## get local average 1673 if len(tab) <= 10: 1674 avg = np.mean( tab ) 1675 else: 1676 kernel = np.repeat (1.0/10.0, 10 ) 1677 avg = np.convolve( tab, kernel, mode="same") 1678 ## normalize the difference by the average value 1679 eps = 0.0000001 1680 diff = np.array(diff, dtype=np.float32) 1681 avg = np.array(avg, dtype=np.float32) 1682 diff = abs(diff+eps)/(avg+eps) 1683 ## keep only local max above threshold 1684 local_max = (np.diff( np.sign(np.diff(diff)) )<0).nonzero()[0] + 1 1685 if min_value is None: 1686 jumps = [i for i in local_max if diff[i] > factor] 1687 else: 1688 jumps = [ i for i in local_max if (diff[i] > factor) and (tab[i] > min_value) ] 1689 return jumps
Detect brutal jump in the values
1691 def find_outliers_tuk( self, tab, factor=3, below=True, above=True ): 1692 """ Returns index of outliers from Tukey's like test """ 1693 q1 = np.quantile(tab, 0.2) 1694 q3 = np.quantile(tab, 0.8) 1695 qtuk = factor * (q3-q1) 1696 outliers = [] 1697 if below: 1698 outliers = outliers + (np.where((tab-q1+qtuk)<0)[0]).tolist() 1699 if above: 1700 outliers = outliers + (np.where((tab-q3-qtuk)>0)[0]).tolist() 1701 return outliers
Returns index of outliers from Tukey's like test
1703 def weirdo_area(self): 1704 """ look at area trajectory for outliers """ 1705 track_df = self.epicure.tracking.track_df 1706 for tid in np.unique(track_df["track_id"]): 1707 rows = track_df[track_df["track_id"]==tid].copy() 1708 if len(rows) >= 3: 1709 rows["smooth"] = rows.area.rolling(self.win_size, min_periods=1).mean() 1710 rows["diff"] = (rows["area"] - rows["smooth"]).abs() 1711 rows["diff"] = rows["diff"].div(rows["smooth"]) 1712 if self.epicure.verbose > 2: 1713 print(rows)
look at area trajectory for outliers