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)
class Inspecting(PyQt6.QtWidgets.QWidget):
  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())

Inspecting(napari_viewer, epic)
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.

viewer
epicure
seglayer
border_cells
boundary_cells
eventlayer_name
events
win_size
event_class
nevents_print
show_class
def key_binding(self):
 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

def skip_nonselected_event(self, nevents, left):
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)

def create_eventlayer(self):
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

def load_events(self, pts, features, event_types):
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

def get_event_types(self):
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

def update_nevents_display(self):
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

def nb_events(self, only_suspect=False):
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

def get_events_from_type(self, feature):
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

def nb_type(self, feature):
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

def create_displayeventBlock(self):
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

def reset_event_range(self):
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

def go_to_event(self):
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

def get_event_infos(self, sid):
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

def zoom_on_event(self, event_pos, event_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

def color_events(self):
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

def suspect_subtypes(self):
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

def show_subset_event(self, feature, show=True):
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

def select_feature_event(self, feature):
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

def set_colors_from_event_type(self, feature):
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..)

def set_colors_from_properties(self, feature):
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)

def update_display(self):
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

def get_current_settings(self):
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

def apply_settings(self, settings):
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

def display_event_size(self):
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

def get_crimes(self, sid):
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)

def add_event_type(self, ind, sid, feature):
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

def first_event(self, pos, label, featurename):
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)

def add_event( self, pos, label, reason, symb='x', color='white', force=False, refresh=True):
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

def refresh_events(self):
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

def new_event_id(self):
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

def reset_events_choice(self):
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

def remove_event_border(self, evt_type):
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

def remove_event_boundary(self, evt_type):
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

def reset_all_events(self):
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

def reset_event_type(self, feature, frame):
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

def remove_event_types(self, sid):
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

def decrease_score(self, ind):
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

def index_from_id(self, sid):
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

def id_from_index(self, ind):
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

def find_event(self, frame, label):
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

def init_suggestion(self):
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

def accept_suggestion(self, pos):
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)

def remove_one_event(self, event_id):
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

def exonerate_one(self, ind, remove_division=True):
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

def clear_event(self):
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

def exonerate_from_event(self, 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

def exonerate(self, indices, seglayer):
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

def show_outlierBlock(self):
895    def show_outlierBlock(self):
896        self.featOutliers.setVisible( self.outlier_vis.isChecked() )
def create_outliersBlock(self):
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

def event_feature(self, featname, funcname):
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

def inspect_outliers(self, tab, props, tuk, frame, 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 )
def event_area_threshold(self):
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

def event_area_threshold_oneframe(self, tframe):
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

def event_area(self, state):
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

def event_area_oneframe(self, frame):
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")
def event_solidity(self, state):
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

def event_solidity_oneframe(self, frame):
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")
def event_intensity(self, state):
 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

def event_intensity_oneframe(self, frame):
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 )
def test_intensity(self, inten, prop, frame):
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

def event_tubeness(self, state):
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

def event_tubeness_oneframe(self, frame):
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 )
def test_tubeness(self, 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

def show_tracksBlock(self):
1058    def show_tracksBlock(self):
1059        self.eventTrack.setVisible( self.track_vis.isChecked() )
def create_tracksBlock(self):
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

def reset_tracking_event(self):
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

def track_length(self):
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

def inspect_tracks(self, subprogress=True):
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

def track_apparition(self, 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)

def add_apparition(self, frame, trackid):
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

def apparition_or_division(self, apevents, do_apparition):
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

def track_disparition(self, tracks, progress_bar):
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)

def track_gaps(self, tracks, progress_bar):
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

def track_21(self):
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

def get_outside_cells(self):
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

def get_boundaries_cells(self, pbar=None):
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)

def get_border_cells(self, pbar=None):
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

def get_divisions(self):
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

def show_hide_events(self, i=None, eclass=None):
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

def show_hide_divisions(self):
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

def show_hide_suspects(self):
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

def add_extrusion(self, label, frame):
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

def add_division_event(self, labela, labelb, parent, frame):
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

def get_event_class(self, ind):
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

def is_extrusion(self, 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

def is_division(self, ind):
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

def is_suspect(self, ind):
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

def is_begin_event(self, sid):
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)

def is_end_event(self, sid):
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)

def track_position_jump(self, track_ids, progress_bar):
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

def track_features(self):
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

def find_jump(self, tab, factor=1, min_value=None):
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

def find_outliers_tuk(self, tab, factor=3, below=True, above=True):
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

def weirdo_area(self):
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