epicure.editing

EpiCure Edit interface

Handles the panel Edit of EpiCure interface and user interaction to edit the segmentation. It proposes options like Group to classify cells, Seeds to perform semi-automatic segmentation based on seeds placed manually by the user, sanity check to check that all the epicure data are fine.

   1"""
   2    **EpiCure Edit interface**
   3
   4    Handles the panel `Edit` of EpiCure interface and user interaction to edit the segmentation.
   5    It proposes options like `Group` to classify cells, `Seeds` to perform semi-automatic segmentation based on seeds placed manually by the user, `sanity check` to check that all the epicure data are fine.
   6"""
   7
   8import numpy as np
   9import edt # type: ignore
  10from skimage.segmentation import watershed, clear_border, find_boundaries, random_walker
  11from skimage.measure import label, points_in_poly
  12from skimage.morphology import binary_closing, binary_opening, binary_dilation, binary_erosion, disk
  13from qtpy.QtWidgets import QWidget # type: ignore
  14from scipy.ndimage import binary_fill_holes, distance_transform_edt, generate_binary_structure
  15from scipy.ndimage import label as ndlabel 
  16from napari.layers.labels._labels_utils import sphere_indices # type: ignore
  17from napari.layers.labels._labels_utils import interpolate_coordinates # type: ignore
  18from napari.utils import progress # type: ignore
  19from napari.qt.threading import thread_worker # type: ignore
  20import epicure.Utils as ut
  21import epicure.epiwidgets as wid
  22
  23class Editing( QWidget ):
  24    """ Handle user interaction to edit the segmentation """
  25
  26    def __init__(self, napari_viewer, epic):
  27        """ Initialize the Edit panel interface """
  28        super().__init__()
  29        self.viewer = napari_viewer
  30        self.epicure = epic
  31        self.old_mouse_drag = None
  32        self.tracklayer_name = "Tracks"
  33        self.shapelayer_name = "ROIs"
  34        self.grouplayer_name = "Groups"
  35        self.updated_labels = None   ## keep which labels are being edited
  36        self.seed_active = False ## if place seed option is on
  37
  38        layout = wid.vlayout()
  39        
  40        ## Option to use default napari painting options
  41        #self.napari_painting = wid.add_check( "Default Napari painting tools (no checks)", checked=False, check_func=self.painting_tools, descr="Use the label painting of Napari instead of customized EpiCure ones (will not perform any sanity check)" )
  42        #layout.addWidget( self.napari_painting )
  43
  44        ## Option to remove all border cells
  45        clean_line, self.clean_vis, self.gCleaned = wid.checkgroup_help( name="Cleaning options", checked=False, descr="Show/hide options to clean the segmentation", help_link="Edit#cleaning-options", display_settings=self.epicure.display_colors, groupnb="group" )
  46        layout.addLayout(clean_line)
  47        self.create_cleaningBlock()
  48        layout.addWidget(self.gCleaned)
  49        self.gCleaned.hide()
  50
  51        ## handle grouping cells into categories
  52        group_line, self.group_vis, self.gGroup = wid.checkgroup_help( name="Cell group options", checked=False, descr="Show/hide options to define cell groups", help_link="Edit#group-options", display_settings=self.epicure.display_colors, groupnb="group2"  )
  53        layout.addLayout(group_line)
  54        self.create_groupCellsBlock()
  55        layout.addWidget(self.gGroup)
  56        self.gGroup.hide()
  57        
  58        ## Selection option: crop, remove cells
  59        select_line, self.select_vis, self.gSelect = wid.checkgroup_help( name="ROI options", checked=False, descr="Show/hide options to work on Regions", help_link="Edit#roi-options", display_settings=self.epicure.display_colors, groupnb="group3" )
  60        layout.addLayout(select_line)
  61        self.create_selectBlock()
  62        layout.addWidget(self.gSelect)
  63        self.gSelect.hide()
  64        
  65        ## Put seeds and do watershed from it
  66        seed_line, self.seed_vis, self.gSeed = wid.checkgroup_help( name="Seeds options", checked=False, descr="Show/hide options to segment from seeds", help_link="Edit#seeds-options", display_settings=self.epicure.display_colors, groupnb="group4" )
  67        layout.addLayout(seed_line)
  68        self.create_seedsBlock()
  69        layout.addWidget(self.gSeed)
  70        self.gSeed.hide()
  71        
  72        self.setLayout(layout)
  73        
  74        ## interface done, ready to work 
  75        self.create_shapelayer()
  76        self.modify_cells()
  77        self.key_tracking_binding()
  78        self.add_overlay_message()
  79
  80        ## catch filling/painting operations
  81        self.napari_fill = self.epicure.seglayer.fill
  82        self.epicure.seglayer.fill = self.epicure_fill
  83        self.napari_paint = self.epicure.seglayer.paint
  84        self.epicure.seglayer.paint = self.lazy #self.epicure_paint
  85        ### scale and radius for paiting
  86        self.paint_scale = np.array([self.epicure.seglayer.scale[i+1] for i in range(2)], dtype=float)
  87        self.epicure.seglayer.events.brush_size.connect( self.paint_radius )
  88        self.paint_radius()
  89        self.disk_one = disk(radius=1)
  90        self.classif = ClassifyIntensity( self )
  91        self.classif_event = ClassifyEvent( self )
  92        self.scalexy = self.epicure.epi_metadata["ScaleXY"]
  93
  94    def painting_tools( self ):
  95        """ Choose which painting tools should be activated """
  96        if self.napari_painting.isChecked():
  97            self.epicure.seglayer.fill = self.napari_fill
  98            self.epicure.seglayer.paint = self.napari_paint
  99        else:
 100            self.epicure.seglayer.fill = self.epicure_fill
 101            self.epicure.seglayer.paint = self.lazy
 102
 103
 104    def apply_settings( self, settings ):
 105        """ Load the prefered settings for Edit panel """
 106        for setting, val in settings.items():
 107            if setting == "Show group option":
 108                self.group_vis.setChecked( val )
 109            if setting == "Show clean option":
 110                self.clean_vis.setChecked( val )
 111            if setting ==  "Show ROI option":
 112                self.select_vis.setChecked( val )
 113            if setting == "Show seed option":
 114                self.seed_vis.setChecked( val )
 115            if setting == "Show groups":
 116                self.group_show.setChecked( val )
 117            if setting == "Border size":
 118                self.border_size.setText( val )
 119            if setting == "Seed method":
 120                self.seed_method.setCurrentText( val )
 121            if setting == "Seed max cell":
 122                self.max_distance.setText( val )
 123           
 124
 125    def get_current_settings( self ):
 126        """ Returns the current state of the Edit widget """
 127        setting = {}
 128        setting["Show group option"] = self.group_vis.isChecked()
 129        setting["Show clean option"] = self.clean_vis.isChecked()
 130        setting["Show ROI option"] = self.select_vis.isChecked()
 131        setting["Show seed option"] = self.seed_vis.isChecked()
 132        setting["Show groups"] = self.group_show.isChecked()
 133        setting["Border size"] = self.border_size.text()
 134        setting["Seed method"] = self.seed_method.currentText()
 135        setting["Seed max cell"] = self.max_distance.text()
 136        return setting
 137   
 138    def paint_radius( self ):
 139        """ Update painitng radius with brush size """
 140        self.radius = np.floor(self.epicure.seglayer.brush_size / 2) + 0.5
 141        self.brush_indices = sphere_indices(self.radius, tuple(self.paint_scale)) 
 142
 143    def setParent(self, epy):
 144        self.epicure = epy
 145
 146    def get_filename(self, endname):
 147        return ut.get_filename(self.epicure.outdir, self.epicure.imgname+endname )
 148        
 149    def get_values(self, coord):
 150        """ Get the label value under coord, the current frame, prepare the coords """
 151        int_coord = tuple(np.round(coord).astype(int))
 152        tframe = int(coord[0])
 153        segdata = self.epicure.seglayer.data[tframe]
 154        int_coord = int_coord[1:3]
 155        # get value of the label that will be painted over
 156        prev_label = int(segdata[int_coord])
 157        return int_coord, tframe, segdata, prev_label
 158
 159    ### Get fill or paint action and assure compatibility with structure
 160    def epicure_fill(self, coord, new_label, refresh=True):
 161        """ Check if the filled cell is already registered """
 162        if new_label == 0:
 163            if self.epicure.verbose > 0:
 164                ut.show_warning("Fill with 0 (background) not allowed \n Use Eraser tool (press <1>) to erase")
 165                return
 166        int_coord, tframe, segdata, prev_label = self.get_values( coord )
 167
 168        hascell = self.epicure.has_label( new_label )
 169        if hascell:
 170            ## already present, check that it is at the same place
 171            ## label before
 172            mask_before = segdata==new_label
 173            if np.sum(mask_before) <= 0:
 174                ut.show_warning("Label "+str(new_label)+" is already used in other frames. Choose another label")
 175                return
 176        
 177        ## if try to fill an empty zone, ensure that it doesn't fill the skeletons
 178        if prev_label == 0:
 179            skel = ut.frame_to_skeleton( segdata )
 180            skel_fill = max(np.max(segdata)+2, new_label+1)
 181            segdata[skel] = skel_fill
 182            skel = None
 183            
 184        if hascell:
 185            # if contiguous replace only selected connected component, calculate how it would be changed
 186            matches = (segdata == prev_label)
 187            labeled_matches, num_features = label(matches, return_num=True)
 188            if num_features != 1:
 189                match_label = labeled_matches[int_coord]
 190                matches = np.logical_and( matches, labeled_matches == match_label )
 191           
 192            # check if touch the already present cell
 193            ok = self.touching_masks(mask_before, matches)
 194            if not ok:
 195                ut.show_warning("Label "+str(new_label)+" added do not touch already present cell. Choose another label or draw contiguously")
 196                ## reset if necessary
 197                if prev_label == 0:
 198                    segdata[segdata==skel_fill] = 0  ## put skeleton back to 0
 199                return
 200            ut.setNewLabel( self.epicure.seglayer, (np.argwhere(matches)).tolist(), new_label, add_frame=tframe )
 201            if prev_label == 0:
 202                segdata[skel] = 0  ## put skeleton back to 0
 203        else:
 204            ## new cell, add it to the tracks list
 205            self.napari_fill(coord, new_label, refresh=True)
 206            if prev_label == 0:
 207                segdata[segdata==skel_fill] = 0  ## put skeleton back to 0
 208                ut.remove_boundaries(segdata)
 209            self.epicure.add_label(new_label, tframe)
 210        
 211        ## Finish filling step to ensure everything's fine
 212        self.epicure.seglayer.refresh()
 213        ## put the active mode of the layer back to the zoom one
 214        self.epicure.seglayer.mode = "pan_zoom"
 215        if prev_label != 0: 
 216            self.epicure.tracking.remove_one_frame( [prev_label], tframe, handle_gaps=self.epicure.forbid_gaps )
 217
 218    def lazy( self, coord, new_label, refresh=True ):
 219        return
 220
 221    def epicure_paint( self, coords, new_label, tframe, hascell ):
 222        """ Edit a label with paint tool, with several pixels at once """
 223        mask_indices = None
 224        ## convert the coords with brush size, check that is fully inside
 225        for coord in coords:
 226            int_coord = np.array( np.round(coord).astype(int)[1:3] ) 
 227            for brush in self.brush_indices:
 228                pt = int_coord + brush
 229                if ut.inside_bounds( pt, self.epicure.imgshape2D ):
 230                    if mask_indices is None:
 231                        mask_indices = pt
 232                    else:
 233                        mask_indices = np.vstack( ( mask_indices, pt ) )
 234        
 235        ## crop around part of the image to update
 236        bbox = ut.getBBoxFromPts( mask_indices, extend=0, imshape=self.epicure.imgshape2D )
 237        if hascell:
 238            ## extend around points a lot if the label is there already to avoid cutting it
 239            extend = 4
 240        else:
 241            extend = 1.5
 242        bbox = ut.extendBBox2D( bbox, extend_factor=extend, imshape=self.epicure.imgshape2D )
 243        cropdata = ut.cropBBox2D( self.epicure.seglayer.data[tframe], bbox )
 244        crop_indices = ut.positions2DIn2DBBox( mask_indices, bbox )
 245        
 246        ## get previous data before painting
 247        prev_labels = np.unique( cropdata[ tuple(np.array(crop_indices).T) ] ).tolist()
 248        if 0 in prev_labels:
 249            prev_labels.remove(0)
 250
 251        if new_label > 0:    
 252            if hascell:
 253                ## check that label is in current frame
 254                mask_before = cropdata==new_label
 255                if not np.isin(1, mask_before):
 256                    ut.show_warning("Label "+str(new_label)+" is already used in other frames. Choose another label")
 257                    return
 258
 259                ## already present, check that it is at the same place
 260                #### Test if painting touch previous label
 261                mask_after = np.zeros(cropdata.shape)
 262                mask_after[ tuple(np.array(crop_indices).T) ] = 1
 263                ok = self.touching_masks(mask_before, mask_after)
 264                if not ok:
 265                    ut.show_warning("Label "+str(new_label)+" added do not touch already present cell. Choose another label or draw contiguously")
 266                    return
 267            else:
 268                ## drawing new cell, fill it at the end
 269                if self.epicure.verbose > 2:
 270                    print("Painting a new cell")
 271
 272        ## Paint and update everything    
 273        painted = np.copy(cropdata)
 274        painted[ tuple(np.array(crop_indices).T) ] = new_label
 275        if new_label > 0:
 276            if self.epicure.seglayer.preserve_labels:
 277                painted = painted*(np.isin( cropdata, [0, new_label] ))
 278                painted = binary_fill_holes( (painted==new_label) )
 279                ## remove one-pixel thick lines
 280                painted = binary_opening( painted )
 281                crop_indices = np.argwhere( (painted>0) )
 282            else:
 283                painted = binary_fill_holes( painted==new_label )
 284                crop_indices = np.argwhere(painted>0)    
 285        ### if preseve label is on, there can be nothing left to paint
 286        if len(crop_indices) <= 0:
 287            return
 288        mask_indices = ut.toFullMoviePos( crop_indices, bbox, tframe )
 289        new_labels = np.repeat(new_label, len(mask_indices)).tolist()
 290
 291        ## Update label boundaries if necessary
 292        cind_bound = ut.ind_boundaries( painted )
 293        if self.epicure.seglayer.preserve_labels:
 294            ind_bound = [ ind for ind in cind_bound if (cropdata[tuple(ind)] == new_label) ]
 295        else:
 296            ind_bound = [ ind for ind in cind_bound if cropdata[tuple(ind)] in prev_labels ]
 297        if (new_label>0) and (len( ind_bound ) > 0):
 298            bound_ind = ut.toFullMoviePos( ind_bound, bbox, tframe )
 299            bound_labels = np.repeat(0, len(bound_ind)).tolist()
 300            mask_indices = np.vstack( (mask_indices, bound_ind) )
 301            new_labels = new_labels + bound_labels
 302
 303        ## Go, apply the change, and update the tracks
 304        self.epicure.change_labels( mask_indices, new_labels )
 305
 306    def create_cell_from_line( self, tframe, positions ):
 307        """ Create new cell(s) from drawn line (junction) """
 308        bbox = ut.getBBox2DFromPts( positions, extend=0, imshape=self.epicure.imgshape2D )
 309        bbox = ut.extendBBox2D( bbox, extend_factor=2, imshape=self.epicure.imgshape2D )
 310
 311        segt = self.epicure.seglayer.data[tframe]
 312        cropt = ut.cropBBox2D( segt, bbox )
 313        crop_positions = ut.positionsIn2DBBox( positions, bbox )
 314
 315        line = np.zeros(cropt.shape, dtype="uint8")
 316        ## fill the already filled pixels by other labels
 317        line[ cropt > 0 ] = 1
 318        ## expand from one pixel to fill the junction
 319        line = binary_dilation( line )
 320        ## fill the interpolated line
 321        for i, pos in enumerate(crop_positions):
 322            if cropt[round(pos[0]), round(pos[1])] == 0:
 323                line[round(pos[0]), round(pos[1])] = 1
 324            if (i > 0):
 325                prev = (crop_positions[i-1][0], crop_positions[i-1][1])
 326                cur = (pos[0], pos[1])
 327                interp_coords = interpolate_coordinates(prev, cur, 1)
 328                for ic in interp_coords:
 329                    line[tuple(np.round(ic).astype(int))] = 1
 330        
 331        ## close the junction gaps, and the line eventually
 332        line = binary_closing( line )
 333        new_cells, nlabels = label( line, background=1, return_num=True, connectivity=1 )
 334        ## no new cell to create
 335        if nlabels <= 0:
 336            return
 337        ## get the new labels to relabel and add as new cells
 338        labels = list( set( new_cells.flatten() ) )
 339        if 0 in labels:
 340            labels.remove(0)
 341       
 342        ## try to get new cell labels from previous and next slices
 343        parents = [None]*len(labels)
 344        if tframe > 0:
 345            twoframes = ut.crop_twoframes( self.epicure.seglayer.data, bbox, tframe )
 346            twoframes[1] = new_cells
 347            twoframes = self.keep_orphans( twoframes, tframe )
 348            parents = self.get_parents( twoframes, labels )
 349        childs = [None]*len(labels)
 350        if tframe < (self.epicure.nframes-1):
 351            twoframes = np.copy( ut.cropBBox2D(self.epicure.seglayer.data[tframe+1], bbox) )
 352            twoframes = np.stack( (twoframes, np.copy(new_cells)) )
 353            twoframes = self.keep_orphans( twoframes, tframe )
 354            childs = self.get_parents( twoframes, labels )
 355        
 356        free_labels = self.epicure.get_free_labels( nlabels )  
 357        torelink = []
 358        for i in range( len(labels) ):
 359            if (parents[i] is not None) and (childs[i] is not None):
 360                free_labels[i] = parents[i]
 361                if self.epicure.verbose > 0:
 362                    print("Link new cell with previous/next "+str(free_labels[i]))
 363                #if childs[i] != parents[i]:
 364                #    torelink.append( [free_labels[i], childs[i]] )
 365            ## only one link found, take it
 366            if (parents[i] is not None) and (childs[i] is None):
 367                free_labels[i] = parents[i]
 368                if self.epicure.verbose > 0:
 369                    print("Link new cell with previous/next "+str(free_labels[i]))
 370            if (parents[i] is None) and (childs[i] is not None):
 371                free_labels[i] = childs[i]
 372                if self.epicure.verbose > 0:
 373                    print("Link new cell with previous/next "+str(free_labels[i]))
 374
 375        print("Added cells "+str(free_labels))
 376
 377        ## get the new indices and labels to draw
 378        new_labels = []
 379        indices = None
 380        for i, lab in enumerate( labels ):
 381            curindices = np.argwhere( new_cells == lab )
 382            if indices is None:
 383                indices = curindices
 384            else:
 385                indices = np.vstack((indices, curindices))
 386            new_labels = new_labels + ([free_labels[i]]*curindices.shape[0])    
 387        
 388        ## add the label boundary
 389        indbound = ut.ind_boundaries( new_cells )
 390        indices = np.vstack( (indices, indbound) )
 391        new_labels = new_labels + np.repeat( 0, len(indbound) ).tolist()
 392        indices = ut.toFullMoviePos( indices, bbox, tframe )
 393        self.epicure.change_labels( indices, new_labels )
 394
 395        ## relink child tracks if necessary
 396        #for relink in torelink:
 397        #    self.epicure.replace_label( relink[1], relink[0], tframe )
 398        
 399    def touching_masks(self, maska, maskb):
 400        """ Check if the two mask touch """
 401        maska = binary_dilation(maska, footprint=self.disk_one)
 402        return np.sum(np.logical_and(maska, maskb))>0
 403    
 404    def touching_indices(self, maska, indices):
 405        """ Check if the indices touch the mask """
 406        maska = binary_dilation(maska, footprint=self.disk_one)
 407        return np.isin(1, maska[indices]) > 0
 408
 409
 410    ## Merging/splitting cells functions
 411    def modify_cells(self):
 412        sl = self.epicure.shortcuts["Labels"]
 413        self.epicure.overtext["labels"] = "---- Labels editing ---- \n"
 414        self.epicure.overtext["labels"] += ut.print_shortcuts( sl )
 415        
 416        sgroup = self.epicure.shortcuts["Groups"]
 417        self.epicure.overtext["grouped"] = "---- Group cells ---- \n"
 418        self.epicure.overtext["grouped"] += ut.print_shortcuts( sgroup )
 419        
 420        sseed = self.epicure.shortcuts["Seeds"]
 421        self.epicure.overtext["seed"] = "---- Seed options --- \n"
 422        self.epicure.overtext["seed"] += ut.print_shortcuts( sseed )
 423
 424        @self.epicure.seglayer.mouse_drag_callbacks.append
 425        def set_checked(layer, event):
 426            if event.type == "mouse_press":
 427                if (event.button == 1) and (len(event.modifiers) == 0):
 428                    if layer.mode == "paint": 
 429                        #and not self.napari_painting.isChecked():
 430                        ### Overwrite the painting to check that everything stays within EpiCure constraints
 431                        if self.shapelayer_name not in self.viewer.layers:
 432                            self.create_shapelayer()
 433                        shape_lay = self.viewer.layers[self.shapelayer_name]
 434                        shape_lay.mode = "add_path"
 435                        shape_lay.visible = True
 436                        @thread_worker
 437                        def refresh_image():                       
 438                            shape_lay.refresh()
 439                            return
 440                        pos = np.array( [shape_lay.world_to_data(event.position)] )
 441                        yield
 442                        ## record all the successives position of the mouse while clicked
 443                        iter = 0
 444                        while (event.type == 'mouse_move'): # and (len(pos)<200):
 445                            pos = np.vstack( (pos, np.array(shape_lay.world_to_data(event.position))) )
 446                            if iter == 5:
 447                                shape_lay.data = pos
 448                                shape_lay.shape_type = "path"
 449                                refresh_image()
 450                                #shape_lay.refresh()
 451                                iter = 0
 452                            iter = iter + 1
 453                            yield
 454                        pos = np.vstack( (pos, np.array(shape_lay.world_to_data(event.position))) )    
 455                        tframe = int( pos[0][0] )
 456                        ## painting a new or extending a cell
 457                        new_label = layer.selected_label
 458                        hascell = None
 459                        if new_label > 0:
 460                            hascell = self.epicure.has_label( new_label )
 461                        ## paint the selected pixels following EpiCure constraints
 462                        self.epicure_paint( pos, new_label, tframe, hascell )
 463                        shape_lay.data = []
 464                        shape_lay.refresh()
 465                        shape_lay.visible = False
 466
 467        @self.epicure.seglayer.mouse_drag_callbacks.append
 468        def set_checked(layer, event):
 469            if event.type == "mouse_press":
 470                if ut.shortcut_click_match( sgroup["add group"], event ):
 471                    if self.group_choice.currentText() == "":
 472                        ut.show_warning("Write a group name before")
 473                        return
 474                    if self.epicure.verbose > 0:
 475                        print("Mark cell in group "+self.group_choice.currentText())
 476                    self.add_cell_to_group(event)
 477                    return
 478                
 479                if ut.shortcut_click_match( sgroup["remove group"], event ):
 480                    if self.epicure.verbose > 0:
 481                        print("Remove cell from its group")
 482                    self.remove_cell_group(event)
 483                    return
 484
 485        @self.epicure.seglayer.bind_key("Control-z", overwrite=False)
 486        def undo_operations(seglayer):
 487            if self.epicure.verbose > 0:
 488                print("Undo previous action")
 489            img_before = np.copy(self.epicure.seg)
 490            self.epicure.seglayer.undo()
 491            self.epicure.update_changed_labels_img( img_before, self.epicure.seglayer.data )
 492
 493        @self.epicure.seglayer.bind_key( sl["unused paint"]["key"], overwrite=True )
 494        def set_nextlabel(layer):
 495            lab = self.epicure.get_free_label()
 496            ut.show_info( "Unused label "+": "+str(lab) )
 497            ut.set_label(layer, lab)
 498        
 499        @self.epicure.seglayer.bind_key( sl["unused fill"]["key"], overwrite=True )
 500        def set_nextlabel_paint(layer):
 501            lab = self.epicure.get_free_label()
 502            ut.show_info( "Unused label "+": "+str(lab) )
 503            ut.set_label(layer, lab)
 504            layer.mode = "FILL"
 505        
 506        @self.epicure.seglayer.bind_key( sl["swap mode"]["key"], overwrite=True )
 507        def key_swap(layer):
 508            """ Active key bindings for label swapping options """
 509            ut.show_info("Begin swap mode: Control and click to swap two labels")
 510            self.old_mouse_drag, self.old_key_map = ut.clear_bindings( self.epicure.seglayer )
 511
 512            @self.epicure.seglayer.mouse_drag_callbacks.append
 513            def click(layer, event):
 514                """ Swap the labels from first to last position of the pressed mouse """
 515                if event.type == "mouse_press":
 516                    if len(event.modifiers) > 0:
 517                        start_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 518                        start_pos = event.position
 519                        yield
 520                        while event.type == 'mouse_move':
 521                            yield
 522                        end_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 523                        end_pos = event.position
 524                        tframe = int(event.position[0])
 525                    
 526                        if start_label == 0 or end_label == 0:
 527                            if self.epicure.verbose > 0:
 528                                print("One position is not a cell, do nothing")
 529                            return
 530
 531                        if (event.button == 1) and ("Control" in event.modifiers):
 532                            # Left-click: swap labels at each end of the click
 533                            if self.epicure.verbose > 0:
 534                                print("Swap cell "+str(start_label)+" and "+str(end_label))
 535                            self.swap_labels(tframe, start_label, end_label)
 536                    
 537                ut.reactive_bindings( self.epicure.seglayer, self.old_mouse_drag, self.old_key_map )
 538                ut.show_info("End swap")
 539        
 540        @self.epicure.seglayer.bind_key( sseed["new seed"]["key"], overwrite=True )
 541        def place_seed(layer):
 542            if self.seed_active:
 543                ## if option is currently on, stop it
 544                self.end_place_seed()
 545                return
 546            if "Seeds" not in self.viewer.layers:
 547                self.create_seedlayer()
 548                ut.set_active_layer( self.viewer, "Segmentation" )
 549            ## desactivate other click-binding
 550            self.old_mouse_drag = self.epicure.seglayer.mouse_drag_callbacks.copy()
 551            self.epicure.seglayer.mouse_drag_callbacks = []
 552            self.seed_active = True
 553            ut.show_info("Left-click to place a new seed")
 554
 555            @self.epicure.seglayer.mouse_drag_callbacks.append
 556            def click(layer, event):
 557                if (event.type == "mouse_press") and (len(event.modifiers)==0) and (event.button==1):
 558                    ## single left-click place a seed
 559                    if "Seeds" not in self.viewer.layers:
 560                        self.reset_seeds()
 561                    self.place_seed(event.position)
 562                else:
 563                    self.end_place_seed()
 564
 565        @self.epicure.seglayer.bind_key( sl["draw junction mode"]["key"], overwrite=True )
 566        def manual_junction(layer):
 567            """ Launch the manual drawing junction mode """
 568            self.drawing_junction_mode()
 569
 570        @self.epicure.seglayer.mouse_drag_callbacks.append
 571        def click(layer, event):
 572            if event.type == "mouse_press":
 573                zoom = self.viewer.camera.zoom ## in case a napari shortcut changes the zoom
 574                center = self.viewer.camera.center ## same
 575                ## erase cell option
 576                if ut.shortcut_click_match( sl["erase"], event ):
 577                    # single right-click: erase the cell
 578                    tframe = ut.current_frame(self.viewer)
 579                    erased = ut.setLabelValue(self.epicure.seglayer, self.epicure.seglayer, event, 0, tframe, tframe)
 580                    ## delete also in track data
 581                    if erased is not None:
 582                        self.epicure.delete_track( erased, tframe )
 583                    ut.reset_view( self.viewer, zoom, center )
 584                    return
 585                        
 586                merging = ut.shortcut_click_match( sl["merge"], event )
 587                splitting = ut.shortcut_click_match( sl["split accross"], event )
 588                if merging or splitting:
 589                    # get the start and last labels
 590                    start_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 591                    start_pos = self.epicure.seglayer.world_to_data( event.position )
 592                    yield
 593                    while event.type == 'mouse_move':
 594                        yield
 595                    end_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 596                    end_pos = self.epicure.seglayer.world_to_data( event.position )
 597                    tframe = int(end_pos[0])
 598                    
 599                    if start_label == 0 or end_label == 0:
 600                        if self.epicure.verbose > 0:
 601                            print("One position is not a cell, do nothing")
 602                        ut.reset_view( self.viewer, zoom, center )
 603                        return
 604
 605                    if merging:
 606                        ## Merge labels at each end of the click
 607                        if start_label != end_label:
 608                            if self.epicure.verbose > 0:
 609                                print("Merge cell "+str(start_label)+" with "+str(end_label))
 610                            self.merge_labels(tframe, start_label, end_label)
 611                            ut.reset_view( self.viewer, zoom, center )
 612                            return
 613                    
 614                    if splitting:
 615                        ## split label at each end of the click
 616                        if start_label == end_label:
 617                            if self.epicure.verbose > 0:
 618                                print("Split cell "+str(start_label))
 619                            self.split_label(tframe, start_label, start_pos, end_pos)
 620                            ut.reset_view( self.viewer, zoom, center )
 621                        else:
 622                            if self.epicure.verbose > 0:
 623                                print("Not the same cell already, do nothing")
 624                    ut.reset_view( self.viewer, zoom, center )
 625                    return
 626
 627                drawing_split = ut.shortcut_click_match( sl["split draw"], event )
 628                redrawing = ut.shortcut_click_match( sl["redraw junction"], event )
 629                if drawing_split or redrawing:
 630                    if self.shapelayer_name not in self.viewer.layers:
 631                        self.create_shapelayer()
 632                    shape_lay = self.viewer.layers[self.shapelayer_name]
 633                    shape_lay.mode = "add_path"
 634                    shape_lay.visible = True
 635                    shape_lay.data = []
 636                    scaled_pos = shape_lay.world_to_data(event.position)
 637                    pos = [scaled_pos]
 638                    yield
 639                    ## record all the successives position of the mouse while clicked
 640                    while event.type == 'mouse_move':
 641                        scaled_pos = shape_lay.world_to_data(event.position)
 642                        pos.append( scaled_pos )
 643                        shape_lay.data = np.array( pos )
 644                        shape_lay.shape_type = "path"
 645                        shape_lay.refresh()
 646                        yield
 647                    scaled_pos = shape_lay.world_to_data(event.position)
 648                    pos.append( scaled_pos )
 649                    ut.set_active_layer(self.viewer, "Segmentation")
 650                    tframe = int(event.position[0])
 651                    if redrawing:
 652                        ##  modify junction along the drawn line
 653                        if self.epicure.verbose > 0:
 654                            print("Correct junction with the drawn line ")
 655                        self.redraw_along_line(tframe, pos)
 656                        shape_lay.data = []
 657                        shape_lay.refresh()
 658                        shape_lay.visible = False
 659                        ut.reset_view( self.viewer, zoom, center )
 660                        return
 661                    if drawing_split:
 662                        ## split labels along the drawn line
 663                        if self.epicure.verbose > 0:
 664                            print("Split cell along the drawn line ")
 665                        self.split_along_line(tframe, pos)
 666                        shape_lay.data = []
 667                        shape_lay.refresh()
 668                        shape_lay.visible = False
 669                        ut.reset_view( self.viewer, zoom, center )
 670                        return
 671                    ut.reset_view( self.viewer, zoom, center )
 672                    return
 673        
 674    def drawing_junction_mode( self ):
 675        """ Active mouse bindings for manually drawing the junction, and try to fill defined area """
 676            
 677        sl = self.epicure.shortcuts["Labels"]
 678        ut.show_info("Begin drawing junction: Control-Left-click to draw the junction and create new cell(s) from it")
 679        self.old_mouse_drag, self.old_key_map = ut.clear_bindings( self.epicure.seglayer )
 680        
 681        @self.epicure.seglayer.bind_key( sl["draw junction mode"]["key"], overwrite=True )
 682        def stop_draw_junction_mode( layer ):
 683            ut.reactive_bindings( self.epicure.seglayer, self.old_mouse_drag, self.old_key_map )
 684            ut.show_info("End drawing mode")
 685        
 686        @self.epicure.seglayer.mouse_drag_callbacks.append
 687        def click(layer, event):
 688            if ut.shortcut_click_match( sl["drawing junction"], event ):
 689                shape_lay = self.viewer.layers[self.shapelayer_name]
 690                shape_lay.mode = "add_path"
 691                shape_lay.visible = True
 692                scaled_position = shape_lay.world_to_data( event.position )
 693                pos = [scaled_position]
 694                yield
 695                ## record all the successives position of the mouse while clicked
 696                i = 0
 697                while event.type == 'mouse_move':
 698                    scaled_position = shape_lay.world_to_data( event.position )
 699                    pos.append( scaled_position )
 700                    if i%5 == 0:
 701                        # refresh display every n steps
 702                        shape_lay.data = np.array( pos ) 
 703                        shape_lay.shape_type = "path"
 704                        shape_lay.refresh()
 705                    i = i + 1
 706                    yield
 707                scaled_position = shape_lay.world_to_data( event.position )
 708                pos.append(scaled_position)
 709                ut.set_active_layer(self.viewer, "Segmentation")
 710                tframe = int(event.position[0])
 711                self.create_cell_from_line( tframe, pos )        
 712                shape_lay.data = []
 713                shape_lay.refresh()
 714                shape_lay.visible = False
 715                ut.reactive_bindings( self.epicure.seglayer, self.old_mouse_drag, self.old_key_map )
 716                ut.show_info("End drawing mode")
 717
 718    def split_label(self, tframe, startlab, start_pos, end_pos):
 719        """ Split the label in two cells based on the two seeds """
 720        segt = self.epicure.seglayer.data[tframe]
 721        labelBB = ut.getBBox2D(segt, startlab)
 722        labelBB = ut.extendBBox2D( labelBB, extend_factor=1.25, imshape=self.epicure.imgshape2D )
 723
 724        mov = self.viewer.layers["Movie"].data[tframe]
 725        imgBB = ut.cropBBox2D(mov, labelBB)
 726        segBB = ut.cropBBox2D(segt, labelBB)
 727        maskBB = np.zeros(segBB.shape, dtype="uint8")
 728        maskBB[segBB==startlab] = 1
 729        spos = ut.positionIn2DBBox( start_pos, labelBB )
 730        epos = ut.positionIn2DBBox( end_pos, labelBB )
 731
 732        markers = np.zeros(maskBB.shape, dtype=self.epicure.dtype)
 733        markers[spos] = startlab
 734        markers[epos] = self.epicure.get_free_label()
 735        splitted = watershed( imgBB, markers=markers, mask=maskBB )
 736        if (np.sum(splitted==startlab) < self.epicure.minsize) or (np.sum(splitted==markers[epos]) < self.epicure.minsize):
 737            if self.epicure.verbose > 0:
 738                print("Sorry, split failed, one cell smaller than "+str(self.epicure.minsize)+" pixels")
 739        else:
 740            if len(np.unique(splitted)) > 2:
 741                curframe = np.zeros(segBB.shape, dtype="uint8")
 742                labels = []
 743                for i, splitlab in enumerate(np.unique(splitted)):
 744                    if splitlab > 0:
 745                        curframe[splitted==splitlab] = i+1
 746                        labels.append(i+1)
 747
 748                curframe = ut.remove_boundaries(curframe)
 749                ## apply the split and propagate the label to descendant label
 750                self.propagate_label_change( curframe, labels, labelBB, tframe, [startlab] )
 751            else:
 752                if self.epicure.verbose > 0:
 753                    print("Split failed, no boundary in pixel intensities found")
 754
 755
 756    def redraw_along_line(self, tframe, positions):
 757        """ Redraw the two labels separated by a line drawn manually """
 758        bbox = ut.getBBox2DFromPts( positions, extend=0, imshape=self.epicure.imgshape2D )
 759        #bbox = ut.extendBBox2D( bbox, extend_factor=1.25, imshape=self.epicure.imgshape2D )
 760
 761        segt = self.epicure.seglayer.data[tframe]
 762        cropt = ut.cropBBox2D( segt, bbox )
 763        crop_positions = ut.positionsIn2DBBox( positions, bbox )
 764
 765        # get the value of the cells to update (most frequent label along the line)
 766        curlabels = []
 767        prev_pos = None
 768        # Find closest zero elements in the inverted image (same as closest non-zero for image)
 769        
 770        crop_zeros = distance_transform_edt(cropt, return_distances=False, return_indices=True)
 771
 772        for pos in crop_positions:
 773            if (prev_pos is None) or ((round(pos[0]) != round(prev_pos[0])) and (round(pos[1]) != round(prev_pos[1]) )):
 774                ## find closest pixel that is 0 (on a junction)
 775                juncpoint = crop_zeros[:, round(pos[0]), round(pos[1])]
 776                labs = np.unique( cropt[ (juncpoint[0]-2):(juncpoint[0]+2), (juncpoint[1]-2):(juncpoint[1]+2) ] )
 777                for clab in labs:
 778                    if clab > 0:
 779                        curlabels.append(clab)
 780                prev_pos = pos
 781                
 782        sort_curlabel = sorted(set(curlabels), key=curlabels.count)
 783        ## external junction: only one cell
 784        if len(sort_curlabel) < 2:
 785            if self.epicure.verbose > 0:
 786                print("Only one cell along the junction: can't do it")
 787                return
 788        flabel = sort_curlabel[-1]
 789        slabel = sort_curlabel[-2]
 790        if self.epicure.verbose > 0:
 791            print("Cells to update: "+str(flabel)+" "+str(slabel))
 792        
 793        ## crop around selected label
 794        bbox, _ = ut.getBBox2DMerge( segt, flabel, slabel )
 795        bbox = ut.extendBBox2D( bbox, extend_factor=1.25, imshape=self.epicure.imgshape2D )
 796        init_cropt = ut.cropBBox2D( segt, bbox )
 797        curlabel = flabel
 798        ## merge the two labels together
 799        binlab = np.isin( init_cropt, [flabel, slabel] )*1
 800        footprint = disk(radius=2)
 801        cropt = flabel*binary_closing(binlab, footprint)
 802        crop_positions = ut.positionsIn2DBBox( positions, bbox )
 803
 804        # draw the line only in the cell to split
 805        line = np.zeros(cropt.shape, dtype="uint8")
 806        for i, pos in enumerate(crop_positions):
 807            if cropt[round(pos[0]), round(pos[1])] == curlabel:
 808                line[round(pos[0]), round(pos[1])] = 1
 809            if (i > 0):
 810                prev = (crop_positions[i-1][0], crop_positions[i-1][1])
 811                cur = (pos[0], pos[1])
 812                interp_coords = interpolate_coordinates(prev, cur, 1)
 813                for ic in interp_coords:
 814                    line[tuple(np.round(ic).astype(int))] = 1
 815        self.move_in_crop( curlabel, init_cropt, cropt, crop_positions, line, bbox, tframe, retry=0)
 816    
 817    def move_in_crop(self, curlabel, init_cropt, cropt, crop_positions, line, bbox, frame, retry):
 818        """ Move the junction in the cropped region """
 819        dis = retry
 820        footprint = disk(radius=dis)
 821        dilline = binary_dilation(line, footprint=footprint)
 822
 823        # get the two splitted regions and relabel one of them
 824        clab = np.zeros(cropt.shape, dtype="uint8")
 825        clab[cropt==curlabel] = 1
 826        clab[dilline] = 0
 827        labels = label(clab, background=0, connectivity=1)
 828        if (np.max(labels) == 2) & (np.sum(labels==1)>self.epicure.minsize) & (np.sum(labels==2)>self.epicure.minsize):
 829            ## get new image with the 2 cells to retrack
 830            labels = ut.touching_labels(labels, expand=dis+1)
 831            indmodif = []
 832            newlabels = []
 833            for i in range(2):
 834                imodif = ( (labels==(i+1)) & (cropt==curlabel) )
 835                val, counts = np.unique( init_cropt[ imodif ], return_counts=True) 
 836                init_label = val[np.argmax(counts)]
 837                imodif = np.argwhere(imodif).tolist()
 838                indmodif = indmodif + imodif
 839                newlabels = newlabels + np.repeat( init_label, len(imodif) ).tolist()
 840            
 841            indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
 842            
 843            # remove the boundary between the two updated labels only
 844            cind_bound = ut.ind_boundaries( labels )
 845            ind_bound = [ ind for ind in cind_bound if cropt[tuple(ind)]==curlabel ]
 846            ind_bound = ut.toFullMoviePos( ind_bound, bbox, frame )
 847            indmodif = np.vstack((indmodif, ind_bound))
 848            newlabels = newlabels + np.repeat(0, len(ind_bound)).tolist()
 849            
 850            self.epicure.change_labels( indmodif, newlabels )
 851            ## udpate the centroid of the modified labels
 852            #for clabel in np.unique(newlabels):
 853            #    if clabel > 0:
 854            #        self.epicure.update_centroid( clabel, frame )
 855        else:
 856            if (retry > 6) :
 857                if self.epicure.verbose > 0:
 858                    print("Update failed "+str(np.max(labels)))
 859                return
 860            retry = retry + 1
 861            self.move_in_crop(curlabel, init_cropt, cropt, crop_positions, line, bbox, frame, retry=retry)
 862
 863    def split_along_line(self, tframe, positions):
 864        """ Split a label along a line drawn manually """
 865        bbox = ut.getBBox2DFromPts( positions, extend=0, imshape=self.epicure.imgshape2D )
 866        bbox = ut.extendBBox2D( bbox, extend_factor=1.25, imshape=self.epicure.imgshape2D )
 867
 868        segt = self.epicure.seglayer.data[tframe]
 869        cropt = ut.cropBBox2D( segt, bbox )
 870        crop_positions = ut.positionsIn2DBBox( positions, bbox )
 871
 872        # get the value of the cell to split (most frequent label along the line)
 873        curlabels = []
 874        prev_pos = None
 875        for pos in crop_positions:
 876            if (prev_pos is None) or ((round(pos[0]) != round(prev_pos[0])) and (round(pos[1]) != round(prev_pos[1]) )):
 877                clab = cropt[round(pos[0]), round(pos[1])]
 878                curlabels.append(clab)
 879                prev_pos = pos
 880                
 881        curlabel = max(set(curlabels), key=curlabels.count)
 882        if self.epicure.verbose > 0:
 883            print("Cell to split: "+str(curlabel))
 884        if curlabel == 0:
 885            if self.epicure.verbose > 0:
 886                print("Refusing to split background")
 887            return               
 888                        
 889        ## crop around selected label
 890        bbox = ut.getBBox2D(segt, curlabel)
 891        bbox = ut.extendBBox2D( bbox, extend_factor=1.5, imshape=self.epicure.imgshape2D )
 892        cropt = ut.cropBBox2D( segt, bbox )
 893        crop_positions = ut.positionsIn2DBBox( positions, bbox )
 894
 895        # draw the line only in the cell to split
 896        line = np.zeros(cropt.shape, dtype="uint8")
 897        for i, pos in enumerate(crop_positions):
 898            if cropt[round(pos[0]), round(pos[1])] == curlabel:
 899                line[round(pos[0]), round(pos[1])] = 1
 900            if (i > 0):
 901                prev = (crop_positions[i-1][0], crop_positions[i-1][1])
 902                cur = (pos[0], pos[1])
 903                interp_coords = interpolate_coordinates(prev, cur, 1)
 904                for ic in interp_coords:
 905                    line[tuple(np.round(ic).astype(int))] = 1
 906        self.split_in_crop( curlabel, cropt, crop_positions, line, bbox, tframe, retry=0)
 907
 908    def split_in_crop(self, curlabel, cropt, crop_positions, line, bbox, frame, retry):
 909        """ Find the split to do in the cropped region """
 910        dis = retry
 911        footprint = disk(radius=dis)
 912        dilline = binary_dilation(line, footprint=footprint)
 913
 914        # get the two splitted regions and relabel one of them
 915        clab = np.zeros(cropt.shape, dtype="uint8")
 916        clab[cropt==curlabel] = 1
 917        clab[dilline] = 0
 918        labels = label(clab, background=0, connectivity=1)
 919        if (np.max(labels) == 2) & (np.sum(labels==1)>self.epicure.minsize) & (np.sum(labels==2)>self.epicure.minsize):
 920            ## get new image with the 2 cells to retrack
 921            labels = ut.touching_labels(labels, expand=dis+1)
 922            curframe = np.zeros( cropt.shape, dtype="uint8" )
 923            for i in range(2):
 924                curframe[ (labels==(i+1)) & (cropt==curlabel) ] = i+1
 925            
 926            curframe = ut.remove_boundaries(curframe)
 927            self.propagate_label_change( curframe, [1,2], bbox, frame, [curlabel] )
 928
 929        else:
 930            if (retry > 6) :
 931                if self.epicure.verbose > 0:
 932                    print("Split failed "+str(np.max(labels)))
 933                return
 934            retry = retry + 1
 935            self.split_in_crop(curlabel, cropt, crop_positions, line, bbox, frame, retry=retry)
 936
 937    def merge_labels(self, tframe, startlab, endlab, extend_factor=1.25):
 938        """ Merge the two given labels """
 939        start_time = ut.start_time()
 940        segt = self.epicure.seglayer.data[tframe]
 941        
 942        ## Crop around labels to work on smaller field of view
 943        bbox, merged = ut.getBBox2DMerge( segt, startlab, endlab )
 944        
 945        ## keep only the region of interest
 946        bbox = ut.extendBBox2D( bbox, extend_factor, self.epicure.imgshape2D )
 947        segt_crop = ut.cropBBox2D( segt, bbox )
 948
 949        ## check that labels can be merged
 950        touch = ut.checkTouchingLabels( segt_crop, startlab, endlab )
 951        if not touch:
 952            ut.show_warning("Labels not touching, I refuse to merge them")
 953            return
 954
 955        ## merge the two labels together
 956        joinlab = ut.cropBBox2D( merged, bbox )
 957        footprint = disk(radius=2)
 958        joinlab = endlab * binary_closing(joinlab, footprint)
 959        
 960        if self.epicure.verbose > 1:
 961            ut.show_duration(start_time, "Merged in ")
 962
 963        ## update and propagate the change
 964        self.propagate_label_change(joinlab, [endlab], bbox, tframe, [startlab, endlab])
 965        if self.epicure.verbose > 1:
 966            ut.show_duration(start_time, "Merged and propagated in ")
 967
 968    def touching_labels(self, img, lab, olab):
 969        """ Check if the two labels are neighbors or not """
 970        flab = find_boundaries(img==lab)
 971        folab = find_boundaries(img==olab)
 972        return np.sum(np.logical_and(flab, folab))>0
 973    
 974    def swap_labels(self, tframe, lab, olab):
 975        """ Swap two labels """
 976        segt = self.epicure.seglayer.data[tframe]
 977        ## Get the two labels position to swap
 978        modiflab = np.argwhere(segt==lab).tolist()
 979        modifolab = np.argwhere(segt==olab).tolist()
 980        newlabs = np.repeat(olab, len(modiflab)).tolist() + np.repeat(lab, len(modifolab)).tolist()
 981        ## Change the labels
 982        ut.setNewLabel( self.epicure.seglayer, modiflab+modifolab, newlabs, add_frame=tframe )
 983        ## Update the tracks and graph with swap
 984        self.epicure.swap_labels( lab, olab, tframe )
 985        self.epicure.seglayer.refresh()
 986
 987
 988    ######################
 989    ## Erase border cells
 990    def remove_border(self):
 991        """ Remove all cells that touch the border """
 992        start_time = ut.start_time()
 993        self.viewer.window._status_bar._toggle_activity_dock(True)
 994        size = int(self.border_size.text())
 995        if size == 0:
 996            for i in progress(range(0, self.epicure.nframes)):
 997                img = np.copy( self.epicure.seglayer.data[i] )
 998                resimg = clear_border( img )
 999                self.epicure.seglayer.data[i] = resimg
1000                self.epicure.removed_labels( img, resimg, i )
1001        else:
1002            maxx = self.epicure.imgshape2D[0] - size - 1
1003            maxy = self.epicure.imgshape2D[1] - size - 1
1004            for i in progress(range(0, self.epicure.nframes)):
1005                frame = self.epicure.seglayer.data[i]
1006                img = np.copy( frame ) 
1007                crop_img = img[ size:maxx, size:maxy ]
1008                crop_img = clear_border( crop_img )
1009                frame[0:size, :] = 0
1010                frame[:, 0:size] = 0
1011                frame[maxx:, :] = 0
1012                frame[:, maxy:] = 0
1013                frame[size:maxx, size:maxy] = crop_img
1014                ## update the tracks after the potential disappearance of some cells
1015                self.epicure.removed_labels( img, frame, i )
1016        
1017        self.viewer.window._status_bar._toggle_activity_dock(False)
1018        self.epicure.seglayer.refresh()
1019        if self.epicure.verbose > 0:
1020            ut.show_duration( start_time, "Border cells removed in ")
1021
1022               
1023
1024    def remove_smalls( self ):
1025        """ Remove all cells smaller than given area (in nb pixels) """
1026        start_time = ut.start_time()
1027        self.viewer.window._status_bar._toggle_activity_dock(True)
1028        for i in progress(range(0, self.epicure.nframes)):
1029            self.remove_small_cells( np.copy(self.epicure.seglayer.data[i]), i)
1030        self.viewer.window._status_bar._toggle_activity_dock(False)
1031        if self.epicure.verbose > 0:
1032            ut.show_duration( start_time, "Small cells removed in ")
1033
1034    def remove_small_cells(self, img, frame):
1035        """ Remove if few the cell is only few pixels """
1036        #init_labels = set(np.unique(img))
1037        minarea = int(self.small_size.text())
1038        props = ut.labels_properties( img )
1039        resimg = np.copy( img )
1040        for prop in props:
1041            if prop.area < minarea:
1042                (resimg[prop.slice])[prop.image] = 0
1043        ## update the tracks after the potential disappearance of some cells
1044        self.epicure.seglayer.data[frame] = resimg
1045        self.epicure.removed_labels( img, resimg, frame )
1046    
1047    def merge_inside_cells( self ):
1048        """ Merge cell that falls inside another cell with ut """
1049        start_time = ut.start_time()
1050        self.viewer.window._status_bar._toggle_activity_dock(True)
1051        for i in progress(range(0, self.epicure.nframes)):
1052            self.merge_inside_cell(self.epicure.seglayer.data[i], i)
1053        self.viewer.window._status_bar._toggle_activity_dock(False)
1054        if self.epicure.verbose > 0:
1055            ut.show_duration( start_time, "Inside cells merged in ")
1056
1057    def merge_inside_cell( self, img, frame ):
1058        """ Merge cells that fits inside the convex hull of a cell with it """
1059        graph = ut.connectivity_graph( img, distance=3)
1060        adj_bg = []
1061        
1062        nodes = list(graph.nodes)
1063        for label in nodes:
1064            nneighbor = len(graph.adj[label])
1065            if nneighbor == 1:
1066                neigh_label = graph.adj[label]
1067                for lab in neigh_label.keys():
1068                    nlabel = int( lab )
1069                # both labels are still present in the current frame
1070                if nlabel>0 and sum( np.isin( [label, nlabel], self.epicure.seglayer.data[frame] ) ) == 2:
1071                    self.merge_labels( frame, label, nlabel, 1.05 )
1072                    if self.epicure.verbose > 0:
1073                        print( "Merged label "+str(label)+" into label "+str(nlabel)+" at frame "+str(frame) )
1074
1075    ###############
1076    ## Shapes functions
1077    def create_shapelayer( self ):
1078        """ Create the layer that handle temporary drawings """
1079        shapes = []
1080        shap = self.viewer.add_shapes( shapes, name=self.shapelayer_name, ndim=3, blending="additive", opacity=1, edge_width=2, scale=self.viewer.layers["Segmentation"].scale )
1081        shap.text.visible = False
1082        shap.visible = False
1083
1084    ######################################"
1085    ## Seeds and watershed functions
1086    def show_hide_seedMapBlock(self):
1087        self.gSeed.setVisible(not self.gSeed.isVisible())
1088        if not self.gSeed.isVisible():
1089            ut.remove_layer(self.viewer, "Seeds")
1090    
1091    def create_seedsBlock(self):
1092        seed_layout = wid.vlayout()
1093        reset_color = self.epicure.get_resetbtn_color()
1094        seed_createbtn = wid.add_button( btn="Create seeds layer", btn_func=self.reset_seeds, descr="Create/reset the layer to add seeds", color=reset_color )
1095        seed_layout.addWidget(seed_createbtn)
1096        seed_loadbtn = wid.add_button( btn="Load seeds from previous time point", btn_func=self.get_seeds_from_prev, descr="Place seeds in background area where cells are in previous time point" )
1097        seed_layout.addWidget(seed_loadbtn)
1098        
1099        ## choose method and segment from seeds
1100        gseg, gseg_layout = wid.group_layout( "Seed based segmentation" )
1101        seed_btn = wid.add_button( btn="Segment cells from seeds", btn_func=self.segment_from_points, descr="Segment new cells from placed seeds" )
1102        gseg_layout.addWidget(seed_btn)
1103        method_line, self.seed_method = wid.list_line( label="Method", descr="Seed based segmentation method to segment some cells" )
1104        self.seed_method.addItem("Intensity-based (watershed)")
1105        self.seed_method.addItem("Distance-based")
1106        self.seed_method.addItem("Diffusion-based")
1107        gseg_layout.addLayout( method_line )
1108        maxdist, self.max_distance = wid.value_line( label="Max cell radius", default_value="100.0", descr="Max cell radius allowed in new cell creation" )
1109        gseg_layout.addLayout(maxdist)
1110        gseg.setLayout(gseg_layout)
1111        
1112        seed_layout.addWidget(gseg)
1113        self.gSeed.setLayout(seed_layout)
1114
1115    def create_seedlayer(self):
1116        pts = []
1117        ## handle change of parameter name in napari versions
1118        if ut.version_napari_above("0.4.19"):
1119            self.viewer.add_points( np.array(pts), face_color="blue", size = 7,  border_width=0, name="Seeds", scale=self.viewer.layers["Segmentation"].scale )
1120        else:
1121            self.viewer.add_points( np.array(pts), face_color="blue", size = 7,  edge_width=0, name="Seeds", scale=self.viewer.layers["Segmentation"].scale )
1122
1123    def reset_seeds(self):
1124        ut.remove_layer(self.viewer, "Seeds")
1125        self.create_seedlayer()
1126
1127    def get_seeds_from_prev(self):
1128        #self.reset_seeds()
1129        if "Seeds" not in self.viewer.layers:
1130            self.create_seedlayer()
1131        tframe = int(self.viewer.cursor.position[0])
1132        segt = self.epicure.seglayer.data[tframe]
1133        if tframe > 0:
1134            pts = self.viewer.layers["Seeds"].data
1135            segp = self.epicure.seglayer.data[tframe-1]
1136            props = ut.labels_properties(segp)
1137            for prop in props:
1138                cent = prop.centroid
1139                ## create a seed in the centroid only in empty spaces
1140                if int(segt[int(cent[0]), int(cent[1])]) == 0:
1141                    pts = np.append(pts, [[tframe, cent[0], cent[1]]], axis=0)
1142            self.viewer.layers["Seeds"].data = pts
1143            self.viewer.layers["Seeds"].refresh()
1144        
1145    def end_place_seed(self):
1146        """ Finish placing seeds mode """
1147        if not self.seed_active:
1148            return
1149        if self.old_mouse_drag is not None:
1150            self.epicure.seglayer.mouse_drag_callbacks = self.old_mouse_drag
1151            self.seed_active = False
1152            ut.show_info("End seed")
1153        ut.set_active_layer( self.viewer, "Segmentation" )
1154
1155    def place_seed(self, event_pos):
1156        """ Add a seed under the cursor """
1157        tframe = int(self.viewer.cursor.position[0])
1158        segt = self.epicure.seglayer.data[tframe]
1159        pts = self.viewer.layers["Seeds"].data
1160        cent = self.viewer.layers["Seeds"].world_to_data( event_pos )
1161        ## create a seed in the centroid only in empty spaces
1162        if int(segt[int(cent[1]), int(cent[2])]) == 0:
1163            pts = np.append(pts, [[tframe, cent[1], cent[2]]], axis=0)
1164            self.viewer.layers["Seeds"].data = pts
1165            self.viewer.layers["Seeds"].refresh()
1166        ut.set_active_layer( self.viewer, "Segmentation" )
1167
1168
1169    def segment_from_points(self):
1170        """ Do cells segmentation from seed points """
1171        if not "Seeds" in self.viewer.layers:
1172            ut.show_warning("No seeds placed")
1173            return
1174        self.end_place_seed()
1175        if len(self.viewer.layers["Seeds"].data) <= 0:
1176            ut.show_warning("No seeds placed")
1177            return
1178
1179        ## get crop of the image around seeds
1180        tframe = ut.current_frame(self.viewer)
1181        segBB, markers, maskBB, labelBB = self.crop_around_seeds( tframe )
1182        ## save current labels to compare afterwards
1183        before_seeding = np.copy(segBB)
1184
1185        ## segment current seeds from points with selected method
1186        if self.seed_method.currentText() == "Intensity-based (watershed)":
1187            self.watershed_from_points( tframe, segBB, markers, maskBB, labelBB )
1188        if self.seed_method.currentText() == "Distance-based":
1189            self.distance_from_points( tframe, segBB, markers, maskBB, labelBB )
1190        if self.seed_method.currentText() == "Diffusion-based":
1191            self.diffusion_from_points( tframe, segBB, markers, maskBB, labelBB )
1192
1193        ## finish segmentation: thin to have one pixel boundaries, update all
1194        skelBB = ut.frame_to_skeleton( segBB, connectivity=1 )
1195        segBB[ skelBB>0 ] = 0
1196        self.reset_seeds()
1197        ## update the list of tracks with the potential new cells
1198        self.epicure.added_labels_oneframe( tframe, before_seeding, segBB )
1199        #self.end_place_seed()
1200        ut.set_active_layer( self.viewer, "Segmentation" )
1201        self.epicure.seglayer.refresh()
1202
1203    def crop_around_seeds( self, tframe ):
1204        """ Get cropped image around the seeds """
1205        ## crop around the seeds, with a margin
1206        seeds = self.viewer.layers["Seeds"].data
1207        segt = self.epicure.seglayer.data[tframe]
1208        extend = int(float(self.max_distance.text())*1.1)
1209        labelBB = ut.getBBox2DFromPts( seeds, extend, segt.shape )
1210        segBB = ut.cropBBox2D(segt, labelBB)
1211        ## mask where there are cells
1212        maskBB = np.copy(segBB)
1213        maskBB = 1*(maskBB==0)
1214        maskBB = np.uint8(maskBB)
1215        ## fill the borders
1216        maskBB = binary_erosion(maskBB, footprint=self.disk_one)
1217        ## place labels in the seed positions
1218        pos = ut.positionsIn2DBBox( seeds, labelBB )
1219        markers = np.zeros(maskBB.shape, dtype="int32")
1220        freelabs = self.epicure.get_free_labels( len(pos) )
1221        for freelab, p in zip(freelabs, pos):
1222            markers[p] = freelab
1223        return segBB, markers, maskBB, labelBB
1224    
1225    def diffusion_from_points(self, tframe, segBB, markers, maskBB, labelBB):
1226        """ Segment from seeds with a diffusion based method (gradient intensity slows it) """
1227        movt = self.viewer.layers["Movie"].data[tframe]
1228        imgBB = ut.cropBBox2D(movt, labelBB)
1229        markers[maskBB==0] = -1 ## block filled area 
1230        ## fill from seeds with diffusion method
1231        splitted = random_walker( imgBB, labels=markers, beta=700, tol=0.01 )
1232        new_labels = list(np.unique(markers))
1233        new_labels.remove(-1)
1234        new_labels.remove(0)
1235        i = 0
1236        lablist = set( splitted.flatten() )
1237        #print(lablist)
1238        #print(new_labels)
1239        for lab in lablist:
1240            if lab > 0:
1241                mask = (splitted == lab)
1242                labels_mask = label(mask)                       
1243                ## keep only biggest region if the label is splitted
1244                regions = ut.labels_properties(labels_mask)
1245                if len(regions) > 2:
1246                    regions.sort(key=lambda x: x.area, reverse=True)
1247                    if len(regions) > 1:
1248                        for rg in regions[1:]:
1249                            splitted[rg.coords[:,0], rg.coords[:,1]] = 0
1250                splitted[splitted==lab] = new_labels[i]
1251                i = i + 1
1252        segBB[(maskBB>0)*(splitted>0)] = splitted[(maskBB>0)*(splitted>0)]
1253        return segBB
1254
1255    def watershed_from_points(self, tframe, segBB, markers, maskBB, labelBB):
1256        """ Performs watershed from the seed points """
1257        movt = self.viewer.layers["Movie"].data[tframe] 
1258        imgBB = ut.cropBBox2D(movt, labelBB)
1259        splitted = watershed( imgBB, markers=markers, mask=maskBB )
1260        segBB[splitted>0] = splitted[splitted>0]
1261        return segBB
1262    
1263    def distance_from_points(self, tframe, segBB, markers, maskBB, labelBB):
1264        """ Segment cells from seed points with Voronoi method """
1265        # iteratif to block when meet other fixed labels 
1266        maxdist = float(self.max_distance.text())
1267        dist = 0
1268        while dist <= maxdist:
1269            markers = ut.touching_labels( markers, expand=1 )
1270            markers[maskBB==0] = 0
1271            dist = dist + 1
1272        segBB[(maskBB>0) * (markers>0)] = markers[(maskBB>0) * (markers>0)]
1273        return segBB
1274        
1275
1276    ######################################
1277    ## Cleaning options
1278
1279    def create_cleaningBlock(self):
1280        """ GUI for cleaning segmentation """
1281        clean_layout = wid.vlayout()
1282        ## cells on border
1283        border_line, self.border_size = wid.button_parameter_line( btn="Remove border cells", btn_func=self.remove_border, value="1", descr_btn="Remove all cell at a distance <= value (in pixels)", descr_value="Distance of the cells to be removed (in pixels)" )
1284        clean_layout.addLayout(border_line)
1285        
1286        ## too small cells
1287        small_line, self.small_size = wid.button_parameter_line( btn="Remove mini cells", btn_func=self.remove_smalls, value="4", descr_btn="Remove all cells smaller than given value (in pixels^2)", descr_value="Minimal cell area (in pixels^2)" )
1288        clean_layout.addLayout(small_line)
1289
1290        ## Cell inside another cell
1291        inside_btn = wid.add_button( btn="Cell inside another: merge", btn_func=self.merge_inside_cells, descr="Merge all small cells fully contained inside another cell to this cell" )
1292        clean_layout.addWidget(inside_btn)
1293
1294        ## sanity check
1295        sanity_btn = wid.add_button( btn="Sanity check", btn_func=self.sanity_check, descr="Check that labels and tracks are consistent with EpiCure restrictions, and try to fix some errors" )
1296        clean_layout.addWidget(sanity_btn)
1297
1298        ## reset labels
1299        reset_color = self.epicure.get_resetbtn_color()
1300        reset_btn = wid.add_button( btn="Reset all", btn_func=self.reset_all, descr="Reset all tracks, groups, suspects..", color=reset_color )
1301        clean_layout.addWidget(reset_btn)
1302
1303        self.gCleaned.setLayout(clean_layout)
1304
1305    ####################################
1306    ## Sanity check/correction options
1307    def sanity_check(self):
1308        """ Check if everything looks okayish, in case some bug or weird editions broke things """
1309        self.viewer.window._status_bar._toggle_activity_dock(True)
1310        progress_bar = progress(total=6)
1311        progress_bar.set_description("Sanity check:")
1312        progress_bar.update(0)
1313        ## check layers presence
1314        ut.show_info("Check and reopen if necessary EpiCure layers")
1315        self.epicure.check_layers()
1316        ## check that each label is unique
1317        progress_bar.update(1)
1318        progress_bar.set_description("Sanity check: label unicity")
1319        label_list = np.unique(self.epicure.seglayer.data)
1320        if self.epicure.verbose > 0:
1321            print("Checking label unicity...")
1322        self.check_unique_labels( label_list, progress_bar )
1323        ## check and update if necessary tracks 
1324        progress_bar.update(2)
1325        if self.epicure.forbid_gaps:
1326            progress_bar.set_description("Sanity check: track gaps")
1327            ut.show_info("Check if some tracks contain gaps")
1328            gaped = self.epicure.handle_gaps( track_list=None )
1329        ## check that labels and tracks correspond
1330        progress_bar.set_description("Sanity check: label-track")
1331        progress_bar.update(3)
1332        if self.epicure.verbose > 0:
1333            print("Checking labels-tracks correspondance...")
1334        track_list = self.epicure.tracking.get_track_list()
1335        untracked = list(set(label_list) - set(track_list))
1336        if 0 in untracked:
1337            untracked.remove(0)
1338        if len(untracked) > 0:
1339            ut.show_warning("! Labels "+str(untracked)+" not in Tracks -- Adding it now")
1340            for untrack in untracked:
1341                self.epicure.add_one_label_to_track( untrack )
1342        
1343        ## update label list with changes that might have been done
1344        label_list = np.unique(self.epicure.seglayer.data)
1345        track_list = self.epicure.tracking.get_track_list()
1346        ## check if all tracks have associated labels in the image
1347        phantom_tracks = list(set(track_list) - set(label_list))
1348        if len(phantom_tracks) > 0:
1349            print("! Phantom tracks "+str(phantom_tracks)+" found")
1350            self.epicure.delete_tracks(phantom_tracks)
1351            print("-> Phantom tracks deleted from Tracks")
1352        
1353        ## checking events
1354        progress_bar.set_description("Sanity check: extrusions")
1355        progress_bar.update(5)
1356        if self.epicure.verbose > 0:
1357            print("Checking extrusion = end of track...")
1358        self.epicure.check_extrusions_sanity()
1359        
1360        ## finished
1361        if self.epicure.verbose > 0:
1362            print("Checking finished")
1363        progress_bar.close()
1364        self.viewer.window._status_bar._toggle_activity_dock(False)
1365
1366    def check_unique_labels(self, label_list, progress_bar):
1367        """ Check that all labels are contiguous and not present several times (only by frame) """
1368        found = 0
1369        s = generate_binary_structure(2,2)
1370        pbtmp = progress(total=len(label_list), desc="Check labels", nest_under=progress_bar)
1371        for i, lab in enumerate(label_list):
1372            pbtmp.update(i)
1373            if lab > 0:
1374                for frame in self.epicure.seglayer.data:
1375                    if lab in frame:
1376                        labs, num_objects = ndlabel(binary_dilation(frame==lab, footprint=s), structure=s)
1377                        if num_objects > 1:
1378                            ut.show_warning("! Problem, label "+str(lab)+" found several times")
1379                            found = found + 1
1380                            continue
1381        pbtmp.close()
1382        if found <= 0:
1383            ut.show_info("Labels unicity ok")
1384
1385    ###############
1386    ## Resetting
1387
1388    def reset_all( self ):
1389        """ Reset labels through skeletonization, reset tracks, suspects, groups """
1390        if self.epicure.verbose > 0:
1391            ut.show_info( "Resetting everything ")
1392        self.viewer.window._status_bar._toggle_activity_dock(True)
1393        progress_bar = progress(total=5)
1394        ## get skeleton and relabel (ensure label unicity)
1395        progress_bar.update(1)
1396        progress_bar.set_description("Reset: relabel")
1397        self.epicure.reset_data()
1398        self.epicure.tracking.reset()
1399        self.epicure.reset_labels()
1400        progress_bar.update(2)
1401        progress_bar.set_description("Reset: reinit tracks")
1402        self.epicure.tracked = 0
1403        self.epicure.load_tracks(progress_bar)
1404        if self.epicure.verbose > 0:
1405            print("Resetting done")
1406        progress_bar.close()
1407        self.viewer.window._status_bar._toggle_activity_dock(False)
1408
1409
1410
1411    ######################################
1412    ## Selection options
1413
1414    def create_selectBlock(self):
1415        """ GUI for handling selection with shapes """
1416        select_layout = wid.vlayout()
1417        ## create/select the ROI
1418        draw_btn = wid.add_button( btn="Draw/Select ROI", btn_func=self.draw_shape, descr="Draw or select a ROI to apply region action on" )
1419        select_layout.addWidget(draw_btn)
1420        remove_sel_btn = wid.add_button( btn="Remove cells inside ROI", btn_func=self.remove_cells_inside, descr="Remove all cells inside the selected/first ROI" )
1421        select_layout.addWidget(remove_sel_btn)
1422        remove_line, self.keep_new_cells = wid.button_check_line( btn="Remove cells outside ROI", btn_func=self.remove_cells_outside, check="Keep new cells", checked=True, checkfunc=None, descr_btn="Remove all cells outside the current ROI", descr_check="Keep new cells tah appear in the ROI in later frames" )
1423        select_layout.addLayout(remove_line)
1424
1425        self.gSelect.setLayout(select_layout)
1426
1427    def draw_shape(self):
1428        """ Draw/select a shape in the Shapes layer """
1429        if self.shapelayer_name not in self.viewer.layers:
1430            self.create_shapelayer()
1431        ut.set_active_layer(self.viewer, self.shapelayer_name)
1432        lay = self.viewer.layers[self.shapelayer_name]
1433        lay.visible = True
1434        lay.opacity = 0.5
1435
1436    def get_selection(self):
1437        """ Get the active (or first) selection """
1438        if self.shapelayer_name not in self.viewer.layers:
1439            return None
1440        lay = self.viewer.layers[self.shapelayer_name]
1441        selected = lay.selected_data
1442        if len(selected) == 0:
1443            if len(lay.shape_type) == 1:
1444                if self.epicure.verbose > 1:
1445                    print("No shape selected, use the only one present")
1446                lay.selected_data.add(0)
1447                selected = lay.selected_data
1448            else:
1449                ut.show_warning("No shape selected, do nothing")
1450                return None
1451        return lay.data[list(selected)[0]] 
1452
1453    def get_labels_inside(self):
1454        """ Get the list of labels inside the current ROI """
1455        current_shape = self.get_selection()
1456        if current_shape is None:
1457            return None
1458        self.current_bbox = ut.getBBox2DFromPts(current_shape, 30, self.epicure.imgshape2D)
1459        self.current_cropshape = ut.positionsIn2DBBox(current_shape, self.current_bbox )
1460        tframe = ut.current_frame(self.viewer)
1461        segt = self.epicure.seglayer.data[tframe]
1462        croped = ut.cropBBox2D(segt, self.current_bbox)
1463        labprops = ut.labels_properties(croped)
1464        inside = points_in_poly( [lab.centroid for lab in labprops], self.current_cropshape )
1465        toedit = [lab.label for i, lab in enumerate(labprops) if inside[i] ]
1466        return toedit
1467
1468    def remove_cells_outside(self):
1469        """ Remove all labels centroids outside the selected ROI """
1470        tokeep = self.get_labels_inside()
1471        if self.keep_new_cells.isChecked():
1472            tframe = ut.current_frame(self.viewer)
1473            segt = self.epicure.seglayer.data[tframe]
1474            toremove = set(np.unique(segt).flatten()) - set(tokeep)
1475            self.epicure.remove_labels(list(toremove))
1476        else:
1477            self.epicure.keep_labels(tokeep)
1478        lay = self.viewer.layers[self.shapelayer_name]
1479        lay.remove_selected()
1480        self.epicure.finish_update()
1481
1482    def remove_cells_inside(self):
1483        """ Remove all labels centroids inside the selected ROI """
1484        toremove = self.get_labels_inside()
1485        self.epicure.remove_labels(toremove)
1486        lay = self.viewer.layers[self.shapelayer_name]
1487        lay.remove_selected()
1488        self.epicure.finish_update()
1489
1490    def lock_cells_inside(self):
1491        """ Check all cells inside the selected ROI into current group """
1492        tocheck = self.get_labels_inside()
1493        for lab in tocheck:
1494            self.check_label(lab)
1495        if self.epicure.verbose > 0:
1496            print(str(len(tocheck))+" cells checked in group "+str(self.check_group.text()))
1497        lay = self.viewer.layers[self.shapelayer_name]
1498        lay.remove_selected()
1499        self.epicure.finish_update()
1500
1501    def group_classify_intensity( self ):
1502        """ Calls the interface to classify cells by intensity """
1503        self.classif.update()
1504        self.classif.show()
1505    
1506    def group_classify_event( self ):
1507        """ Calls the interface to classify cells by event interaction """
1508        self.classif_event.update()
1509        self.classif_event.show()
1510
1511    def group_event_cells( self, event_type ):
1512        """ Classify the cells that finished with the selected event into the event group """
1513        events = self.epicure.inspecting.get_events_from_type( event_type )
1514        if len( events ) > 0:
1515            tids = []
1516            for evt_sid in events:
1517                pos, label = self.epicure.inspecting.get_event_infos( evt_sid )
1518                if label not in tids:
1519                    tids.append(label)
1520            group_name = "Cells_"+event_type
1521            if event_type == "extrusion":
1522                group_name = "Extruding"
1523            if event_type == "division":    
1524                group_name = "Dividing"
1525            self.group_choice.setCurrentText(group_name)
1526            self.epicure.reset_group( group_name ) 
1527            self.redraw_clear_group( group_name )
1528            self.group_labels( tids )
1529
1530
1531    def group_positive_cells( self, layer_name, meth, min_frame, max_frame, threshold ):
1532        """ Classify the cells with mean intensity in the given frame range above threshold into the current group """
1533        if self.group_choice.currentText() == "":
1534            ut.show_warning("Write a group name before")
1535            return
1536        layer = self.viewer.layers[layer_name]
1537        frames = np.arange(min_frame, max_frame+1)
1538        if (min_frame == 0) and (max_frame == self.epicure.nframes-1):
1539            frames = None
1540        tracks, mean_int = self.epicure.tracking.measure_intensity_features( "intensity_"+meth, intimg=layer.data, frames=frames )
1541        tids = tracks[ mean_int > threshold ]
1542        self.redraw_clear_group( group=None )
1543        self.group_labels( tids )
1544
1545    def group_cells_inside(self):
1546        """ Put all cells inside the selected ROI into current group """
1547        if self.group_choice.currentText() == "":
1548            ut.show_warning("Write a group name before")
1549            return
1550        tocheck = self.get_labels_inside()
1551        if tocheck is None:
1552            if self.epicure.verbose > 0:
1553                print("No cell to add to group")
1554            return
1555        self.group_labels( tocheck )
1556        if self.epicure.verbose > 0:
1557            print(str(len(tocheck))+" cells assigend to group "+str(self.group_choice.currentText()))
1558        lay = self.viewer.layers[self.shapelayer_name]
1559        lay.remove_selected()
1560        self.epicure.finish_update()
1561
1562
1563    ######################################
1564    ## Group cells functions
1565    def create_groupCellsBlock(self):
1566        """ Create subpanel of Cell group options """
1567        group_layout = wid.vlayout()
1568        groupgr, self.group_choice = wid.list_line( label="Group name", descr="Choose/Set the current group name" )
1569        group_layout.addLayout(groupgr)
1570        self.group_choice.setEditable(True)
1571
1572        self.group_show = wid.add_check( check="Show groups", checked=False, check_func=self.see_groups, descr="Add a layer with the cells colored by group" )
1573        group_layout.addWidget(self.group_show)
1574
1575        reset_line, self.reset_list = wid.button_list( btn="Reset group", func=self.reset_group, descr="Remove chosen group (or all) and cell assignation to this group" )
1576        group_layout.addLayout( reset_line )
1577        self.update_group_lists()
1578        group_sel_btn = wid.add_button( btn="Cells inside ROI to group", btn_func=self.group_cells_inside, descr="Add all cells inside ROI to the current group" )
1579        group_layout.addWidget(group_sel_btn)
1580
1581        ## add button for intensity classifier interface
1582        group_class_btn = wid.add_button( btn="Group from track intensity..", btn_func=self.group_classify_intensity, descr="Open interface to group cells based on their mean intensity" )
1583        group_layout.addWidget( group_class_btn )
1584        
1585        ## add button for events classifier interface
1586        group_event_btn = wid.add_button( btn="Group from events..", btn_func=self.group_classify_event, descr="Open interface to group cells according to if they are related to an event (dividing cell, extruding cell..)" )
1587        group_layout.addWidget( group_event_btn )
1588
1589        self.gGroup.setLayout(group_layout)
1590
1591    def load_checked(self):
1592        cfile = self.get_filename("_checked.txt")
1593        with open(cfile) as infile:
1594            labels = infile.read().split(";")
1595        for lab in labels:
1596            self.check_load_label(lab)
1597        ut.show_info("Checked cells loaded")
1598
1599    def reset_group( self ):
1600        gr = self.reset_list.currentText()
1601        if gr != "All":
1602            self.redraw_clear_group( gr )
1603        self.epicure.reset_group( gr )
1604        if gr == "All":
1605            self.see_groups()
1606    
1607    def update_group_choice( self, group ):
1608        """ Check if group has been added in the list choices of group """
1609        if self.group_choice.findText( group ) < 0:
1610            ## not added yet. If user is typing the name and did not press enter, it can be still in edition mode, so not added
1611            self.group_choice.addItem( group )
1612    
1613    def update_group_lists( self ):
1614        """ Update list of groups for reset button """
1615        curchoice = self.group_choice.currentText()
1616        curreset = self.reset_list.currentText()
1617        self.group_choice.clear()
1618        self.reset_list.clear()
1619        self.reset_list.addItem("All")
1620        for group in self.epicure.groups.keys():
1621            self.update_group_choice( group )
1622            self.reset_list.addItem( group )
1623        self.reset_list.setCurrentText("All")
1624        if self.reset_list.findText( curreset ) >= 0:
1625            self.reset_list.setCurrentText(curreset)
1626        if self.group_choice.findText( curchoice ) >= 0:
1627            self.group_choice.setCurrentText( curchoice )
1628
1629    def save_groups(self):
1630        groupfile = self.get_filename("_groups.txt")
1631        with open(groupfile, 'w') as out:
1632            out.write(";".join(group.write_group() for group in self.epicure.groups))
1633        ut.show_info("Cell groups saved in "+groupfile)
1634
1635    def see_groups(self):
1636        if self.group_show.isChecked():
1637            ut.remove_layer(self.viewer, self.grouplayer_name)
1638            grouped = self.epicure.draw_groups()
1639            self.viewer.add_labels(grouped, name=self.grouplayer_name, opacity=0.75, blending="additive", scale=self.viewer.layers["Segmentation"].scale)
1640            ut.set_active_layer(self.viewer, "Segmentation")
1641        else:
1642            ut.remove_layer(self.viewer, self.grouplayer_name)
1643            ut.set_active_layer(self.viewer, "Segmentation")
1644    
1645    def group_labels( self, labels ):
1646        """ Add label(s) to group """
1647        if self.group_choice.currentText() == "":
1648            ut.show_warning("Write group name before")
1649            return
1650        group = self.group_choice.currentText()
1651        self.group_ingroup( labels, group )
1652       
1653    def check_label(self, label):
1654        """ Mark label as checked """
1655        group = self.check_group.text()
1656        self.check_ingroup(label, group)
1657
1658        
1659    def group_ingroup(self, labels, group):
1660        """ Add the given label to chosen group """
1661        self.epicure.cells_ingroup( labels, group )
1662        if self.grouplayer_name in self.viewer.layers:
1663            self.redraw_label_group( labels, group )
1664       
1665    def check_load_label(self, labelstr):
1666        """ Read the label to check from file """
1667        res = labelstr.split("-")
1668        cellgroup = res[0]
1669        celllabel = int(res[1])
1670        self.check_ingroup(celllabel, cellgroup)
1671        
1672    def add_cell_to_group(self, event):
1673        """ Add cell under click to the current group """
1674        label = ut.getCellValue( self.epicure.seglayer, event ) 
1675        self.group_labels( [label] )
1676
1677    def remove_cell_group(self, event):
1678        """ Remove the cell from the group it's in if any """
1679        label = ut.getCellValue( self.epicure.seglayer, event ) 
1680        self.epicure.cell_removegroup( label )
1681        if self.grouplayer_name in self.viewer.layers:
1682            self.redraw_label_group( [label], 0 )
1683
1684    def redraw_clear_group( self, group=None ):
1685        """ Clear all the cells from group in the current group layer """
1686        if group is None:
1687            if self.group_choice.currentText() == "":
1688                ut.show_warning("Write group name before")
1689                return
1690            group = self.group_choice.currentText()
1691        if self.grouplayer_name in self.viewer.layers:
1692            lay = self.viewer.layers[self.grouplayer_name]
1693            igroup = self.epicure.get_group_index(group) + 1
1694            if igroup == 0:
1695                ## the group was not present, igroup is -1
1696                return
1697            lay.data[lay.data == igroup] = 0
1698            lay.refresh()
1699            ut.set_active_layer(self.viewer, "Segmentation")
1700
1701    def redraw_label_group(self, labels, group):
1702        """ Update the Group layer for label """
1703        lay = self.viewer.layers[self.grouplayer_name]
1704        if group == 0:
1705            lay.data[ np.isin( self.epicure.seg, labels ) ] = 0
1706        else:
1707            igroup = self.epicure.get_group_index(group) + 1
1708            lay.data[ np.isin( self.epicure.seg, labels)  ] = igroup
1709        lay.refresh()
1710
1711    ######### overlay message
1712    def add_overlay_message(self):
1713        text = self.epicure.text + "\n"
1714        ut.setOverlayText(self.viewer, text, size=10)
1715
1716    ################## Events editing functions
1717    def add_extrusion( self, labela, frame ):
1718        """ Add an extrusion event, given the label and frame """
1719
1720        if (frame != self.epicure.tracking.get_last_frame( labela )):
1721            if self.epicure.verbose > 0:
1722                print("Clicked label is not the last of the track, don't add extrusion")
1723                return
1724
1725        ## add extrusion to event list (if active)
1726        self.epicure.inspecting.add_extrusion( labela, frame )
1727
1728    def add_division( self, labela, labelb, frame ):
1729        """ Add a division event, given the labels of the two daughter cells """
1730        if frame == 0:
1731            if self.epicure.verbose > 0:
1732                print("Cannot define a division before the first frame")
1733            return False
1734
1735        if (frame != self.epicure.tracking.get_first_frame( labela )) or (frame != self.epicure.tracking.get_first_frame(labelb) ):
1736            if self.epicure.verbose > 0:
1737                print("One daughter track is not starting at current frame, don't add division")
1738                return False
1739
1740        ## merge the two labels to find their parent
1741        bbox, merge = ut.getBBox2DMerge( self.epicure.seglayer.data[frame], labela, labelb )
1742        twoframes = ut.crop_twoframes( self.epicure.seglayer.data, bbox, frame )
1743        crop_merge = ut.cropBBox2D( merge, bbox )
1744        twoframes[1] = crop_merge # merge of the labels and 0 outside
1745            
1746        ## keep only parent labels that stop at the previous frame
1747        twoframes = self.keep_orphans(twoframes, frame)
1748        ## do mini-tracking to assign most likely parent
1749        parent = self.get_parents( twoframes, [1] )
1750        if self.epicure.verbose > 0:
1751            print( "Found parent "+str(parent[0])+" to clicked cells "+str(labela)+" and "+str(labelb) )
1752        ## add division to graph
1753        if parent is not None and parent[0] is not None:
1754            self.epicure.tracking.add_division( labela, labelb, parent[0] )
1755            ## add division to event list (if active)
1756            self.epicure.inspecting.add_division_event( labela, labelb, parent[0], frame )
1757            return True
1758        return False
1759            
1760    ################## Track editing functions
1761    def key_tracking_binding(self):
1762        """ active key bindings for tracking options """
1763        self.epicure.overtext["trackedit"] = "---- Track editing ---- \n"
1764        strack = self.epicure.shortcuts["Tracks"]
1765        etrack = self.epicure.shortcuts["Events"]
1766        self.epicure.overtext["trackedit"] += ut.print_shortcuts( strack )
1767        
1768        @self.epicure.seglayer.mouse_drag_callbacks.append
1769        def manual_add_extrusion(layer, event):
1770            ### add an event of an extrusion under the click
1771            if ut.shortcut_click_match( etrack["add extrusion"], event ):
1772                # get the start and last labels
1773                labela = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1774                tframe = int(event.position[0])
1775                    
1776                if labela == 0:
1777                    if self.epicure.verbose > 0:
1778                        print("Clicked position is not a cell, do nothing")
1779                    return
1780                self.add_extrusion( labela, tframe )
1781        
1782        @self.epicure.seglayer.mouse_drag_callbacks.append
1783        def manual_add_division(layer, event):
1784            ### add an event of a division, selecting the two daughter cells
1785            if ut.shortcut_click_match( etrack["add division"], event ):
1786                # get the start and last labels
1787                labela = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1788                start_pos = event.position
1789                yield
1790                while event.type == 'mouse_move':
1791                    yield
1792                labelb = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1793                end_pos = event.position
1794                tframe = int(event.position[0])
1795                    
1796                if labela == 0 or labelb == 0:
1797                    if self.epicure.verbose > 0:
1798                        print("One position is not a cell, do nothing")
1799                    return
1800                self.add_division( labela, labelb, tframe )
1801        
1802        @self.epicure.seglayer.bind_key( strack["lineage color"]["key"], overwrite=True )
1803        def color_tracks_lineage(seglayer):
1804            if self.tracklayer_name in self.viewer.layers:
1805                self.epicure.tracking.color_tracks_by_lineage()
1806        
1807        @self.epicure.seglayer.bind_key( strack["show"]["key"], overwrite=True )
1808        def see_tracks(seglayer):
1809            if self.tracklayer_name in self.viewer.layers:
1810                tlayer = self.viewer.layers[self.tracklayer_name]
1811                tlayer.visible = not tlayer.visible
1812
1813        @self.epicure.seglayer.bind_key( strack["mode"]["key"], overwrite=True)
1814        def edit_track(layer):
1815            self.label_tr = None 
1816            self.start_label = None
1817            self.interp_labela = None
1818            self.interp_labelb = None
1819            ut.show_info("Tracks editing mode")
1820            self.old_mouse_drag, self.old_key_map = ut.clear_bindings(self.epicure.seglayer)
1821
1822            @self.epicure.seglayer.mouse_drag_callbacks.append
1823            def click(layer, event):
1824                """ Edit tracking """
1825                if event.type == "mouse_press":
1826                  
1827                    """ Merge two tracks, spatially or temporally: left click, select the first label """
1828                    if ut.shortcut_click_match( strack["merge first"], event ):
1829                        self.start_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1830                        self.start_pos = event.position
1831                        # move one frame after for next cell to link
1832                        #ut.set_frame( self.epicure.viewer, event.position[0]+1 )
1833                        return
1834                    """ Merge two tracks, spatially or temporally: right click, select the second label """
1835                    if ut.shortcut_click_match( strack["merge second"], event ):
1836                        if self.start_label is None:
1837                            if self.epicure.verbose > 0:
1838                                print("No left click done before right click, don't merge anything")
1839                            return
1840                        end_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1841                        end_pos = event.position
1842                        if self.epicure.verbose > 0:
1843                            print("Merging track "+str(self.start_label)+" with track "+str(end_label))
1844                        
1845                        if self.start_label is None or self.start_label == 0 or end_label == 0:
1846                            if self.epicure.verbose > 0:
1847                                print("One position is not a cell, do nothing")
1848                            return
1849                        ## ready, merge
1850                        self.merge_tracks( self.start_label, self.start_pos, end_label, end_pos )
1851                        self.end_track_edit()
1852                        return
1853
1854                    ### Split the track in 2: new label for the next frames 
1855                    if ut.shortcut_click_match( strack["split track"], event ):
1856                        start_frame = int(event.position[0])
1857                        label = ut.getCellValue(self.epicure.seglayer, event) 
1858                        self.epicure.split_track( label, start_frame )
1859                        self.end_track_edit()
1860                        return
1861                        
1862                    ### Swap the two track from the current frame 
1863                    if ut.shortcut_click_match( strack["swap"], event ):
1864                        start_frame = int(event.position[0])
1865                        label = ut.getCellValue(self.epicure.seglayer, event) 
1866                        yield
1867                        while event.type == 'mouse_move':
1868                            yield
1869                        end_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)                           
1870                            
1871                        if label == 0 or end_label == 0:
1872                            if self.epicure.verbose > 0:
1873                                print("One position is not a cell, do nothing")
1874                            return
1875
1876                        self.epicure.swap_tracks( label, end_label, start_frame )
1877                            
1878                        if self.epicure.verbose > 0:
1879                            ut.show_info("Swapped track "+str(label)+" with track "+str(end_label)+" from frame "+str(start_frame))
1880                        self.end_track_edit()
1881                        return
1882
1883                    # Manual tracking: get a new label and spread it to clicked cells on next frames
1884                    if ut.shortcut_click_match( strack["start manual"], event ):
1885                        zpos = int(event.position[0])
1886                        if self.label_tr is None:
1887                            ## first click: get the track label
1888                            self.label_tr = ut.getCellValue(self.epicure.seglayer, event) 
1889                        else:
1890                            old_label = ut.setCellValue(self.epicure.seglayer, self.epicure.seglayer, event, self.label_tr, layer_frame=zpos, label_frame=zpos)
1891                            self.epicure.tracking.remove_one_frame( old_label, zpos, handle_gaps=self.epicure.forbid_gaps )
1892                            self.epicure.add_label( [self.label_tr], zpos )
1893                        ## advance to next frame, ready for a click
1894                        self.viewer.dims.set_point(0, zpos+1)
1895                        ## if reach the end, stops here for this track
1896                        if (zpos+1) >= self.epicure.seglayer.data.shape[0]:
1897                            self.end_track_edit()
1898                        return
1899                    
1900                    ## Finish manual tracking
1901                    if ut.shortcut_click_match( strack["end manual"], event ):
1902                        self.end_track_edit()
1903                        return
1904                   
1905                    ## Interpolate between two labels: get first label
1906                    if ut.shortcut_click_match( strack["interpolate first"], event ):
1907                        ## left click, first cell
1908                        self.interp_labela = ut.getCellValue(self.epicure.seglayer, event) 
1909                        self.interp_framea = int(event.position[0])
1910                        return
1911                    
1912                    ## Interpolate between two labels: get second label and interpolate
1913                    if ut.shortcut_click_match( strack["interpolate second"], event ):
1914                        ## right click, second cell
1915                        labelb = ut.getCellValue(self.epicure.seglayer, event) 
1916                        interp_frameb = int(event.position[0])
1917                        if self.interp_labela is not None:
1918                            if abs(self.interp_framea - interp_frameb) <= 1:
1919                                print("No frames to interpolate, exit")
1920                                self.end_track_edit()
1921                                return
1922                            if self.interp_framea < interp_frameb:
1923                                self.interpolate_labels(self.interp_labela, self.interp_framea, labelb, interp_frameb)
1924                            else:
1925                                self.interpolate_labels(labelb, interp_frameb, self.interp_labela, self.interp_framea )
1926                            self.end_track_edit()
1927                            return
1928                        else:
1929                            print("No cell selected with left click before. Exit mode")
1930                            self.end_track_edit()
1931                            return
1932                        
1933                    ## Delete all the labels of the track until its end
1934                    if ut.shortcut_click_match( strack["delete"], event ):
1935                        tframe = int(event.position[0])
1936                        label = ut.getCellValue(self.epicure.seglayer, event)
1937                        if label > 0:
1938                            self.epicure.replace_label( label, 0, tframe )
1939                            if self.epicure.verbose > 0:
1940                                print("Track "+str(label)+" deleted from frame "+str(tframe))
1941                        self.end_track_edit()
1942                        return
1943
1944                ## A right click or other click stops it
1945                self.end_track_edit()
1946
1947            #@self.epicure.seglayer.mouse_double_click_callbacks.append
1948            #def double_click(layer, event):
1949            #    """ Edit tracking : double click options """
1950            #    if event.type == "mouse_double_click":      
1951                    
1952        
1953            @self.epicure.seglayer.bind_key( strack["mode"]["key"], overwrite=True )
1954            def end_edit_track(layer):
1955                self.end_track_edit()
1956
1957    def end_track_edit(self):
1958        self.start_label = None
1959        self.interp_labela = None
1960        self.interp_labelb = None
1961        ut.reactive_bindings( self.epicure.seglayer, self.old_mouse_drag, self.old_key_map )
1962        ut.show_info("End track edit mode")
1963
1964    def merge_tracks(self, labela, posa, labelb, posb):
1965        """ 
1966            Merge track with label a with track of label b, temporally or spatially 
1967        """
1968        if labela == labelb:
1969            if self.epicure.verbose > 0:
1970                print("Already the same track" )
1971                return
1972        if int(posb[0]) == int(posa[0]):
1973            self.tracks_spatial_merging( labela, posa, labelb )
1974        else:
1975            self.tracks_temporal_merging( labela, posa, labelb, posb )
1976
1977    def tracks_spatial_merging( self, labela, posa, labelb ):
1978        """ Merge spatially two tracks: labels have to be touching all along the common frames """
1979        start_time = ut.start_time()
1980        ## get last common frame
1981        lasta = self.epicure.tracking.get_last_frame( labela )
1982        lastb = self.epicure.tracking.get_last_frame( labelb )
1983        lastcommon = min(lasta, lastb)
1984
1985        ## if longer than the last common, split the label(s) that continue
1986        if lasta > lastcommon:
1987            if self.epicure.tracking.get_first_frame( labela ) < int(posa[0]):
1988                self.epicure.split_track( labela, lastcommon+1 )
1989        if lastb > lastcommon:
1990            if self.epicure.tracking.get_first_frame( labelb ) < int(posa[0]):
1991                self.epicure.split_track( labelb, lastcommon+1 )
1992
1993        ## Looks, ok, create a new track and merge the two tracks in it
1994        new_label = self.epicure.get_free_label()
1995        new_labels = []
1996        ind_tomodif = None
1997        footprint = disk(radius=3)
1998        for frame in range( int(posa[0]), lastcommon+1 ):
1999            bbox, merged = ut.getBBox2DMerge( self.epicure.seg[frame], labela, labelb )
2000            bbox = ut.extendBBox2D( bbox, 1.05, self.epicure.imgshape2D )
2001            
2002            ## check if labels are touching at each frame
2003            segt_crop = ut.cropBBox2D( self.epicure.seg[frame], bbox )
2004            touched = ut.checkTouchingLabels( segt_crop, labela, labelb )
2005            if not touched:
2006                print("Labels "+str(labela)+" and "+str(labelb)+" are not always touching. Refusing to merge them")
2007                return 
2008            
2009            ## merge the two labels together
2010            joinlab = ut.cropBBox2D( merged, bbox )
2011            joinlab = new_label * binary_closing(joinlab, footprint)
2012           
2013            ## get the index and new values to change
2014            indmodif = ut.ind_boundaries( joinlab )
2015            #indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2016            new_labels = new_labels + [0]*len(indmodif)
2017            curmodif = np.transpose( np.nonzero( joinlab == new_label ) )
2018            new_labels = new_labels + [new_label]*len(curmodif)
2019            indmodif = np.vstack((indmodif, curmodif))
2020            indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2021            if ind_tomodif is None:
2022                ind_tomodif = indmodif
2023            else:
2024                ind_tomodif = np.vstack((ind_tomodif, indmodif))
2025            #ind_tomodif = np.vstack((ind_tomodif, curmodif))
2026        
2027        ## update the labels and the tracks
2028        self.epicure.change_labels_frommerge( ind_tomodif, new_labels, remove_labels=[labela, labelb] )
2029        if self.epicure.verbose > 0:
2030            ut.show_info("Merged spatially "+str(labela)+" with "+str(labelb)+" from frame "+str(int(posa[0]))+" to frame "+str(lastcommon)+"\n New track label is "+str(new_label))
2031        if self.epicure.verbose > 1:
2032            ut.show_duration(start_time, "Merging spatially tracks in ")
2033
2034
2035    def tracks_temporal_merging( self, labela, posa, labelb, posb ):
2036        """ 
2037        Merge track with label a with track of label b if consecutives frames. 
2038        It does not check if label are close in distance, assume it is.
2039        """
2040
2041        if self.epicure.forbid_gaps:
2042            if abs(int(posb[0]) - int(posa[0])) != 1:
2043                if self.epicure.verbose > 0:
2044                    print("Frames to merge are not consecutives, refused")
2045                return
2046
2047        ## If frame b is before frame a, swap so that a is first 
2048        if posa[0] > posb[0]:
2049            posc = np.copy(posa)
2050            posa = posb
2051            posb = posc
2052            labelc = labela
2053            labela = labelb
2054            labelb = labelc
2055
2056        ## Check that posa is last frame of label a and pos b first frame of label b
2057        if int(posa[0]) != self.epicure.tracking.get_last_frame( labela ):
2058            if self.epicure.verbose > 0:
2059                print("Clicked label "+str(labela)+" at frame "+str(posa[0])+" was not the last frame of the track -> splitting it")
2060            self.epicure.split_track( labela, int(posa[0])+1 )
2061
2062        if posb[0] != self.epicure.tracking.get_first_frame( labelb ):
2063            if self.epicure.verbose > 0:
2064                print("Clicked label "+str(labelb)+" at frame "+str(posb[0])+" is not the first frame of the track -> splitting it")
2065            labelb = self.epicure.split_track( labelb, int(posb[0]) )
2066
2067        self.epicure.replace_label( labelb, labela, int(posb[0]) )
2068        
2069
2070    def get_parents(self, twoframes, labels):
2071        """ Get parent of all labels """
2072        return self.epicure.tracking.find_parents( labels, twoframes )
2073    
2074    def get_position_label_2D(self, img, labels, parent_labels):
2075        """ Get position of each label to update with parent label """
2076        indmodif = None
2077        new_labels = []
2078        ## get possible free labels, to be sure that it will not take the same ones
2079        free_labels = self.epicure.get_free_labels(len(labels))
2080        for i, lab in enumerate(labels):
2081            parent_label = parent_labels[i]
2082            if parent_label is None:
2083                parent_label = free_labels[i]
2084                parent_labels[i] = parent_label
2085            curmodif = np.argwhere( img==lab )
2086            if indmodif is None:
2087                indmodif = curmodif
2088            else:
2089                indmodif = np.vstack((indmodif, curmodif))
2090            new_labels = new_labels + ([parent_label]*curmodif.shape[0])
2091        return indmodif, new_labels, parent_labels
2092
2093    def keep_orphans( self, img, frame, keep_labels=[]):
2094        """ Keep only labels that doesn't have a follower (track is finishing at that frame) """
2095        ## remove the labels to track
2096        labs = np.unique(img[0]).tolist() #np.setdiff1d( img[0], labels ).tolist()
2097        if 0 in labs:
2098            labs.remove(0)
2099        ## Check that it's not present at current frame
2100        torem = [ lab for lab in labs if (lab not in keep_labels) and (self.epicure.tracking.is_in_frame( lab, frame ) ) ]
2101        if len(torem) == 0:
2102            return img
2103        mask = np.isin(img[0], torem)
2104        img[0][mask] = 0
2105        return img
2106
2107    def inherit_parent_labels(self, myframe, labels, bbox, frame, keep_labels):
2108        """ Get parent labels if any and indices to modify with it """
2109        if ( self.epicure.tracked == 0 ) or (frame<=0):
2110            parent_labels = [None]*len(labels)
2111            indmodif, new_labels, parent_labels = self.get_position_label_2D(myframe, labels, parent_labels)
2112        else:
2113            twoframes = ut.crop_twoframes( self.epicure.seglayer.data, bbox, frame )
2114            twoframes[1] = np.copy(myframe) # merge of the labels and 0 outside
2115            twoframes = self.keep_orphans( twoframes, frame, keep_labels=keep_labels)
2116            
2117            parent_labels = self.get_parents( twoframes, labels )
2118        
2119            indmodif, new_labels, parent_labels = self.get_position_label_2D(twoframes[1], labels, parent_labels)
2120
2121        if self.epicure.verbose > 0:
2122            print("Set value (from parent or new): "+str(np.unique(new_labels)))
2123        ## back to movie position
2124        indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2125        return indmodif, new_labels, parent_labels
2126    
2127    def inherit_child_labels(self, myframe, labels, bbox, frame, parent_labels, keep_labels):
2128        """ Get child labels if any and indices to modify with it """
2129        if (self.epicure.tracked == 0 ) or (frame>=self.epicure.nframes-1):
2130            return [], []
2131        else:
2132            twoframes = np.copy( ut.cropBBox2D(self.epicure.seglayer.data[frame+1], bbox) )
2133            ## check if the new value to set is present in the following frame, in that case don't do any propagation
2134            for par in parent_labels:
2135                if np.any( twoframes==par ):
2136                    if self.epicure.verbose > 1:
2137                        print("Propagating: not because new value present in labels: "+str(par))
2138                    return [], []
2139
2140            twoframes = np.stack( (twoframes, np.copy(myframe)) )
2141            twoframes = self.keep_orphans(twoframes, frame, keep_labels=keep_labels)
2142            child_labels = self.get_parents( twoframes, labels )
2143            
2144            if self.epicure.verbose > 0:
2145                print("Propagate  the new value to: "+str(child_labels))
2146            if child_labels is None:
2147                return [], []
2148        
2149        # get position of each child label to update with current label
2150        indmodif = []
2151        new_labels = []
2152        for i, lab in enumerate(child_labels):
2153            if lab is not None:
2154                if lab == parent_labels[i]:
2155                    ## going to propagate to itself, no need
2156                    continue
2157                after_frame = frame+1
2158                last_frame = self.epicure.tracking.get_last_frame( parent_labels[i] )
2159                if (last_frame is not None) and (last_frame >= after_frame):
2160                    ## the label to propagate is present somewhere after the current frame
2161                    self.epicure.split_track( parent_labels[i], after_frame )
2162                inds = self.epicure.get_label_indexes( lab, after_frame )
2163                if len(indmodif) == 0:
2164                    indmodif = inds
2165                else:
2166                    indmodif = np.vstack((indmodif, inds))
2167                new_labels = new_labels + np.repeat(parent_labels[i], len(inds)).tolist()
2168        return indmodif, new_labels
2169
2170    def propagate_label_change(self, myframe, labels, bbox, frame, keep_labels):
2171        """ Propagate the new labelling to match parent/child labels """
2172        start_time = ut.start_time()
2173        indmodif = ut.ind_boundaries( myframe )
2174        indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2175        #ut.show_info("Boundaries in "+"{:.3f}".format((time.time()-start_time)/60)+" min")
2176        new_labels = np.repeat(0, len(indmodif)).tolist()
2177
2178        ## get parent labels if any for each label
2179        indmodif2, new_labels2, parent_labels = self.inherit_parent_labels(myframe, labels, bbox, frame, keep_labels)
2180        if indmodif2 is not None:
2181            indmodif = np.vstack((indmodif, indmodif2))
2182            new_labels = new_labels+new_labels2
2183        if self.epicure.verbose > 1:
2184            ut.show_duration(start_time, "Propagation, parents found, ")
2185
2186        ## propagate the change: get child labels if any for each label
2187        indmodif_child, new_labels_child = self.inherit_child_labels(myframe, labels, bbox, frame, parent_labels, keep_labels)
2188        if len(indmodif_child) > 0:
2189            indmodif = np.vstack((indmodif, indmodif_child))
2190            new_labels = new_labels + new_labels_child
2191        if self.epicure.verbose > 1:
2192            ut.show_duration(start_time, "Propagation, childs found, ")
2193        
2194        ## go, do the update
2195        self.epicure.change_labels(indmodif, new_labels)
2196
2197    ############# Test
2198    def interpolate_labels( self, labela, framea, labelb, frameb ):
2199        """ 
2200            Interpolate the label shape in between two labels 
2201            Based on signed distance transform, like Fiji ROIs interpolation
2202        """
2203        if self.epicure.verbose > 1:
2204            print("Interpolating between "+str(labela)+" and "+str(labelb))
2205            print("From frame "+str(framea)+" to frame "+str(frameb))
2206            start_time = ut.start_time()
2207        
2208        sega = self.epicure.seglayer.data[framea]
2209        maska = np.isin( sega, [labela] )
2210        segb = self.epicure.seglayer.data[frameb]
2211        maskb = np.isin( segb, [labelb] )
2212
2213        ## get merged bounding box, and crop around it
2214        mask = maska | maskb
2215        props = ut.labels_properties(mask*1)
2216        bbox = ut.extendBBox2D( props[0].bbox, extend_factor=1.2, imshape=mask.shape )
2217
2218        maska = ut.cropBBox2D( maska, bbox )
2219        maskb = ut.cropBBox2D( maskb, bbox )
2220
2221        ## get signed distance transform of each label
2222        dista = edt.sdf( maska )
2223        distb = edt.sdf( maskb )
2224
2225        inds = None
2226        new_labels = []
2227        for frame in range(framea+1, frameb):
2228            p = (frame-framea)/(frameb-framea)
2229            dist = (1-p) * dista + p * distb
2230            ## change only pixels that are 0
2231            frame_crop = ut.cropBBox2D( self.epicure.seglayer.data[frame], bbox )
2232            tochange = binary_dilation(dist>0, footprint=disk(radius=2)) * (frame_crop<=0)   # expand to touch neighbor label
2233            
2234            ## indexes and new values to change
2235            indmodif = np.argwhere( tochange > 0 ).tolist()
2236            indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2237            if inds is None:
2238                inds = indmodif
2239            else:
2240                inds = np.vstack( (inds, indmodif) )
2241            new_labels = new_labels + [labela]*len(indmodif)
2242
2243            ## be sure to remove the boundaries with neighbor labels
2244            bound_ind = ut.ind_boundaries( tochange )
2245            new_labels = new_labels + [0]*len(bound_ind)
2246            bound_ind = ut.toFullMoviePos( bound_ind, bbox, frame )
2247            inds = np.vstack( (inds, bound_ind) )
2248
2249        ## Go, apply the changes
2250        self.epicure.change_labels( inds, new_labels )
2251        ## change the second track to first track value
2252        self.epicure.replace_label( labelb, labela, frameb )
2253        if self.epicure.verbose > 1:
2254            ut.show_duration( start_time, "Interpolation took " )
2255        if self.epicure.verbose > 0:
2256            ut.show_info( "Interpolated label "+str(labela)+" from frame "+str(framea+1)+" to "+str(frameb-1) )
2257
2258        
2259
2260
2261
2262class ClassifyIntensity( QWidget ):
2263    """ Interface to group cells based on their mean intensity """
2264    def __init__( self, edit ):
2265        super().__init__()
2266        self.edit = edit
2267        poplayout = wid.vlayout()
2268
2269        ## Show in which group cells will be added
2270        self.group_name = wid.label_line( "Positive cells will be added to group: "+str(self.edit.group_choice.currentText() ) )
2271        poplayout.addWidget( self.group_name )
2272
2273        ## Choose the intensity layer
2274        line, self.layer_choice = wid.list_line( label="Measure intensity from: ", descr="Choose the layer to use for intensity classification" )
2275        for lay in self.edit.viewer.layers:
2276            if lay.name in [ "Events", "Tracks", "ROIs" ]:
2277                continue
2278            self.layer_choice.addItem( lay.name )
2279            print(lay.name)
2280        poplayout.addLayout( line )
2281
2282        ## Choose the method to use for intensity measurement
2283        method_line, self.method_choice = wid.list_line( label="Method to measure intensity along track: ", descr="Choose the method to measure intensity" )
2284        meths = ["mean", "median", "max", "min", "sum"]
2285        for meth in meths:
2286            self.method_choice.addItem( meth)        
2287        poplayout.addLayout( method_line )
2288
2289        ## Choose frames to use for classification 
2290        frame_lab = wid.label_line( "Measure intensity on frame(s):" )
2291        min_frame_line, self.min_frame = wid.ranged_value_line( label="From frame: ", descr="First frame to use for classification", minval=0, maxval=self.edit.epicure.nframes-1, step=1, val=0 )
2292        poplayout.addLayout( min_frame_line )
2293        max_frame_line, self.max_frame = wid.ranged_value_line( label="To frame: ", descr="Last frame to use for classification", minval=0, maxval=self.edit.epicure.nframes-1, step=1, val=self.edit.epicure.nframes-1 )
2294        poplayout.addLayout( max_frame_line )
2295
2296        ## Choose the threshold for classification
2297        thres_line, self.threshold = wid.value_line( label="Track intensity threshold: ", default_value="100", descr="Threshold of measured intensity of a track to be considered as positive" )
2298        poplayout.addLayout( thres_line )
2299
2300        go_btn = wid.add_button( "Add positive cells to group", self.classify, "Start the classification of positive cells" )
2301        poplayout.addWidget( go_btn )
2302
2303        self.setLayout( poplayout )
2304
2305    def update( self ):
2306        """ Update the parameters with current GUI state """
2307        self.group_name.setText( "Positive cells will be added to group: "+str(self.edit.group_choice.currentText() ) )
2308        self.layer_choice.clear()
2309        for lay in self.edit.viewer.layers:
2310            if lay.name in [ "Events", "Tracks", "ROIs" ]:
2311                continue
2312            self.layer_choice.addItem( lay.name )
2313
2314    def classify( self ):
2315        self.edit.group_positive_cells( self.layer_choice.currentText(), self.method_choice.currentText(), self.min_frame.value(), self.max_frame.value(), float(self.threshold.text()) )
2316
2317class ClassifyEvent( QWidget ):
2318    """ Interface to group cells based on their interaction with an event (dividing or extruding cells) """
2319
2320    def __init__( self, edit ):
2321        super().__init__()
2322        self.edit = edit
2323        poplayout = wid.vlayout()
2324
2325        ## Choose the event to use
2326        line, self.event_choice = wid.list_line( label="Select cells that ends with: ", descr="Choose the event to use to select the cells" )
2327        for evt in self.edit.epicure.event_class:
2328            self.event_choice.addItem( evt )
2329        poplayout.addLayout( line )
2330
2331        go_btn = wid.add_button( "Add selected cells to new group", self.classify, "Start the classification of cells" )
2332        poplayout.addWidget( go_btn )
2333
2334        self.setLayout( poplayout )
2335
2336    def classify( self ):
2337        """ Add all the cell that finish with the selected event to the group """
2338        self.edit.group_event_cells( self.event_choice.currentText() )
class Editing(PyQt6.QtWidgets.QWidget):
  24class Editing( QWidget ):
  25    """ Handle user interaction to edit the segmentation """
  26
  27    def __init__(self, napari_viewer, epic):
  28        """ Initialize the Edit panel interface """
  29        super().__init__()
  30        self.viewer = napari_viewer
  31        self.epicure = epic
  32        self.old_mouse_drag = None
  33        self.tracklayer_name = "Tracks"
  34        self.shapelayer_name = "ROIs"
  35        self.grouplayer_name = "Groups"
  36        self.updated_labels = None   ## keep which labels are being edited
  37        self.seed_active = False ## if place seed option is on
  38
  39        layout = wid.vlayout()
  40        
  41        ## Option to use default napari painting options
  42        #self.napari_painting = wid.add_check( "Default Napari painting tools (no checks)", checked=False, check_func=self.painting_tools, descr="Use the label painting of Napari instead of customized EpiCure ones (will not perform any sanity check)" )
  43        #layout.addWidget( self.napari_painting )
  44
  45        ## Option to remove all border cells
  46        clean_line, self.clean_vis, self.gCleaned = wid.checkgroup_help( name="Cleaning options", checked=False, descr="Show/hide options to clean the segmentation", help_link="Edit#cleaning-options", display_settings=self.epicure.display_colors, groupnb="group" )
  47        layout.addLayout(clean_line)
  48        self.create_cleaningBlock()
  49        layout.addWidget(self.gCleaned)
  50        self.gCleaned.hide()
  51
  52        ## handle grouping cells into categories
  53        group_line, self.group_vis, self.gGroup = wid.checkgroup_help( name="Cell group options", checked=False, descr="Show/hide options to define cell groups", help_link="Edit#group-options", display_settings=self.epicure.display_colors, groupnb="group2"  )
  54        layout.addLayout(group_line)
  55        self.create_groupCellsBlock()
  56        layout.addWidget(self.gGroup)
  57        self.gGroup.hide()
  58        
  59        ## Selection option: crop, remove cells
  60        select_line, self.select_vis, self.gSelect = wid.checkgroup_help( name="ROI options", checked=False, descr="Show/hide options to work on Regions", help_link="Edit#roi-options", display_settings=self.epicure.display_colors, groupnb="group3" )
  61        layout.addLayout(select_line)
  62        self.create_selectBlock()
  63        layout.addWidget(self.gSelect)
  64        self.gSelect.hide()
  65        
  66        ## Put seeds and do watershed from it
  67        seed_line, self.seed_vis, self.gSeed = wid.checkgroup_help( name="Seeds options", checked=False, descr="Show/hide options to segment from seeds", help_link="Edit#seeds-options", display_settings=self.epicure.display_colors, groupnb="group4" )
  68        layout.addLayout(seed_line)
  69        self.create_seedsBlock()
  70        layout.addWidget(self.gSeed)
  71        self.gSeed.hide()
  72        
  73        self.setLayout(layout)
  74        
  75        ## interface done, ready to work 
  76        self.create_shapelayer()
  77        self.modify_cells()
  78        self.key_tracking_binding()
  79        self.add_overlay_message()
  80
  81        ## catch filling/painting operations
  82        self.napari_fill = self.epicure.seglayer.fill
  83        self.epicure.seglayer.fill = self.epicure_fill
  84        self.napari_paint = self.epicure.seglayer.paint
  85        self.epicure.seglayer.paint = self.lazy #self.epicure_paint
  86        ### scale and radius for paiting
  87        self.paint_scale = np.array([self.epicure.seglayer.scale[i+1] for i in range(2)], dtype=float)
  88        self.epicure.seglayer.events.brush_size.connect( self.paint_radius )
  89        self.paint_radius()
  90        self.disk_one = disk(radius=1)
  91        self.classif = ClassifyIntensity( self )
  92        self.classif_event = ClassifyEvent( self )
  93        self.scalexy = self.epicure.epi_metadata["ScaleXY"]
  94
  95    def painting_tools( self ):
  96        """ Choose which painting tools should be activated """
  97        if self.napari_painting.isChecked():
  98            self.epicure.seglayer.fill = self.napari_fill
  99            self.epicure.seglayer.paint = self.napari_paint
 100        else:
 101            self.epicure.seglayer.fill = self.epicure_fill
 102            self.epicure.seglayer.paint = self.lazy
 103
 104
 105    def apply_settings( self, settings ):
 106        """ Load the prefered settings for Edit panel """
 107        for setting, val in settings.items():
 108            if setting == "Show group option":
 109                self.group_vis.setChecked( val )
 110            if setting == "Show clean option":
 111                self.clean_vis.setChecked( val )
 112            if setting ==  "Show ROI option":
 113                self.select_vis.setChecked( val )
 114            if setting == "Show seed option":
 115                self.seed_vis.setChecked( val )
 116            if setting == "Show groups":
 117                self.group_show.setChecked( val )
 118            if setting == "Border size":
 119                self.border_size.setText( val )
 120            if setting == "Seed method":
 121                self.seed_method.setCurrentText( val )
 122            if setting == "Seed max cell":
 123                self.max_distance.setText( val )
 124           
 125
 126    def get_current_settings( self ):
 127        """ Returns the current state of the Edit widget """
 128        setting = {}
 129        setting["Show group option"] = self.group_vis.isChecked()
 130        setting["Show clean option"] = self.clean_vis.isChecked()
 131        setting["Show ROI option"] = self.select_vis.isChecked()
 132        setting["Show seed option"] = self.seed_vis.isChecked()
 133        setting["Show groups"] = self.group_show.isChecked()
 134        setting["Border size"] = self.border_size.text()
 135        setting["Seed method"] = self.seed_method.currentText()
 136        setting["Seed max cell"] = self.max_distance.text()
 137        return setting
 138   
 139    def paint_radius( self ):
 140        """ Update painitng radius with brush size """
 141        self.radius = np.floor(self.epicure.seglayer.brush_size / 2) + 0.5
 142        self.brush_indices = sphere_indices(self.radius, tuple(self.paint_scale)) 
 143
 144    def setParent(self, epy):
 145        self.epicure = epy
 146
 147    def get_filename(self, endname):
 148        return ut.get_filename(self.epicure.outdir, self.epicure.imgname+endname )
 149        
 150    def get_values(self, coord):
 151        """ Get the label value under coord, the current frame, prepare the coords """
 152        int_coord = tuple(np.round(coord).astype(int))
 153        tframe = int(coord[0])
 154        segdata = self.epicure.seglayer.data[tframe]
 155        int_coord = int_coord[1:3]
 156        # get value of the label that will be painted over
 157        prev_label = int(segdata[int_coord])
 158        return int_coord, tframe, segdata, prev_label
 159
 160    ### Get fill or paint action and assure compatibility with structure
 161    def epicure_fill(self, coord, new_label, refresh=True):
 162        """ Check if the filled cell is already registered """
 163        if new_label == 0:
 164            if self.epicure.verbose > 0:
 165                ut.show_warning("Fill with 0 (background) not allowed \n Use Eraser tool (press <1>) to erase")
 166                return
 167        int_coord, tframe, segdata, prev_label = self.get_values( coord )
 168
 169        hascell = self.epicure.has_label( new_label )
 170        if hascell:
 171            ## already present, check that it is at the same place
 172            ## label before
 173            mask_before = segdata==new_label
 174            if np.sum(mask_before) <= 0:
 175                ut.show_warning("Label "+str(new_label)+" is already used in other frames. Choose another label")
 176                return
 177        
 178        ## if try to fill an empty zone, ensure that it doesn't fill the skeletons
 179        if prev_label == 0:
 180            skel = ut.frame_to_skeleton( segdata )
 181            skel_fill = max(np.max(segdata)+2, new_label+1)
 182            segdata[skel] = skel_fill
 183            skel = None
 184            
 185        if hascell:
 186            # if contiguous replace only selected connected component, calculate how it would be changed
 187            matches = (segdata == prev_label)
 188            labeled_matches, num_features = label(matches, return_num=True)
 189            if num_features != 1:
 190                match_label = labeled_matches[int_coord]
 191                matches = np.logical_and( matches, labeled_matches == match_label )
 192           
 193            # check if touch the already present cell
 194            ok = self.touching_masks(mask_before, matches)
 195            if not ok:
 196                ut.show_warning("Label "+str(new_label)+" added do not touch already present cell. Choose another label or draw contiguously")
 197                ## reset if necessary
 198                if prev_label == 0:
 199                    segdata[segdata==skel_fill] = 0  ## put skeleton back to 0
 200                return
 201            ut.setNewLabel( self.epicure.seglayer, (np.argwhere(matches)).tolist(), new_label, add_frame=tframe )
 202            if prev_label == 0:
 203                segdata[skel] = 0  ## put skeleton back to 0
 204        else:
 205            ## new cell, add it to the tracks list
 206            self.napari_fill(coord, new_label, refresh=True)
 207            if prev_label == 0:
 208                segdata[segdata==skel_fill] = 0  ## put skeleton back to 0
 209                ut.remove_boundaries(segdata)
 210            self.epicure.add_label(new_label, tframe)
 211        
 212        ## Finish filling step to ensure everything's fine
 213        self.epicure.seglayer.refresh()
 214        ## put the active mode of the layer back to the zoom one
 215        self.epicure.seglayer.mode = "pan_zoom"
 216        if prev_label != 0: 
 217            self.epicure.tracking.remove_one_frame( [prev_label], tframe, handle_gaps=self.epicure.forbid_gaps )
 218
 219    def lazy( self, coord, new_label, refresh=True ):
 220        return
 221
 222    def epicure_paint( self, coords, new_label, tframe, hascell ):
 223        """ Edit a label with paint tool, with several pixels at once """
 224        mask_indices = None
 225        ## convert the coords with brush size, check that is fully inside
 226        for coord in coords:
 227            int_coord = np.array( np.round(coord).astype(int)[1:3] ) 
 228            for brush in self.brush_indices:
 229                pt = int_coord + brush
 230                if ut.inside_bounds( pt, self.epicure.imgshape2D ):
 231                    if mask_indices is None:
 232                        mask_indices = pt
 233                    else:
 234                        mask_indices = np.vstack( ( mask_indices, pt ) )
 235        
 236        ## crop around part of the image to update
 237        bbox = ut.getBBoxFromPts( mask_indices, extend=0, imshape=self.epicure.imgshape2D )
 238        if hascell:
 239            ## extend around points a lot if the label is there already to avoid cutting it
 240            extend = 4
 241        else:
 242            extend = 1.5
 243        bbox = ut.extendBBox2D( bbox, extend_factor=extend, imshape=self.epicure.imgshape2D )
 244        cropdata = ut.cropBBox2D( self.epicure.seglayer.data[tframe], bbox )
 245        crop_indices = ut.positions2DIn2DBBox( mask_indices, bbox )
 246        
 247        ## get previous data before painting
 248        prev_labels = np.unique( cropdata[ tuple(np.array(crop_indices).T) ] ).tolist()
 249        if 0 in prev_labels:
 250            prev_labels.remove(0)
 251
 252        if new_label > 0:    
 253            if hascell:
 254                ## check that label is in current frame
 255                mask_before = cropdata==new_label
 256                if not np.isin(1, mask_before):
 257                    ut.show_warning("Label "+str(new_label)+" is already used in other frames. Choose another label")
 258                    return
 259
 260                ## already present, check that it is at the same place
 261                #### Test if painting touch previous label
 262                mask_after = np.zeros(cropdata.shape)
 263                mask_after[ tuple(np.array(crop_indices).T) ] = 1
 264                ok = self.touching_masks(mask_before, mask_after)
 265                if not ok:
 266                    ut.show_warning("Label "+str(new_label)+" added do not touch already present cell. Choose another label or draw contiguously")
 267                    return
 268            else:
 269                ## drawing new cell, fill it at the end
 270                if self.epicure.verbose > 2:
 271                    print("Painting a new cell")
 272
 273        ## Paint and update everything    
 274        painted = np.copy(cropdata)
 275        painted[ tuple(np.array(crop_indices).T) ] = new_label
 276        if new_label > 0:
 277            if self.epicure.seglayer.preserve_labels:
 278                painted = painted*(np.isin( cropdata, [0, new_label] ))
 279                painted = binary_fill_holes( (painted==new_label) )
 280                ## remove one-pixel thick lines
 281                painted = binary_opening( painted )
 282                crop_indices = np.argwhere( (painted>0) )
 283            else:
 284                painted = binary_fill_holes( painted==new_label )
 285                crop_indices = np.argwhere(painted>0)    
 286        ### if preseve label is on, there can be nothing left to paint
 287        if len(crop_indices) <= 0:
 288            return
 289        mask_indices = ut.toFullMoviePos( crop_indices, bbox, tframe )
 290        new_labels = np.repeat(new_label, len(mask_indices)).tolist()
 291
 292        ## Update label boundaries if necessary
 293        cind_bound = ut.ind_boundaries( painted )
 294        if self.epicure.seglayer.preserve_labels:
 295            ind_bound = [ ind for ind in cind_bound if (cropdata[tuple(ind)] == new_label) ]
 296        else:
 297            ind_bound = [ ind for ind in cind_bound if cropdata[tuple(ind)] in prev_labels ]
 298        if (new_label>0) and (len( ind_bound ) > 0):
 299            bound_ind = ut.toFullMoviePos( ind_bound, bbox, tframe )
 300            bound_labels = np.repeat(0, len(bound_ind)).tolist()
 301            mask_indices = np.vstack( (mask_indices, bound_ind) )
 302            new_labels = new_labels + bound_labels
 303
 304        ## Go, apply the change, and update the tracks
 305        self.epicure.change_labels( mask_indices, new_labels )
 306
 307    def create_cell_from_line( self, tframe, positions ):
 308        """ Create new cell(s) from drawn line (junction) """
 309        bbox = ut.getBBox2DFromPts( positions, extend=0, imshape=self.epicure.imgshape2D )
 310        bbox = ut.extendBBox2D( bbox, extend_factor=2, imshape=self.epicure.imgshape2D )
 311
 312        segt = self.epicure.seglayer.data[tframe]
 313        cropt = ut.cropBBox2D( segt, bbox )
 314        crop_positions = ut.positionsIn2DBBox( positions, bbox )
 315
 316        line = np.zeros(cropt.shape, dtype="uint8")
 317        ## fill the already filled pixels by other labels
 318        line[ cropt > 0 ] = 1
 319        ## expand from one pixel to fill the junction
 320        line = binary_dilation( line )
 321        ## fill the interpolated line
 322        for i, pos in enumerate(crop_positions):
 323            if cropt[round(pos[0]), round(pos[1])] == 0:
 324                line[round(pos[0]), round(pos[1])] = 1
 325            if (i > 0):
 326                prev = (crop_positions[i-1][0], crop_positions[i-1][1])
 327                cur = (pos[0], pos[1])
 328                interp_coords = interpolate_coordinates(prev, cur, 1)
 329                for ic in interp_coords:
 330                    line[tuple(np.round(ic).astype(int))] = 1
 331        
 332        ## close the junction gaps, and the line eventually
 333        line = binary_closing( line )
 334        new_cells, nlabels = label( line, background=1, return_num=True, connectivity=1 )
 335        ## no new cell to create
 336        if nlabels <= 0:
 337            return
 338        ## get the new labels to relabel and add as new cells
 339        labels = list( set( new_cells.flatten() ) )
 340        if 0 in labels:
 341            labels.remove(0)
 342       
 343        ## try to get new cell labels from previous and next slices
 344        parents = [None]*len(labels)
 345        if tframe > 0:
 346            twoframes = ut.crop_twoframes( self.epicure.seglayer.data, bbox, tframe )
 347            twoframes[1] = new_cells
 348            twoframes = self.keep_orphans( twoframes, tframe )
 349            parents = self.get_parents( twoframes, labels )
 350        childs = [None]*len(labels)
 351        if tframe < (self.epicure.nframes-1):
 352            twoframes = np.copy( ut.cropBBox2D(self.epicure.seglayer.data[tframe+1], bbox) )
 353            twoframes = np.stack( (twoframes, np.copy(new_cells)) )
 354            twoframes = self.keep_orphans( twoframes, tframe )
 355            childs = self.get_parents( twoframes, labels )
 356        
 357        free_labels = self.epicure.get_free_labels( nlabels )  
 358        torelink = []
 359        for i in range( len(labels) ):
 360            if (parents[i] is not None) and (childs[i] is not None):
 361                free_labels[i] = parents[i]
 362                if self.epicure.verbose > 0:
 363                    print("Link new cell with previous/next "+str(free_labels[i]))
 364                #if childs[i] != parents[i]:
 365                #    torelink.append( [free_labels[i], childs[i]] )
 366            ## only one link found, take it
 367            if (parents[i] is not None) and (childs[i] is None):
 368                free_labels[i] = parents[i]
 369                if self.epicure.verbose > 0:
 370                    print("Link new cell with previous/next "+str(free_labels[i]))
 371            if (parents[i] is None) and (childs[i] is not None):
 372                free_labels[i] = childs[i]
 373                if self.epicure.verbose > 0:
 374                    print("Link new cell with previous/next "+str(free_labels[i]))
 375
 376        print("Added cells "+str(free_labels))
 377
 378        ## get the new indices and labels to draw
 379        new_labels = []
 380        indices = None
 381        for i, lab in enumerate( labels ):
 382            curindices = np.argwhere( new_cells == lab )
 383            if indices is None:
 384                indices = curindices
 385            else:
 386                indices = np.vstack((indices, curindices))
 387            new_labels = new_labels + ([free_labels[i]]*curindices.shape[0])    
 388        
 389        ## add the label boundary
 390        indbound = ut.ind_boundaries( new_cells )
 391        indices = np.vstack( (indices, indbound) )
 392        new_labels = new_labels + np.repeat( 0, len(indbound) ).tolist()
 393        indices = ut.toFullMoviePos( indices, bbox, tframe )
 394        self.epicure.change_labels( indices, new_labels )
 395
 396        ## relink child tracks if necessary
 397        #for relink in torelink:
 398        #    self.epicure.replace_label( relink[1], relink[0], tframe )
 399        
 400    def touching_masks(self, maska, maskb):
 401        """ Check if the two mask touch """
 402        maska = binary_dilation(maska, footprint=self.disk_one)
 403        return np.sum(np.logical_and(maska, maskb))>0
 404    
 405    def touching_indices(self, maska, indices):
 406        """ Check if the indices touch the mask """
 407        maska = binary_dilation(maska, footprint=self.disk_one)
 408        return np.isin(1, maska[indices]) > 0
 409
 410
 411    ## Merging/splitting cells functions
 412    def modify_cells(self):
 413        sl = self.epicure.shortcuts["Labels"]
 414        self.epicure.overtext["labels"] = "---- Labels editing ---- \n"
 415        self.epicure.overtext["labels"] += ut.print_shortcuts( sl )
 416        
 417        sgroup = self.epicure.shortcuts["Groups"]
 418        self.epicure.overtext["grouped"] = "---- Group cells ---- \n"
 419        self.epicure.overtext["grouped"] += ut.print_shortcuts( sgroup )
 420        
 421        sseed = self.epicure.shortcuts["Seeds"]
 422        self.epicure.overtext["seed"] = "---- Seed options --- \n"
 423        self.epicure.overtext["seed"] += ut.print_shortcuts( sseed )
 424
 425        @self.epicure.seglayer.mouse_drag_callbacks.append
 426        def set_checked(layer, event):
 427            if event.type == "mouse_press":
 428                if (event.button == 1) and (len(event.modifiers) == 0):
 429                    if layer.mode == "paint": 
 430                        #and not self.napari_painting.isChecked():
 431                        ### Overwrite the painting to check that everything stays within EpiCure constraints
 432                        if self.shapelayer_name not in self.viewer.layers:
 433                            self.create_shapelayer()
 434                        shape_lay = self.viewer.layers[self.shapelayer_name]
 435                        shape_lay.mode = "add_path"
 436                        shape_lay.visible = True
 437                        @thread_worker
 438                        def refresh_image():                       
 439                            shape_lay.refresh()
 440                            return
 441                        pos = np.array( [shape_lay.world_to_data(event.position)] )
 442                        yield
 443                        ## record all the successives position of the mouse while clicked
 444                        iter = 0
 445                        while (event.type == 'mouse_move'): # and (len(pos)<200):
 446                            pos = np.vstack( (pos, np.array(shape_lay.world_to_data(event.position))) )
 447                            if iter == 5:
 448                                shape_lay.data = pos
 449                                shape_lay.shape_type = "path"
 450                                refresh_image()
 451                                #shape_lay.refresh()
 452                                iter = 0
 453                            iter = iter + 1
 454                            yield
 455                        pos = np.vstack( (pos, np.array(shape_lay.world_to_data(event.position))) )    
 456                        tframe = int( pos[0][0] )
 457                        ## painting a new or extending a cell
 458                        new_label = layer.selected_label
 459                        hascell = None
 460                        if new_label > 0:
 461                            hascell = self.epicure.has_label( new_label )
 462                        ## paint the selected pixels following EpiCure constraints
 463                        self.epicure_paint( pos, new_label, tframe, hascell )
 464                        shape_lay.data = []
 465                        shape_lay.refresh()
 466                        shape_lay.visible = False
 467
 468        @self.epicure.seglayer.mouse_drag_callbacks.append
 469        def set_checked(layer, event):
 470            if event.type == "mouse_press":
 471                if ut.shortcut_click_match( sgroup["add group"], event ):
 472                    if self.group_choice.currentText() == "":
 473                        ut.show_warning("Write a group name before")
 474                        return
 475                    if self.epicure.verbose > 0:
 476                        print("Mark cell in group "+self.group_choice.currentText())
 477                    self.add_cell_to_group(event)
 478                    return
 479                
 480                if ut.shortcut_click_match( sgroup["remove group"], event ):
 481                    if self.epicure.verbose > 0:
 482                        print("Remove cell from its group")
 483                    self.remove_cell_group(event)
 484                    return
 485
 486        @self.epicure.seglayer.bind_key("Control-z", overwrite=False)
 487        def undo_operations(seglayer):
 488            if self.epicure.verbose > 0:
 489                print("Undo previous action")
 490            img_before = np.copy(self.epicure.seg)
 491            self.epicure.seglayer.undo()
 492            self.epicure.update_changed_labels_img( img_before, self.epicure.seglayer.data )
 493
 494        @self.epicure.seglayer.bind_key( sl["unused paint"]["key"], overwrite=True )
 495        def set_nextlabel(layer):
 496            lab = self.epicure.get_free_label()
 497            ut.show_info( "Unused label "+": "+str(lab) )
 498            ut.set_label(layer, lab)
 499        
 500        @self.epicure.seglayer.bind_key( sl["unused fill"]["key"], overwrite=True )
 501        def set_nextlabel_paint(layer):
 502            lab = self.epicure.get_free_label()
 503            ut.show_info( "Unused label "+": "+str(lab) )
 504            ut.set_label(layer, lab)
 505            layer.mode = "FILL"
 506        
 507        @self.epicure.seglayer.bind_key( sl["swap mode"]["key"], overwrite=True )
 508        def key_swap(layer):
 509            """ Active key bindings for label swapping options """
 510            ut.show_info("Begin swap mode: Control and click to swap two labels")
 511            self.old_mouse_drag, self.old_key_map = ut.clear_bindings( self.epicure.seglayer )
 512
 513            @self.epicure.seglayer.mouse_drag_callbacks.append
 514            def click(layer, event):
 515                """ Swap the labels from first to last position of the pressed mouse """
 516                if event.type == "mouse_press":
 517                    if len(event.modifiers) > 0:
 518                        start_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 519                        start_pos = event.position
 520                        yield
 521                        while event.type == 'mouse_move':
 522                            yield
 523                        end_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 524                        end_pos = event.position
 525                        tframe = int(event.position[0])
 526                    
 527                        if start_label == 0 or end_label == 0:
 528                            if self.epicure.verbose > 0:
 529                                print("One position is not a cell, do nothing")
 530                            return
 531
 532                        if (event.button == 1) and ("Control" in event.modifiers):
 533                            # Left-click: swap labels at each end of the click
 534                            if self.epicure.verbose > 0:
 535                                print("Swap cell "+str(start_label)+" and "+str(end_label))
 536                            self.swap_labels(tframe, start_label, end_label)
 537                    
 538                ut.reactive_bindings( self.epicure.seglayer, self.old_mouse_drag, self.old_key_map )
 539                ut.show_info("End swap")
 540        
 541        @self.epicure.seglayer.bind_key( sseed["new seed"]["key"], overwrite=True )
 542        def place_seed(layer):
 543            if self.seed_active:
 544                ## if option is currently on, stop it
 545                self.end_place_seed()
 546                return
 547            if "Seeds" not in self.viewer.layers:
 548                self.create_seedlayer()
 549                ut.set_active_layer( self.viewer, "Segmentation" )
 550            ## desactivate other click-binding
 551            self.old_mouse_drag = self.epicure.seglayer.mouse_drag_callbacks.copy()
 552            self.epicure.seglayer.mouse_drag_callbacks = []
 553            self.seed_active = True
 554            ut.show_info("Left-click to place a new seed")
 555
 556            @self.epicure.seglayer.mouse_drag_callbacks.append
 557            def click(layer, event):
 558                if (event.type == "mouse_press") and (len(event.modifiers)==0) and (event.button==1):
 559                    ## single left-click place a seed
 560                    if "Seeds" not in self.viewer.layers:
 561                        self.reset_seeds()
 562                    self.place_seed(event.position)
 563                else:
 564                    self.end_place_seed()
 565
 566        @self.epicure.seglayer.bind_key( sl["draw junction mode"]["key"], overwrite=True )
 567        def manual_junction(layer):
 568            """ Launch the manual drawing junction mode """
 569            self.drawing_junction_mode()
 570
 571        @self.epicure.seglayer.mouse_drag_callbacks.append
 572        def click(layer, event):
 573            if event.type == "mouse_press":
 574                zoom = self.viewer.camera.zoom ## in case a napari shortcut changes the zoom
 575                center = self.viewer.camera.center ## same
 576                ## erase cell option
 577                if ut.shortcut_click_match( sl["erase"], event ):
 578                    # single right-click: erase the cell
 579                    tframe = ut.current_frame(self.viewer)
 580                    erased = ut.setLabelValue(self.epicure.seglayer, self.epicure.seglayer, event, 0, tframe, tframe)
 581                    ## delete also in track data
 582                    if erased is not None:
 583                        self.epicure.delete_track( erased, tframe )
 584                    ut.reset_view( self.viewer, zoom, center )
 585                    return
 586                        
 587                merging = ut.shortcut_click_match( sl["merge"], event )
 588                splitting = ut.shortcut_click_match( sl["split accross"], event )
 589                if merging or splitting:
 590                    # get the start and last labels
 591                    start_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 592                    start_pos = self.epicure.seglayer.world_to_data( event.position )
 593                    yield
 594                    while event.type == 'mouse_move':
 595                        yield
 596                    end_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 597                    end_pos = self.epicure.seglayer.world_to_data( event.position )
 598                    tframe = int(end_pos[0])
 599                    
 600                    if start_label == 0 or end_label == 0:
 601                        if self.epicure.verbose > 0:
 602                            print("One position is not a cell, do nothing")
 603                        ut.reset_view( self.viewer, zoom, center )
 604                        return
 605
 606                    if merging:
 607                        ## Merge labels at each end of the click
 608                        if start_label != end_label:
 609                            if self.epicure.verbose > 0:
 610                                print("Merge cell "+str(start_label)+" with "+str(end_label))
 611                            self.merge_labels(tframe, start_label, end_label)
 612                            ut.reset_view( self.viewer, zoom, center )
 613                            return
 614                    
 615                    if splitting:
 616                        ## split label at each end of the click
 617                        if start_label == end_label:
 618                            if self.epicure.verbose > 0:
 619                                print("Split cell "+str(start_label))
 620                            self.split_label(tframe, start_label, start_pos, end_pos)
 621                            ut.reset_view( self.viewer, zoom, center )
 622                        else:
 623                            if self.epicure.verbose > 0:
 624                                print("Not the same cell already, do nothing")
 625                    ut.reset_view( self.viewer, zoom, center )
 626                    return
 627
 628                drawing_split = ut.shortcut_click_match( sl["split draw"], event )
 629                redrawing = ut.shortcut_click_match( sl["redraw junction"], event )
 630                if drawing_split or redrawing:
 631                    if self.shapelayer_name not in self.viewer.layers:
 632                        self.create_shapelayer()
 633                    shape_lay = self.viewer.layers[self.shapelayer_name]
 634                    shape_lay.mode = "add_path"
 635                    shape_lay.visible = True
 636                    shape_lay.data = []
 637                    scaled_pos = shape_lay.world_to_data(event.position)
 638                    pos = [scaled_pos]
 639                    yield
 640                    ## record all the successives position of the mouse while clicked
 641                    while event.type == 'mouse_move':
 642                        scaled_pos = shape_lay.world_to_data(event.position)
 643                        pos.append( scaled_pos )
 644                        shape_lay.data = np.array( pos )
 645                        shape_lay.shape_type = "path"
 646                        shape_lay.refresh()
 647                        yield
 648                    scaled_pos = shape_lay.world_to_data(event.position)
 649                    pos.append( scaled_pos )
 650                    ut.set_active_layer(self.viewer, "Segmentation")
 651                    tframe = int(event.position[0])
 652                    if redrawing:
 653                        ##  modify junction along the drawn line
 654                        if self.epicure.verbose > 0:
 655                            print("Correct junction with the drawn line ")
 656                        self.redraw_along_line(tframe, pos)
 657                        shape_lay.data = []
 658                        shape_lay.refresh()
 659                        shape_lay.visible = False
 660                        ut.reset_view( self.viewer, zoom, center )
 661                        return
 662                    if drawing_split:
 663                        ## split labels along the drawn line
 664                        if self.epicure.verbose > 0:
 665                            print("Split cell along the drawn line ")
 666                        self.split_along_line(tframe, pos)
 667                        shape_lay.data = []
 668                        shape_lay.refresh()
 669                        shape_lay.visible = False
 670                        ut.reset_view( self.viewer, zoom, center )
 671                        return
 672                    ut.reset_view( self.viewer, zoom, center )
 673                    return
 674        
 675    def drawing_junction_mode( self ):
 676        """ Active mouse bindings for manually drawing the junction, and try to fill defined area """
 677            
 678        sl = self.epicure.shortcuts["Labels"]
 679        ut.show_info("Begin drawing junction: Control-Left-click to draw the junction and create new cell(s) from it")
 680        self.old_mouse_drag, self.old_key_map = ut.clear_bindings( self.epicure.seglayer )
 681        
 682        @self.epicure.seglayer.bind_key( sl["draw junction mode"]["key"], overwrite=True )
 683        def stop_draw_junction_mode( layer ):
 684            ut.reactive_bindings( self.epicure.seglayer, self.old_mouse_drag, self.old_key_map )
 685            ut.show_info("End drawing mode")
 686        
 687        @self.epicure.seglayer.mouse_drag_callbacks.append
 688        def click(layer, event):
 689            if ut.shortcut_click_match( sl["drawing junction"], event ):
 690                shape_lay = self.viewer.layers[self.shapelayer_name]
 691                shape_lay.mode = "add_path"
 692                shape_lay.visible = True
 693                scaled_position = shape_lay.world_to_data( event.position )
 694                pos = [scaled_position]
 695                yield
 696                ## record all the successives position of the mouse while clicked
 697                i = 0
 698                while event.type == 'mouse_move':
 699                    scaled_position = shape_lay.world_to_data( event.position )
 700                    pos.append( scaled_position )
 701                    if i%5 == 0:
 702                        # refresh display every n steps
 703                        shape_lay.data = np.array( pos ) 
 704                        shape_lay.shape_type = "path"
 705                        shape_lay.refresh()
 706                    i = i + 1
 707                    yield
 708                scaled_position = shape_lay.world_to_data( event.position )
 709                pos.append(scaled_position)
 710                ut.set_active_layer(self.viewer, "Segmentation")
 711                tframe = int(event.position[0])
 712                self.create_cell_from_line( tframe, pos )        
 713                shape_lay.data = []
 714                shape_lay.refresh()
 715                shape_lay.visible = False
 716                ut.reactive_bindings( self.epicure.seglayer, self.old_mouse_drag, self.old_key_map )
 717                ut.show_info("End drawing mode")
 718
 719    def split_label(self, tframe, startlab, start_pos, end_pos):
 720        """ Split the label in two cells based on the two seeds """
 721        segt = self.epicure.seglayer.data[tframe]
 722        labelBB = ut.getBBox2D(segt, startlab)
 723        labelBB = ut.extendBBox2D( labelBB, extend_factor=1.25, imshape=self.epicure.imgshape2D )
 724
 725        mov = self.viewer.layers["Movie"].data[tframe]
 726        imgBB = ut.cropBBox2D(mov, labelBB)
 727        segBB = ut.cropBBox2D(segt, labelBB)
 728        maskBB = np.zeros(segBB.shape, dtype="uint8")
 729        maskBB[segBB==startlab] = 1
 730        spos = ut.positionIn2DBBox( start_pos, labelBB )
 731        epos = ut.positionIn2DBBox( end_pos, labelBB )
 732
 733        markers = np.zeros(maskBB.shape, dtype=self.epicure.dtype)
 734        markers[spos] = startlab
 735        markers[epos] = self.epicure.get_free_label()
 736        splitted = watershed( imgBB, markers=markers, mask=maskBB )
 737        if (np.sum(splitted==startlab) < self.epicure.minsize) or (np.sum(splitted==markers[epos]) < self.epicure.minsize):
 738            if self.epicure.verbose > 0:
 739                print("Sorry, split failed, one cell smaller than "+str(self.epicure.minsize)+" pixels")
 740        else:
 741            if len(np.unique(splitted)) > 2:
 742                curframe = np.zeros(segBB.shape, dtype="uint8")
 743                labels = []
 744                for i, splitlab in enumerate(np.unique(splitted)):
 745                    if splitlab > 0:
 746                        curframe[splitted==splitlab] = i+1
 747                        labels.append(i+1)
 748
 749                curframe = ut.remove_boundaries(curframe)
 750                ## apply the split and propagate the label to descendant label
 751                self.propagate_label_change( curframe, labels, labelBB, tframe, [startlab] )
 752            else:
 753                if self.epicure.verbose > 0:
 754                    print("Split failed, no boundary in pixel intensities found")
 755
 756
 757    def redraw_along_line(self, tframe, positions):
 758        """ Redraw the two labels separated by a line drawn manually """
 759        bbox = ut.getBBox2DFromPts( positions, extend=0, imshape=self.epicure.imgshape2D )
 760        #bbox = ut.extendBBox2D( bbox, extend_factor=1.25, imshape=self.epicure.imgshape2D )
 761
 762        segt = self.epicure.seglayer.data[tframe]
 763        cropt = ut.cropBBox2D( segt, bbox )
 764        crop_positions = ut.positionsIn2DBBox( positions, bbox )
 765
 766        # get the value of the cells to update (most frequent label along the line)
 767        curlabels = []
 768        prev_pos = None
 769        # Find closest zero elements in the inverted image (same as closest non-zero for image)
 770        
 771        crop_zeros = distance_transform_edt(cropt, return_distances=False, return_indices=True)
 772
 773        for pos in crop_positions:
 774            if (prev_pos is None) or ((round(pos[0]) != round(prev_pos[0])) and (round(pos[1]) != round(prev_pos[1]) )):
 775                ## find closest pixel that is 0 (on a junction)
 776                juncpoint = crop_zeros[:, round(pos[0]), round(pos[1])]
 777                labs = np.unique( cropt[ (juncpoint[0]-2):(juncpoint[0]+2), (juncpoint[1]-2):(juncpoint[1]+2) ] )
 778                for clab in labs:
 779                    if clab > 0:
 780                        curlabels.append(clab)
 781                prev_pos = pos
 782                
 783        sort_curlabel = sorted(set(curlabels), key=curlabels.count)
 784        ## external junction: only one cell
 785        if len(sort_curlabel) < 2:
 786            if self.epicure.verbose > 0:
 787                print("Only one cell along the junction: can't do it")
 788                return
 789        flabel = sort_curlabel[-1]
 790        slabel = sort_curlabel[-2]
 791        if self.epicure.verbose > 0:
 792            print("Cells to update: "+str(flabel)+" "+str(slabel))
 793        
 794        ## crop around selected label
 795        bbox, _ = ut.getBBox2DMerge( segt, flabel, slabel )
 796        bbox = ut.extendBBox2D( bbox, extend_factor=1.25, imshape=self.epicure.imgshape2D )
 797        init_cropt = ut.cropBBox2D( segt, bbox )
 798        curlabel = flabel
 799        ## merge the two labels together
 800        binlab = np.isin( init_cropt, [flabel, slabel] )*1
 801        footprint = disk(radius=2)
 802        cropt = flabel*binary_closing(binlab, footprint)
 803        crop_positions = ut.positionsIn2DBBox( positions, bbox )
 804
 805        # draw the line only in the cell to split
 806        line = np.zeros(cropt.shape, dtype="uint8")
 807        for i, pos in enumerate(crop_positions):
 808            if cropt[round(pos[0]), round(pos[1])] == curlabel:
 809                line[round(pos[0]), round(pos[1])] = 1
 810            if (i > 0):
 811                prev = (crop_positions[i-1][0], crop_positions[i-1][1])
 812                cur = (pos[0], pos[1])
 813                interp_coords = interpolate_coordinates(prev, cur, 1)
 814                for ic in interp_coords:
 815                    line[tuple(np.round(ic).astype(int))] = 1
 816        self.move_in_crop( curlabel, init_cropt, cropt, crop_positions, line, bbox, tframe, retry=0)
 817    
 818    def move_in_crop(self, curlabel, init_cropt, cropt, crop_positions, line, bbox, frame, retry):
 819        """ Move the junction in the cropped region """
 820        dis = retry
 821        footprint = disk(radius=dis)
 822        dilline = binary_dilation(line, footprint=footprint)
 823
 824        # get the two splitted regions and relabel one of them
 825        clab = np.zeros(cropt.shape, dtype="uint8")
 826        clab[cropt==curlabel] = 1
 827        clab[dilline] = 0
 828        labels = label(clab, background=0, connectivity=1)
 829        if (np.max(labels) == 2) & (np.sum(labels==1)>self.epicure.minsize) & (np.sum(labels==2)>self.epicure.minsize):
 830            ## get new image with the 2 cells to retrack
 831            labels = ut.touching_labels(labels, expand=dis+1)
 832            indmodif = []
 833            newlabels = []
 834            for i in range(2):
 835                imodif = ( (labels==(i+1)) & (cropt==curlabel) )
 836                val, counts = np.unique( init_cropt[ imodif ], return_counts=True) 
 837                init_label = val[np.argmax(counts)]
 838                imodif = np.argwhere(imodif).tolist()
 839                indmodif = indmodif + imodif
 840                newlabels = newlabels + np.repeat( init_label, len(imodif) ).tolist()
 841            
 842            indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
 843            
 844            # remove the boundary between the two updated labels only
 845            cind_bound = ut.ind_boundaries( labels )
 846            ind_bound = [ ind for ind in cind_bound if cropt[tuple(ind)]==curlabel ]
 847            ind_bound = ut.toFullMoviePos( ind_bound, bbox, frame )
 848            indmodif = np.vstack((indmodif, ind_bound))
 849            newlabels = newlabels + np.repeat(0, len(ind_bound)).tolist()
 850            
 851            self.epicure.change_labels( indmodif, newlabels )
 852            ## udpate the centroid of the modified labels
 853            #for clabel in np.unique(newlabels):
 854            #    if clabel > 0:
 855            #        self.epicure.update_centroid( clabel, frame )
 856        else:
 857            if (retry > 6) :
 858                if self.epicure.verbose > 0:
 859                    print("Update failed "+str(np.max(labels)))
 860                return
 861            retry = retry + 1
 862            self.move_in_crop(curlabel, init_cropt, cropt, crop_positions, line, bbox, frame, retry=retry)
 863
 864    def split_along_line(self, tframe, positions):
 865        """ Split a label along a line drawn manually """
 866        bbox = ut.getBBox2DFromPts( positions, extend=0, imshape=self.epicure.imgshape2D )
 867        bbox = ut.extendBBox2D( bbox, extend_factor=1.25, imshape=self.epicure.imgshape2D )
 868
 869        segt = self.epicure.seglayer.data[tframe]
 870        cropt = ut.cropBBox2D( segt, bbox )
 871        crop_positions = ut.positionsIn2DBBox( positions, bbox )
 872
 873        # get the value of the cell to split (most frequent label along the line)
 874        curlabels = []
 875        prev_pos = None
 876        for pos in crop_positions:
 877            if (prev_pos is None) or ((round(pos[0]) != round(prev_pos[0])) and (round(pos[1]) != round(prev_pos[1]) )):
 878                clab = cropt[round(pos[0]), round(pos[1])]
 879                curlabels.append(clab)
 880                prev_pos = pos
 881                
 882        curlabel = max(set(curlabels), key=curlabels.count)
 883        if self.epicure.verbose > 0:
 884            print("Cell to split: "+str(curlabel))
 885        if curlabel == 0:
 886            if self.epicure.verbose > 0:
 887                print("Refusing to split background")
 888            return               
 889                        
 890        ## crop around selected label
 891        bbox = ut.getBBox2D(segt, curlabel)
 892        bbox = ut.extendBBox2D( bbox, extend_factor=1.5, imshape=self.epicure.imgshape2D )
 893        cropt = ut.cropBBox2D( segt, bbox )
 894        crop_positions = ut.positionsIn2DBBox( positions, bbox )
 895
 896        # draw the line only in the cell to split
 897        line = np.zeros(cropt.shape, dtype="uint8")
 898        for i, pos in enumerate(crop_positions):
 899            if cropt[round(pos[0]), round(pos[1])] == curlabel:
 900                line[round(pos[0]), round(pos[1])] = 1
 901            if (i > 0):
 902                prev = (crop_positions[i-1][0], crop_positions[i-1][1])
 903                cur = (pos[0], pos[1])
 904                interp_coords = interpolate_coordinates(prev, cur, 1)
 905                for ic in interp_coords:
 906                    line[tuple(np.round(ic).astype(int))] = 1
 907        self.split_in_crop( curlabel, cropt, crop_positions, line, bbox, tframe, retry=0)
 908
 909    def split_in_crop(self, curlabel, cropt, crop_positions, line, bbox, frame, retry):
 910        """ Find the split to do in the cropped region """
 911        dis = retry
 912        footprint = disk(radius=dis)
 913        dilline = binary_dilation(line, footprint=footprint)
 914
 915        # get the two splitted regions and relabel one of them
 916        clab = np.zeros(cropt.shape, dtype="uint8")
 917        clab[cropt==curlabel] = 1
 918        clab[dilline] = 0
 919        labels = label(clab, background=0, connectivity=1)
 920        if (np.max(labels) == 2) & (np.sum(labels==1)>self.epicure.minsize) & (np.sum(labels==2)>self.epicure.minsize):
 921            ## get new image with the 2 cells to retrack
 922            labels = ut.touching_labels(labels, expand=dis+1)
 923            curframe = np.zeros( cropt.shape, dtype="uint8" )
 924            for i in range(2):
 925                curframe[ (labels==(i+1)) & (cropt==curlabel) ] = i+1
 926            
 927            curframe = ut.remove_boundaries(curframe)
 928            self.propagate_label_change( curframe, [1,2], bbox, frame, [curlabel] )
 929
 930        else:
 931            if (retry > 6) :
 932                if self.epicure.verbose > 0:
 933                    print("Split failed "+str(np.max(labels)))
 934                return
 935            retry = retry + 1
 936            self.split_in_crop(curlabel, cropt, crop_positions, line, bbox, frame, retry=retry)
 937
 938    def merge_labels(self, tframe, startlab, endlab, extend_factor=1.25):
 939        """ Merge the two given labels """
 940        start_time = ut.start_time()
 941        segt = self.epicure.seglayer.data[tframe]
 942        
 943        ## Crop around labels to work on smaller field of view
 944        bbox, merged = ut.getBBox2DMerge( segt, startlab, endlab )
 945        
 946        ## keep only the region of interest
 947        bbox = ut.extendBBox2D( bbox, extend_factor, self.epicure.imgshape2D )
 948        segt_crop = ut.cropBBox2D( segt, bbox )
 949
 950        ## check that labels can be merged
 951        touch = ut.checkTouchingLabels( segt_crop, startlab, endlab )
 952        if not touch:
 953            ut.show_warning("Labels not touching, I refuse to merge them")
 954            return
 955
 956        ## merge the two labels together
 957        joinlab = ut.cropBBox2D( merged, bbox )
 958        footprint = disk(radius=2)
 959        joinlab = endlab * binary_closing(joinlab, footprint)
 960        
 961        if self.epicure.verbose > 1:
 962            ut.show_duration(start_time, "Merged in ")
 963
 964        ## update and propagate the change
 965        self.propagate_label_change(joinlab, [endlab], bbox, tframe, [startlab, endlab])
 966        if self.epicure.verbose > 1:
 967            ut.show_duration(start_time, "Merged and propagated in ")
 968
 969    def touching_labels(self, img, lab, olab):
 970        """ Check if the two labels are neighbors or not """
 971        flab = find_boundaries(img==lab)
 972        folab = find_boundaries(img==olab)
 973        return np.sum(np.logical_and(flab, folab))>0
 974    
 975    def swap_labels(self, tframe, lab, olab):
 976        """ Swap two labels """
 977        segt = self.epicure.seglayer.data[tframe]
 978        ## Get the two labels position to swap
 979        modiflab = np.argwhere(segt==lab).tolist()
 980        modifolab = np.argwhere(segt==olab).tolist()
 981        newlabs = np.repeat(olab, len(modiflab)).tolist() + np.repeat(lab, len(modifolab)).tolist()
 982        ## Change the labels
 983        ut.setNewLabel( self.epicure.seglayer, modiflab+modifolab, newlabs, add_frame=tframe )
 984        ## Update the tracks and graph with swap
 985        self.epicure.swap_labels( lab, olab, tframe )
 986        self.epicure.seglayer.refresh()
 987
 988
 989    ######################
 990    ## Erase border cells
 991    def remove_border(self):
 992        """ Remove all cells that touch the border """
 993        start_time = ut.start_time()
 994        self.viewer.window._status_bar._toggle_activity_dock(True)
 995        size = int(self.border_size.text())
 996        if size == 0:
 997            for i in progress(range(0, self.epicure.nframes)):
 998                img = np.copy( self.epicure.seglayer.data[i] )
 999                resimg = clear_border( img )
1000                self.epicure.seglayer.data[i] = resimg
1001                self.epicure.removed_labels( img, resimg, i )
1002        else:
1003            maxx = self.epicure.imgshape2D[0] - size - 1
1004            maxy = self.epicure.imgshape2D[1] - size - 1
1005            for i in progress(range(0, self.epicure.nframes)):
1006                frame = self.epicure.seglayer.data[i]
1007                img = np.copy( frame ) 
1008                crop_img = img[ size:maxx, size:maxy ]
1009                crop_img = clear_border( crop_img )
1010                frame[0:size, :] = 0
1011                frame[:, 0:size] = 0
1012                frame[maxx:, :] = 0
1013                frame[:, maxy:] = 0
1014                frame[size:maxx, size:maxy] = crop_img
1015                ## update the tracks after the potential disappearance of some cells
1016                self.epicure.removed_labels( img, frame, i )
1017        
1018        self.viewer.window._status_bar._toggle_activity_dock(False)
1019        self.epicure.seglayer.refresh()
1020        if self.epicure.verbose > 0:
1021            ut.show_duration( start_time, "Border cells removed in ")
1022
1023               
1024
1025    def remove_smalls( self ):
1026        """ Remove all cells smaller than given area (in nb pixels) """
1027        start_time = ut.start_time()
1028        self.viewer.window._status_bar._toggle_activity_dock(True)
1029        for i in progress(range(0, self.epicure.nframes)):
1030            self.remove_small_cells( np.copy(self.epicure.seglayer.data[i]), i)
1031        self.viewer.window._status_bar._toggle_activity_dock(False)
1032        if self.epicure.verbose > 0:
1033            ut.show_duration( start_time, "Small cells removed in ")
1034
1035    def remove_small_cells(self, img, frame):
1036        """ Remove if few the cell is only few pixels """
1037        #init_labels = set(np.unique(img))
1038        minarea = int(self.small_size.text())
1039        props = ut.labels_properties( img )
1040        resimg = np.copy( img )
1041        for prop in props:
1042            if prop.area < minarea:
1043                (resimg[prop.slice])[prop.image] = 0
1044        ## update the tracks after the potential disappearance of some cells
1045        self.epicure.seglayer.data[frame] = resimg
1046        self.epicure.removed_labels( img, resimg, frame )
1047    
1048    def merge_inside_cells( self ):
1049        """ Merge cell that falls inside another cell with ut """
1050        start_time = ut.start_time()
1051        self.viewer.window._status_bar._toggle_activity_dock(True)
1052        for i in progress(range(0, self.epicure.nframes)):
1053            self.merge_inside_cell(self.epicure.seglayer.data[i], i)
1054        self.viewer.window._status_bar._toggle_activity_dock(False)
1055        if self.epicure.verbose > 0:
1056            ut.show_duration( start_time, "Inside cells merged in ")
1057
1058    def merge_inside_cell( self, img, frame ):
1059        """ Merge cells that fits inside the convex hull of a cell with it """
1060        graph = ut.connectivity_graph( img, distance=3)
1061        adj_bg = []
1062        
1063        nodes = list(graph.nodes)
1064        for label in nodes:
1065            nneighbor = len(graph.adj[label])
1066            if nneighbor == 1:
1067                neigh_label = graph.adj[label]
1068                for lab in neigh_label.keys():
1069                    nlabel = int( lab )
1070                # both labels are still present in the current frame
1071                if nlabel>0 and sum( np.isin( [label, nlabel], self.epicure.seglayer.data[frame] ) ) == 2:
1072                    self.merge_labels( frame, label, nlabel, 1.05 )
1073                    if self.epicure.verbose > 0:
1074                        print( "Merged label "+str(label)+" into label "+str(nlabel)+" at frame "+str(frame) )
1075
1076    ###############
1077    ## Shapes functions
1078    def create_shapelayer( self ):
1079        """ Create the layer that handle temporary drawings """
1080        shapes = []
1081        shap = self.viewer.add_shapes( shapes, name=self.shapelayer_name, ndim=3, blending="additive", opacity=1, edge_width=2, scale=self.viewer.layers["Segmentation"].scale )
1082        shap.text.visible = False
1083        shap.visible = False
1084
1085    ######################################"
1086    ## Seeds and watershed functions
1087    def show_hide_seedMapBlock(self):
1088        self.gSeed.setVisible(not self.gSeed.isVisible())
1089        if not self.gSeed.isVisible():
1090            ut.remove_layer(self.viewer, "Seeds")
1091    
1092    def create_seedsBlock(self):
1093        seed_layout = wid.vlayout()
1094        reset_color = self.epicure.get_resetbtn_color()
1095        seed_createbtn = wid.add_button( btn="Create seeds layer", btn_func=self.reset_seeds, descr="Create/reset the layer to add seeds", color=reset_color )
1096        seed_layout.addWidget(seed_createbtn)
1097        seed_loadbtn = wid.add_button( btn="Load seeds from previous time point", btn_func=self.get_seeds_from_prev, descr="Place seeds in background area where cells are in previous time point" )
1098        seed_layout.addWidget(seed_loadbtn)
1099        
1100        ## choose method and segment from seeds
1101        gseg, gseg_layout = wid.group_layout( "Seed based segmentation" )
1102        seed_btn = wid.add_button( btn="Segment cells from seeds", btn_func=self.segment_from_points, descr="Segment new cells from placed seeds" )
1103        gseg_layout.addWidget(seed_btn)
1104        method_line, self.seed_method = wid.list_line( label="Method", descr="Seed based segmentation method to segment some cells" )
1105        self.seed_method.addItem("Intensity-based (watershed)")
1106        self.seed_method.addItem("Distance-based")
1107        self.seed_method.addItem("Diffusion-based")
1108        gseg_layout.addLayout( method_line )
1109        maxdist, self.max_distance = wid.value_line( label="Max cell radius", default_value="100.0", descr="Max cell radius allowed in new cell creation" )
1110        gseg_layout.addLayout(maxdist)
1111        gseg.setLayout(gseg_layout)
1112        
1113        seed_layout.addWidget(gseg)
1114        self.gSeed.setLayout(seed_layout)
1115
1116    def create_seedlayer(self):
1117        pts = []
1118        ## handle change of parameter name in napari versions
1119        if ut.version_napari_above("0.4.19"):
1120            self.viewer.add_points( np.array(pts), face_color="blue", size = 7,  border_width=0, name="Seeds", scale=self.viewer.layers["Segmentation"].scale )
1121        else:
1122            self.viewer.add_points( np.array(pts), face_color="blue", size = 7,  edge_width=0, name="Seeds", scale=self.viewer.layers["Segmentation"].scale )
1123
1124    def reset_seeds(self):
1125        ut.remove_layer(self.viewer, "Seeds")
1126        self.create_seedlayer()
1127
1128    def get_seeds_from_prev(self):
1129        #self.reset_seeds()
1130        if "Seeds" not in self.viewer.layers:
1131            self.create_seedlayer()
1132        tframe = int(self.viewer.cursor.position[0])
1133        segt = self.epicure.seglayer.data[tframe]
1134        if tframe > 0:
1135            pts = self.viewer.layers["Seeds"].data
1136            segp = self.epicure.seglayer.data[tframe-1]
1137            props = ut.labels_properties(segp)
1138            for prop in props:
1139                cent = prop.centroid
1140                ## create a seed in the centroid only in empty spaces
1141                if int(segt[int(cent[0]), int(cent[1])]) == 0:
1142                    pts = np.append(pts, [[tframe, cent[0], cent[1]]], axis=0)
1143            self.viewer.layers["Seeds"].data = pts
1144            self.viewer.layers["Seeds"].refresh()
1145        
1146    def end_place_seed(self):
1147        """ Finish placing seeds mode """
1148        if not self.seed_active:
1149            return
1150        if self.old_mouse_drag is not None:
1151            self.epicure.seglayer.mouse_drag_callbacks = self.old_mouse_drag
1152            self.seed_active = False
1153            ut.show_info("End seed")
1154        ut.set_active_layer( self.viewer, "Segmentation" )
1155
1156    def place_seed(self, event_pos):
1157        """ Add a seed under the cursor """
1158        tframe = int(self.viewer.cursor.position[0])
1159        segt = self.epicure.seglayer.data[tframe]
1160        pts = self.viewer.layers["Seeds"].data
1161        cent = self.viewer.layers["Seeds"].world_to_data( event_pos )
1162        ## create a seed in the centroid only in empty spaces
1163        if int(segt[int(cent[1]), int(cent[2])]) == 0:
1164            pts = np.append(pts, [[tframe, cent[1], cent[2]]], axis=0)
1165            self.viewer.layers["Seeds"].data = pts
1166            self.viewer.layers["Seeds"].refresh()
1167        ut.set_active_layer( self.viewer, "Segmentation" )
1168
1169
1170    def segment_from_points(self):
1171        """ Do cells segmentation from seed points """
1172        if not "Seeds" in self.viewer.layers:
1173            ut.show_warning("No seeds placed")
1174            return
1175        self.end_place_seed()
1176        if len(self.viewer.layers["Seeds"].data) <= 0:
1177            ut.show_warning("No seeds placed")
1178            return
1179
1180        ## get crop of the image around seeds
1181        tframe = ut.current_frame(self.viewer)
1182        segBB, markers, maskBB, labelBB = self.crop_around_seeds( tframe )
1183        ## save current labels to compare afterwards
1184        before_seeding = np.copy(segBB)
1185
1186        ## segment current seeds from points with selected method
1187        if self.seed_method.currentText() == "Intensity-based (watershed)":
1188            self.watershed_from_points( tframe, segBB, markers, maskBB, labelBB )
1189        if self.seed_method.currentText() == "Distance-based":
1190            self.distance_from_points( tframe, segBB, markers, maskBB, labelBB )
1191        if self.seed_method.currentText() == "Diffusion-based":
1192            self.diffusion_from_points( tframe, segBB, markers, maskBB, labelBB )
1193
1194        ## finish segmentation: thin to have one pixel boundaries, update all
1195        skelBB = ut.frame_to_skeleton( segBB, connectivity=1 )
1196        segBB[ skelBB>0 ] = 0
1197        self.reset_seeds()
1198        ## update the list of tracks with the potential new cells
1199        self.epicure.added_labels_oneframe( tframe, before_seeding, segBB )
1200        #self.end_place_seed()
1201        ut.set_active_layer( self.viewer, "Segmentation" )
1202        self.epicure.seglayer.refresh()
1203
1204    def crop_around_seeds( self, tframe ):
1205        """ Get cropped image around the seeds """
1206        ## crop around the seeds, with a margin
1207        seeds = self.viewer.layers["Seeds"].data
1208        segt = self.epicure.seglayer.data[tframe]
1209        extend = int(float(self.max_distance.text())*1.1)
1210        labelBB = ut.getBBox2DFromPts( seeds, extend, segt.shape )
1211        segBB = ut.cropBBox2D(segt, labelBB)
1212        ## mask where there are cells
1213        maskBB = np.copy(segBB)
1214        maskBB = 1*(maskBB==0)
1215        maskBB = np.uint8(maskBB)
1216        ## fill the borders
1217        maskBB = binary_erosion(maskBB, footprint=self.disk_one)
1218        ## place labels in the seed positions
1219        pos = ut.positionsIn2DBBox( seeds, labelBB )
1220        markers = np.zeros(maskBB.shape, dtype="int32")
1221        freelabs = self.epicure.get_free_labels( len(pos) )
1222        for freelab, p in zip(freelabs, pos):
1223            markers[p] = freelab
1224        return segBB, markers, maskBB, labelBB
1225    
1226    def diffusion_from_points(self, tframe, segBB, markers, maskBB, labelBB):
1227        """ Segment from seeds with a diffusion based method (gradient intensity slows it) """
1228        movt = self.viewer.layers["Movie"].data[tframe]
1229        imgBB = ut.cropBBox2D(movt, labelBB)
1230        markers[maskBB==0] = -1 ## block filled area 
1231        ## fill from seeds with diffusion method
1232        splitted = random_walker( imgBB, labels=markers, beta=700, tol=0.01 )
1233        new_labels = list(np.unique(markers))
1234        new_labels.remove(-1)
1235        new_labels.remove(0)
1236        i = 0
1237        lablist = set( splitted.flatten() )
1238        #print(lablist)
1239        #print(new_labels)
1240        for lab in lablist:
1241            if lab > 0:
1242                mask = (splitted == lab)
1243                labels_mask = label(mask)                       
1244                ## keep only biggest region if the label is splitted
1245                regions = ut.labels_properties(labels_mask)
1246                if len(regions) > 2:
1247                    regions.sort(key=lambda x: x.area, reverse=True)
1248                    if len(regions) > 1:
1249                        for rg in regions[1:]:
1250                            splitted[rg.coords[:,0], rg.coords[:,1]] = 0
1251                splitted[splitted==lab] = new_labels[i]
1252                i = i + 1
1253        segBB[(maskBB>0)*(splitted>0)] = splitted[(maskBB>0)*(splitted>0)]
1254        return segBB
1255
1256    def watershed_from_points(self, tframe, segBB, markers, maskBB, labelBB):
1257        """ Performs watershed from the seed points """
1258        movt = self.viewer.layers["Movie"].data[tframe] 
1259        imgBB = ut.cropBBox2D(movt, labelBB)
1260        splitted = watershed( imgBB, markers=markers, mask=maskBB )
1261        segBB[splitted>0] = splitted[splitted>0]
1262        return segBB
1263    
1264    def distance_from_points(self, tframe, segBB, markers, maskBB, labelBB):
1265        """ Segment cells from seed points with Voronoi method """
1266        # iteratif to block when meet other fixed labels 
1267        maxdist = float(self.max_distance.text())
1268        dist = 0
1269        while dist <= maxdist:
1270            markers = ut.touching_labels( markers, expand=1 )
1271            markers[maskBB==0] = 0
1272            dist = dist + 1
1273        segBB[(maskBB>0) * (markers>0)] = markers[(maskBB>0) * (markers>0)]
1274        return segBB
1275        
1276
1277    ######################################
1278    ## Cleaning options
1279
1280    def create_cleaningBlock(self):
1281        """ GUI for cleaning segmentation """
1282        clean_layout = wid.vlayout()
1283        ## cells on border
1284        border_line, self.border_size = wid.button_parameter_line( btn="Remove border cells", btn_func=self.remove_border, value="1", descr_btn="Remove all cell at a distance <= value (in pixels)", descr_value="Distance of the cells to be removed (in pixels)" )
1285        clean_layout.addLayout(border_line)
1286        
1287        ## too small cells
1288        small_line, self.small_size = wid.button_parameter_line( btn="Remove mini cells", btn_func=self.remove_smalls, value="4", descr_btn="Remove all cells smaller than given value (in pixels^2)", descr_value="Minimal cell area (in pixels^2)" )
1289        clean_layout.addLayout(small_line)
1290
1291        ## Cell inside another cell
1292        inside_btn = wid.add_button( btn="Cell inside another: merge", btn_func=self.merge_inside_cells, descr="Merge all small cells fully contained inside another cell to this cell" )
1293        clean_layout.addWidget(inside_btn)
1294
1295        ## sanity check
1296        sanity_btn = wid.add_button( btn="Sanity check", btn_func=self.sanity_check, descr="Check that labels and tracks are consistent with EpiCure restrictions, and try to fix some errors" )
1297        clean_layout.addWidget(sanity_btn)
1298
1299        ## reset labels
1300        reset_color = self.epicure.get_resetbtn_color()
1301        reset_btn = wid.add_button( btn="Reset all", btn_func=self.reset_all, descr="Reset all tracks, groups, suspects..", color=reset_color )
1302        clean_layout.addWidget(reset_btn)
1303
1304        self.gCleaned.setLayout(clean_layout)
1305
1306    ####################################
1307    ## Sanity check/correction options
1308    def sanity_check(self):
1309        """ Check if everything looks okayish, in case some bug or weird editions broke things """
1310        self.viewer.window._status_bar._toggle_activity_dock(True)
1311        progress_bar = progress(total=6)
1312        progress_bar.set_description("Sanity check:")
1313        progress_bar.update(0)
1314        ## check layers presence
1315        ut.show_info("Check and reopen if necessary EpiCure layers")
1316        self.epicure.check_layers()
1317        ## check that each label is unique
1318        progress_bar.update(1)
1319        progress_bar.set_description("Sanity check: label unicity")
1320        label_list = np.unique(self.epicure.seglayer.data)
1321        if self.epicure.verbose > 0:
1322            print("Checking label unicity...")
1323        self.check_unique_labels( label_list, progress_bar )
1324        ## check and update if necessary tracks 
1325        progress_bar.update(2)
1326        if self.epicure.forbid_gaps:
1327            progress_bar.set_description("Sanity check: track gaps")
1328            ut.show_info("Check if some tracks contain gaps")
1329            gaped = self.epicure.handle_gaps( track_list=None )
1330        ## check that labels and tracks correspond
1331        progress_bar.set_description("Sanity check: label-track")
1332        progress_bar.update(3)
1333        if self.epicure.verbose > 0:
1334            print("Checking labels-tracks correspondance...")
1335        track_list = self.epicure.tracking.get_track_list()
1336        untracked = list(set(label_list) - set(track_list))
1337        if 0 in untracked:
1338            untracked.remove(0)
1339        if len(untracked) > 0:
1340            ut.show_warning("! Labels "+str(untracked)+" not in Tracks -- Adding it now")
1341            for untrack in untracked:
1342                self.epicure.add_one_label_to_track( untrack )
1343        
1344        ## update label list with changes that might have been done
1345        label_list = np.unique(self.epicure.seglayer.data)
1346        track_list = self.epicure.tracking.get_track_list()
1347        ## check if all tracks have associated labels in the image
1348        phantom_tracks = list(set(track_list) - set(label_list))
1349        if len(phantom_tracks) > 0:
1350            print("! Phantom tracks "+str(phantom_tracks)+" found")
1351            self.epicure.delete_tracks(phantom_tracks)
1352            print("-> Phantom tracks deleted from Tracks")
1353        
1354        ## checking events
1355        progress_bar.set_description("Sanity check: extrusions")
1356        progress_bar.update(5)
1357        if self.epicure.verbose > 0:
1358            print("Checking extrusion = end of track...")
1359        self.epicure.check_extrusions_sanity()
1360        
1361        ## finished
1362        if self.epicure.verbose > 0:
1363            print("Checking finished")
1364        progress_bar.close()
1365        self.viewer.window._status_bar._toggle_activity_dock(False)
1366
1367    def check_unique_labels(self, label_list, progress_bar):
1368        """ Check that all labels are contiguous and not present several times (only by frame) """
1369        found = 0
1370        s = generate_binary_structure(2,2)
1371        pbtmp = progress(total=len(label_list), desc="Check labels", nest_under=progress_bar)
1372        for i, lab in enumerate(label_list):
1373            pbtmp.update(i)
1374            if lab > 0:
1375                for frame in self.epicure.seglayer.data:
1376                    if lab in frame:
1377                        labs, num_objects = ndlabel(binary_dilation(frame==lab, footprint=s), structure=s)
1378                        if num_objects > 1:
1379                            ut.show_warning("! Problem, label "+str(lab)+" found several times")
1380                            found = found + 1
1381                            continue
1382        pbtmp.close()
1383        if found <= 0:
1384            ut.show_info("Labels unicity ok")
1385
1386    ###############
1387    ## Resetting
1388
1389    def reset_all( self ):
1390        """ Reset labels through skeletonization, reset tracks, suspects, groups """
1391        if self.epicure.verbose > 0:
1392            ut.show_info( "Resetting everything ")
1393        self.viewer.window._status_bar._toggle_activity_dock(True)
1394        progress_bar = progress(total=5)
1395        ## get skeleton and relabel (ensure label unicity)
1396        progress_bar.update(1)
1397        progress_bar.set_description("Reset: relabel")
1398        self.epicure.reset_data()
1399        self.epicure.tracking.reset()
1400        self.epicure.reset_labels()
1401        progress_bar.update(2)
1402        progress_bar.set_description("Reset: reinit tracks")
1403        self.epicure.tracked = 0
1404        self.epicure.load_tracks(progress_bar)
1405        if self.epicure.verbose > 0:
1406            print("Resetting done")
1407        progress_bar.close()
1408        self.viewer.window._status_bar._toggle_activity_dock(False)
1409
1410
1411
1412    ######################################
1413    ## Selection options
1414
1415    def create_selectBlock(self):
1416        """ GUI for handling selection with shapes """
1417        select_layout = wid.vlayout()
1418        ## create/select the ROI
1419        draw_btn = wid.add_button( btn="Draw/Select ROI", btn_func=self.draw_shape, descr="Draw or select a ROI to apply region action on" )
1420        select_layout.addWidget(draw_btn)
1421        remove_sel_btn = wid.add_button( btn="Remove cells inside ROI", btn_func=self.remove_cells_inside, descr="Remove all cells inside the selected/first ROI" )
1422        select_layout.addWidget(remove_sel_btn)
1423        remove_line, self.keep_new_cells = wid.button_check_line( btn="Remove cells outside ROI", btn_func=self.remove_cells_outside, check="Keep new cells", checked=True, checkfunc=None, descr_btn="Remove all cells outside the current ROI", descr_check="Keep new cells tah appear in the ROI in later frames" )
1424        select_layout.addLayout(remove_line)
1425
1426        self.gSelect.setLayout(select_layout)
1427
1428    def draw_shape(self):
1429        """ Draw/select a shape in the Shapes layer """
1430        if self.shapelayer_name not in self.viewer.layers:
1431            self.create_shapelayer()
1432        ut.set_active_layer(self.viewer, self.shapelayer_name)
1433        lay = self.viewer.layers[self.shapelayer_name]
1434        lay.visible = True
1435        lay.opacity = 0.5
1436
1437    def get_selection(self):
1438        """ Get the active (or first) selection """
1439        if self.shapelayer_name not in self.viewer.layers:
1440            return None
1441        lay = self.viewer.layers[self.shapelayer_name]
1442        selected = lay.selected_data
1443        if len(selected) == 0:
1444            if len(lay.shape_type) == 1:
1445                if self.epicure.verbose > 1:
1446                    print("No shape selected, use the only one present")
1447                lay.selected_data.add(0)
1448                selected = lay.selected_data
1449            else:
1450                ut.show_warning("No shape selected, do nothing")
1451                return None
1452        return lay.data[list(selected)[0]] 
1453
1454    def get_labels_inside(self):
1455        """ Get the list of labels inside the current ROI """
1456        current_shape = self.get_selection()
1457        if current_shape is None:
1458            return None
1459        self.current_bbox = ut.getBBox2DFromPts(current_shape, 30, self.epicure.imgshape2D)
1460        self.current_cropshape = ut.positionsIn2DBBox(current_shape, self.current_bbox )
1461        tframe = ut.current_frame(self.viewer)
1462        segt = self.epicure.seglayer.data[tframe]
1463        croped = ut.cropBBox2D(segt, self.current_bbox)
1464        labprops = ut.labels_properties(croped)
1465        inside = points_in_poly( [lab.centroid for lab in labprops], self.current_cropshape )
1466        toedit = [lab.label for i, lab in enumerate(labprops) if inside[i] ]
1467        return toedit
1468
1469    def remove_cells_outside(self):
1470        """ Remove all labels centroids outside the selected ROI """
1471        tokeep = self.get_labels_inside()
1472        if self.keep_new_cells.isChecked():
1473            tframe = ut.current_frame(self.viewer)
1474            segt = self.epicure.seglayer.data[tframe]
1475            toremove = set(np.unique(segt).flatten()) - set(tokeep)
1476            self.epicure.remove_labels(list(toremove))
1477        else:
1478            self.epicure.keep_labels(tokeep)
1479        lay = self.viewer.layers[self.shapelayer_name]
1480        lay.remove_selected()
1481        self.epicure.finish_update()
1482
1483    def remove_cells_inside(self):
1484        """ Remove all labels centroids inside the selected ROI """
1485        toremove = self.get_labels_inside()
1486        self.epicure.remove_labels(toremove)
1487        lay = self.viewer.layers[self.shapelayer_name]
1488        lay.remove_selected()
1489        self.epicure.finish_update()
1490
1491    def lock_cells_inside(self):
1492        """ Check all cells inside the selected ROI into current group """
1493        tocheck = self.get_labels_inside()
1494        for lab in tocheck:
1495            self.check_label(lab)
1496        if self.epicure.verbose > 0:
1497            print(str(len(tocheck))+" cells checked in group "+str(self.check_group.text()))
1498        lay = self.viewer.layers[self.shapelayer_name]
1499        lay.remove_selected()
1500        self.epicure.finish_update()
1501
1502    def group_classify_intensity( self ):
1503        """ Calls the interface to classify cells by intensity """
1504        self.classif.update()
1505        self.classif.show()
1506    
1507    def group_classify_event( self ):
1508        """ Calls the interface to classify cells by event interaction """
1509        self.classif_event.update()
1510        self.classif_event.show()
1511
1512    def group_event_cells( self, event_type ):
1513        """ Classify the cells that finished with the selected event into the event group """
1514        events = self.epicure.inspecting.get_events_from_type( event_type )
1515        if len( events ) > 0:
1516            tids = []
1517            for evt_sid in events:
1518                pos, label = self.epicure.inspecting.get_event_infos( evt_sid )
1519                if label not in tids:
1520                    tids.append(label)
1521            group_name = "Cells_"+event_type
1522            if event_type == "extrusion":
1523                group_name = "Extruding"
1524            if event_type == "division":    
1525                group_name = "Dividing"
1526            self.group_choice.setCurrentText(group_name)
1527            self.epicure.reset_group( group_name ) 
1528            self.redraw_clear_group( group_name )
1529            self.group_labels( tids )
1530
1531
1532    def group_positive_cells( self, layer_name, meth, min_frame, max_frame, threshold ):
1533        """ Classify the cells with mean intensity in the given frame range above threshold into the current group """
1534        if self.group_choice.currentText() == "":
1535            ut.show_warning("Write a group name before")
1536            return
1537        layer = self.viewer.layers[layer_name]
1538        frames = np.arange(min_frame, max_frame+1)
1539        if (min_frame == 0) and (max_frame == self.epicure.nframes-1):
1540            frames = None
1541        tracks, mean_int = self.epicure.tracking.measure_intensity_features( "intensity_"+meth, intimg=layer.data, frames=frames )
1542        tids = tracks[ mean_int > threshold ]
1543        self.redraw_clear_group( group=None )
1544        self.group_labels( tids )
1545
1546    def group_cells_inside(self):
1547        """ Put all cells inside the selected ROI into current group """
1548        if self.group_choice.currentText() == "":
1549            ut.show_warning("Write a group name before")
1550            return
1551        tocheck = self.get_labels_inside()
1552        if tocheck is None:
1553            if self.epicure.verbose > 0:
1554                print("No cell to add to group")
1555            return
1556        self.group_labels( tocheck )
1557        if self.epicure.verbose > 0:
1558            print(str(len(tocheck))+" cells assigend to group "+str(self.group_choice.currentText()))
1559        lay = self.viewer.layers[self.shapelayer_name]
1560        lay.remove_selected()
1561        self.epicure.finish_update()
1562
1563
1564    ######################################
1565    ## Group cells functions
1566    def create_groupCellsBlock(self):
1567        """ Create subpanel of Cell group options """
1568        group_layout = wid.vlayout()
1569        groupgr, self.group_choice = wid.list_line( label="Group name", descr="Choose/Set the current group name" )
1570        group_layout.addLayout(groupgr)
1571        self.group_choice.setEditable(True)
1572
1573        self.group_show = wid.add_check( check="Show groups", checked=False, check_func=self.see_groups, descr="Add a layer with the cells colored by group" )
1574        group_layout.addWidget(self.group_show)
1575
1576        reset_line, self.reset_list = wid.button_list( btn="Reset group", func=self.reset_group, descr="Remove chosen group (or all) and cell assignation to this group" )
1577        group_layout.addLayout( reset_line )
1578        self.update_group_lists()
1579        group_sel_btn = wid.add_button( btn="Cells inside ROI to group", btn_func=self.group_cells_inside, descr="Add all cells inside ROI to the current group" )
1580        group_layout.addWidget(group_sel_btn)
1581
1582        ## add button for intensity classifier interface
1583        group_class_btn = wid.add_button( btn="Group from track intensity..", btn_func=self.group_classify_intensity, descr="Open interface to group cells based on their mean intensity" )
1584        group_layout.addWidget( group_class_btn )
1585        
1586        ## add button for events classifier interface
1587        group_event_btn = wid.add_button( btn="Group from events..", btn_func=self.group_classify_event, descr="Open interface to group cells according to if they are related to an event (dividing cell, extruding cell..)" )
1588        group_layout.addWidget( group_event_btn )
1589
1590        self.gGroup.setLayout(group_layout)
1591
1592    def load_checked(self):
1593        cfile = self.get_filename("_checked.txt")
1594        with open(cfile) as infile:
1595            labels = infile.read().split(";")
1596        for lab in labels:
1597            self.check_load_label(lab)
1598        ut.show_info("Checked cells loaded")
1599
1600    def reset_group( self ):
1601        gr = self.reset_list.currentText()
1602        if gr != "All":
1603            self.redraw_clear_group( gr )
1604        self.epicure.reset_group( gr )
1605        if gr == "All":
1606            self.see_groups()
1607    
1608    def update_group_choice( self, group ):
1609        """ Check if group has been added in the list choices of group """
1610        if self.group_choice.findText( group ) < 0:
1611            ## not added yet. If user is typing the name and did not press enter, it can be still in edition mode, so not added
1612            self.group_choice.addItem( group )
1613    
1614    def update_group_lists( self ):
1615        """ Update list of groups for reset button """
1616        curchoice = self.group_choice.currentText()
1617        curreset = self.reset_list.currentText()
1618        self.group_choice.clear()
1619        self.reset_list.clear()
1620        self.reset_list.addItem("All")
1621        for group in self.epicure.groups.keys():
1622            self.update_group_choice( group )
1623            self.reset_list.addItem( group )
1624        self.reset_list.setCurrentText("All")
1625        if self.reset_list.findText( curreset ) >= 0:
1626            self.reset_list.setCurrentText(curreset)
1627        if self.group_choice.findText( curchoice ) >= 0:
1628            self.group_choice.setCurrentText( curchoice )
1629
1630    def save_groups(self):
1631        groupfile = self.get_filename("_groups.txt")
1632        with open(groupfile, 'w') as out:
1633            out.write(";".join(group.write_group() for group in self.epicure.groups))
1634        ut.show_info("Cell groups saved in "+groupfile)
1635
1636    def see_groups(self):
1637        if self.group_show.isChecked():
1638            ut.remove_layer(self.viewer, self.grouplayer_name)
1639            grouped = self.epicure.draw_groups()
1640            self.viewer.add_labels(grouped, name=self.grouplayer_name, opacity=0.75, blending="additive", scale=self.viewer.layers["Segmentation"].scale)
1641            ut.set_active_layer(self.viewer, "Segmentation")
1642        else:
1643            ut.remove_layer(self.viewer, self.grouplayer_name)
1644            ut.set_active_layer(self.viewer, "Segmentation")
1645    
1646    def group_labels( self, labels ):
1647        """ Add label(s) to group """
1648        if self.group_choice.currentText() == "":
1649            ut.show_warning("Write group name before")
1650            return
1651        group = self.group_choice.currentText()
1652        self.group_ingroup( labels, group )
1653       
1654    def check_label(self, label):
1655        """ Mark label as checked """
1656        group = self.check_group.text()
1657        self.check_ingroup(label, group)
1658
1659        
1660    def group_ingroup(self, labels, group):
1661        """ Add the given label to chosen group """
1662        self.epicure.cells_ingroup( labels, group )
1663        if self.grouplayer_name in self.viewer.layers:
1664            self.redraw_label_group( labels, group )
1665       
1666    def check_load_label(self, labelstr):
1667        """ Read the label to check from file """
1668        res = labelstr.split("-")
1669        cellgroup = res[0]
1670        celllabel = int(res[1])
1671        self.check_ingroup(celllabel, cellgroup)
1672        
1673    def add_cell_to_group(self, event):
1674        """ Add cell under click to the current group """
1675        label = ut.getCellValue( self.epicure.seglayer, event ) 
1676        self.group_labels( [label] )
1677
1678    def remove_cell_group(self, event):
1679        """ Remove the cell from the group it's in if any """
1680        label = ut.getCellValue( self.epicure.seglayer, event ) 
1681        self.epicure.cell_removegroup( label )
1682        if self.grouplayer_name in self.viewer.layers:
1683            self.redraw_label_group( [label], 0 )
1684
1685    def redraw_clear_group( self, group=None ):
1686        """ Clear all the cells from group in the current group layer """
1687        if group is None:
1688            if self.group_choice.currentText() == "":
1689                ut.show_warning("Write group name before")
1690                return
1691            group = self.group_choice.currentText()
1692        if self.grouplayer_name in self.viewer.layers:
1693            lay = self.viewer.layers[self.grouplayer_name]
1694            igroup = self.epicure.get_group_index(group) + 1
1695            if igroup == 0:
1696                ## the group was not present, igroup is -1
1697                return
1698            lay.data[lay.data == igroup] = 0
1699            lay.refresh()
1700            ut.set_active_layer(self.viewer, "Segmentation")
1701
1702    def redraw_label_group(self, labels, group):
1703        """ Update the Group layer for label """
1704        lay = self.viewer.layers[self.grouplayer_name]
1705        if group == 0:
1706            lay.data[ np.isin( self.epicure.seg, labels ) ] = 0
1707        else:
1708            igroup = self.epicure.get_group_index(group) + 1
1709            lay.data[ np.isin( self.epicure.seg, labels)  ] = igroup
1710        lay.refresh()
1711
1712    ######### overlay message
1713    def add_overlay_message(self):
1714        text = self.epicure.text + "\n"
1715        ut.setOverlayText(self.viewer, text, size=10)
1716
1717    ################## Events editing functions
1718    def add_extrusion( self, labela, frame ):
1719        """ Add an extrusion event, given the label and frame """
1720
1721        if (frame != self.epicure.tracking.get_last_frame( labela )):
1722            if self.epicure.verbose > 0:
1723                print("Clicked label is not the last of the track, don't add extrusion")
1724                return
1725
1726        ## add extrusion to event list (if active)
1727        self.epicure.inspecting.add_extrusion( labela, frame )
1728
1729    def add_division( self, labela, labelb, frame ):
1730        """ Add a division event, given the labels of the two daughter cells """
1731        if frame == 0:
1732            if self.epicure.verbose > 0:
1733                print("Cannot define a division before the first frame")
1734            return False
1735
1736        if (frame != self.epicure.tracking.get_first_frame( labela )) or (frame != self.epicure.tracking.get_first_frame(labelb) ):
1737            if self.epicure.verbose > 0:
1738                print("One daughter track is not starting at current frame, don't add division")
1739                return False
1740
1741        ## merge the two labels to find their parent
1742        bbox, merge = ut.getBBox2DMerge( self.epicure.seglayer.data[frame], labela, labelb )
1743        twoframes = ut.crop_twoframes( self.epicure.seglayer.data, bbox, frame )
1744        crop_merge = ut.cropBBox2D( merge, bbox )
1745        twoframes[1] = crop_merge # merge of the labels and 0 outside
1746            
1747        ## keep only parent labels that stop at the previous frame
1748        twoframes = self.keep_orphans(twoframes, frame)
1749        ## do mini-tracking to assign most likely parent
1750        parent = self.get_parents( twoframes, [1] )
1751        if self.epicure.verbose > 0:
1752            print( "Found parent "+str(parent[0])+" to clicked cells "+str(labela)+" and "+str(labelb) )
1753        ## add division to graph
1754        if parent is not None and parent[0] is not None:
1755            self.epicure.tracking.add_division( labela, labelb, parent[0] )
1756            ## add division to event list (if active)
1757            self.epicure.inspecting.add_division_event( labela, labelb, parent[0], frame )
1758            return True
1759        return False
1760            
1761    ################## Track editing functions
1762    def key_tracking_binding(self):
1763        """ active key bindings for tracking options """
1764        self.epicure.overtext["trackedit"] = "---- Track editing ---- \n"
1765        strack = self.epicure.shortcuts["Tracks"]
1766        etrack = self.epicure.shortcuts["Events"]
1767        self.epicure.overtext["trackedit"] += ut.print_shortcuts( strack )
1768        
1769        @self.epicure.seglayer.mouse_drag_callbacks.append
1770        def manual_add_extrusion(layer, event):
1771            ### add an event of an extrusion under the click
1772            if ut.shortcut_click_match( etrack["add extrusion"], event ):
1773                # get the start and last labels
1774                labela = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1775                tframe = int(event.position[0])
1776                    
1777                if labela == 0:
1778                    if self.epicure.verbose > 0:
1779                        print("Clicked position is not a cell, do nothing")
1780                    return
1781                self.add_extrusion( labela, tframe )
1782        
1783        @self.epicure.seglayer.mouse_drag_callbacks.append
1784        def manual_add_division(layer, event):
1785            ### add an event of a division, selecting the two daughter cells
1786            if ut.shortcut_click_match( etrack["add division"], event ):
1787                # get the start and last labels
1788                labela = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1789                start_pos = event.position
1790                yield
1791                while event.type == 'mouse_move':
1792                    yield
1793                labelb = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1794                end_pos = event.position
1795                tframe = int(event.position[0])
1796                    
1797                if labela == 0 or labelb == 0:
1798                    if self.epicure.verbose > 0:
1799                        print("One position is not a cell, do nothing")
1800                    return
1801                self.add_division( labela, labelb, tframe )
1802        
1803        @self.epicure.seglayer.bind_key( strack["lineage color"]["key"], overwrite=True )
1804        def color_tracks_lineage(seglayer):
1805            if self.tracklayer_name in self.viewer.layers:
1806                self.epicure.tracking.color_tracks_by_lineage()
1807        
1808        @self.epicure.seglayer.bind_key( strack["show"]["key"], overwrite=True )
1809        def see_tracks(seglayer):
1810            if self.tracklayer_name in self.viewer.layers:
1811                tlayer = self.viewer.layers[self.tracklayer_name]
1812                tlayer.visible = not tlayer.visible
1813
1814        @self.epicure.seglayer.bind_key( strack["mode"]["key"], overwrite=True)
1815        def edit_track(layer):
1816            self.label_tr = None 
1817            self.start_label = None
1818            self.interp_labela = None
1819            self.interp_labelb = None
1820            ut.show_info("Tracks editing mode")
1821            self.old_mouse_drag, self.old_key_map = ut.clear_bindings(self.epicure.seglayer)
1822
1823            @self.epicure.seglayer.mouse_drag_callbacks.append
1824            def click(layer, event):
1825                """ Edit tracking """
1826                if event.type == "mouse_press":
1827                  
1828                    """ Merge two tracks, spatially or temporally: left click, select the first label """
1829                    if ut.shortcut_click_match( strack["merge first"], event ):
1830                        self.start_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1831                        self.start_pos = event.position
1832                        # move one frame after for next cell to link
1833                        #ut.set_frame( self.epicure.viewer, event.position[0]+1 )
1834                        return
1835                    """ Merge two tracks, spatially or temporally: right click, select the second label """
1836                    if ut.shortcut_click_match( strack["merge second"], event ):
1837                        if self.start_label is None:
1838                            if self.epicure.verbose > 0:
1839                                print("No left click done before right click, don't merge anything")
1840                            return
1841                        end_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1842                        end_pos = event.position
1843                        if self.epicure.verbose > 0:
1844                            print("Merging track "+str(self.start_label)+" with track "+str(end_label))
1845                        
1846                        if self.start_label is None or self.start_label == 0 or end_label == 0:
1847                            if self.epicure.verbose > 0:
1848                                print("One position is not a cell, do nothing")
1849                            return
1850                        ## ready, merge
1851                        self.merge_tracks( self.start_label, self.start_pos, end_label, end_pos )
1852                        self.end_track_edit()
1853                        return
1854
1855                    ### Split the track in 2: new label for the next frames 
1856                    if ut.shortcut_click_match( strack["split track"], event ):
1857                        start_frame = int(event.position[0])
1858                        label = ut.getCellValue(self.epicure.seglayer, event) 
1859                        self.epicure.split_track( label, start_frame )
1860                        self.end_track_edit()
1861                        return
1862                        
1863                    ### Swap the two track from the current frame 
1864                    if ut.shortcut_click_match( strack["swap"], event ):
1865                        start_frame = int(event.position[0])
1866                        label = ut.getCellValue(self.epicure.seglayer, event) 
1867                        yield
1868                        while event.type == 'mouse_move':
1869                            yield
1870                        end_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)                           
1871                            
1872                        if label == 0 or end_label == 0:
1873                            if self.epicure.verbose > 0:
1874                                print("One position is not a cell, do nothing")
1875                            return
1876
1877                        self.epicure.swap_tracks( label, end_label, start_frame )
1878                            
1879                        if self.epicure.verbose > 0:
1880                            ut.show_info("Swapped track "+str(label)+" with track "+str(end_label)+" from frame "+str(start_frame))
1881                        self.end_track_edit()
1882                        return
1883
1884                    # Manual tracking: get a new label and spread it to clicked cells on next frames
1885                    if ut.shortcut_click_match( strack["start manual"], event ):
1886                        zpos = int(event.position[0])
1887                        if self.label_tr is None:
1888                            ## first click: get the track label
1889                            self.label_tr = ut.getCellValue(self.epicure.seglayer, event) 
1890                        else:
1891                            old_label = ut.setCellValue(self.epicure.seglayer, self.epicure.seglayer, event, self.label_tr, layer_frame=zpos, label_frame=zpos)
1892                            self.epicure.tracking.remove_one_frame( old_label, zpos, handle_gaps=self.epicure.forbid_gaps )
1893                            self.epicure.add_label( [self.label_tr], zpos )
1894                        ## advance to next frame, ready for a click
1895                        self.viewer.dims.set_point(0, zpos+1)
1896                        ## if reach the end, stops here for this track
1897                        if (zpos+1) >= self.epicure.seglayer.data.shape[0]:
1898                            self.end_track_edit()
1899                        return
1900                    
1901                    ## Finish manual tracking
1902                    if ut.shortcut_click_match( strack["end manual"], event ):
1903                        self.end_track_edit()
1904                        return
1905                   
1906                    ## Interpolate between two labels: get first label
1907                    if ut.shortcut_click_match( strack["interpolate first"], event ):
1908                        ## left click, first cell
1909                        self.interp_labela = ut.getCellValue(self.epicure.seglayer, event) 
1910                        self.interp_framea = int(event.position[0])
1911                        return
1912                    
1913                    ## Interpolate between two labels: get second label and interpolate
1914                    if ut.shortcut_click_match( strack["interpolate second"], event ):
1915                        ## right click, second cell
1916                        labelb = ut.getCellValue(self.epicure.seglayer, event) 
1917                        interp_frameb = int(event.position[0])
1918                        if self.interp_labela is not None:
1919                            if abs(self.interp_framea - interp_frameb) <= 1:
1920                                print("No frames to interpolate, exit")
1921                                self.end_track_edit()
1922                                return
1923                            if self.interp_framea < interp_frameb:
1924                                self.interpolate_labels(self.interp_labela, self.interp_framea, labelb, interp_frameb)
1925                            else:
1926                                self.interpolate_labels(labelb, interp_frameb, self.interp_labela, self.interp_framea )
1927                            self.end_track_edit()
1928                            return
1929                        else:
1930                            print("No cell selected with left click before. Exit mode")
1931                            self.end_track_edit()
1932                            return
1933                        
1934                    ## Delete all the labels of the track until its end
1935                    if ut.shortcut_click_match( strack["delete"], event ):
1936                        tframe = int(event.position[0])
1937                        label = ut.getCellValue(self.epicure.seglayer, event)
1938                        if label > 0:
1939                            self.epicure.replace_label( label, 0, tframe )
1940                            if self.epicure.verbose > 0:
1941                                print("Track "+str(label)+" deleted from frame "+str(tframe))
1942                        self.end_track_edit()
1943                        return
1944
1945                ## A right click or other click stops it
1946                self.end_track_edit()
1947
1948            #@self.epicure.seglayer.mouse_double_click_callbacks.append
1949            #def double_click(layer, event):
1950            #    """ Edit tracking : double click options """
1951            #    if event.type == "mouse_double_click":      
1952                    
1953        
1954            @self.epicure.seglayer.bind_key( strack["mode"]["key"], overwrite=True )
1955            def end_edit_track(layer):
1956                self.end_track_edit()
1957
1958    def end_track_edit(self):
1959        self.start_label = None
1960        self.interp_labela = None
1961        self.interp_labelb = None
1962        ut.reactive_bindings( self.epicure.seglayer, self.old_mouse_drag, self.old_key_map )
1963        ut.show_info("End track edit mode")
1964
1965    def merge_tracks(self, labela, posa, labelb, posb):
1966        """ 
1967            Merge track with label a with track of label b, temporally or spatially 
1968        """
1969        if labela == labelb:
1970            if self.epicure.verbose > 0:
1971                print("Already the same track" )
1972                return
1973        if int(posb[0]) == int(posa[0]):
1974            self.tracks_spatial_merging( labela, posa, labelb )
1975        else:
1976            self.tracks_temporal_merging( labela, posa, labelb, posb )
1977
1978    def tracks_spatial_merging( self, labela, posa, labelb ):
1979        """ Merge spatially two tracks: labels have to be touching all along the common frames """
1980        start_time = ut.start_time()
1981        ## get last common frame
1982        lasta = self.epicure.tracking.get_last_frame( labela )
1983        lastb = self.epicure.tracking.get_last_frame( labelb )
1984        lastcommon = min(lasta, lastb)
1985
1986        ## if longer than the last common, split the label(s) that continue
1987        if lasta > lastcommon:
1988            if self.epicure.tracking.get_first_frame( labela ) < int(posa[0]):
1989                self.epicure.split_track( labela, lastcommon+1 )
1990        if lastb > lastcommon:
1991            if self.epicure.tracking.get_first_frame( labelb ) < int(posa[0]):
1992                self.epicure.split_track( labelb, lastcommon+1 )
1993
1994        ## Looks, ok, create a new track and merge the two tracks in it
1995        new_label = self.epicure.get_free_label()
1996        new_labels = []
1997        ind_tomodif = None
1998        footprint = disk(radius=3)
1999        for frame in range( int(posa[0]), lastcommon+1 ):
2000            bbox, merged = ut.getBBox2DMerge( self.epicure.seg[frame], labela, labelb )
2001            bbox = ut.extendBBox2D( bbox, 1.05, self.epicure.imgshape2D )
2002            
2003            ## check if labels are touching at each frame
2004            segt_crop = ut.cropBBox2D( self.epicure.seg[frame], bbox )
2005            touched = ut.checkTouchingLabels( segt_crop, labela, labelb )
2006            if not touched:
2007                print("Labels "+str(labela)+" and "+str(labelb)+" are not always touching. Refusing to merge them")
2008                return 
2009            
2010            ## merge the two labels together
2011            joinlab = ut.cropBBox2D( merged, bbox )
2012            joinlab = new_label * binary_closing(joinlab, footprint)
2013           
2014            ## get the index and new values to change
2015            indmodif = ut.ind_boundaries( joinlab )
2016            #indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2017            new_labels = new_labels + [0]*len(indmodif)
2018            curmodif = np.transpose( np.nonzero( joinlab == new_label ) )
2019            new_labels = new_labels + [new_label]*len(curmodif)
2020            indmodif = np.vstack((indmodif, curmodif))
2021            indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2022            if ind_tomodif is None:
2023                ind_tomodif = indmodif
2024            else:
2025                ind_tomodif = np.vstack((ind_tomodif, indmodif))
2026            #ind_tomodif = np.vstack((ind_tomodif, curmodif))
2027        
2028        ## update the labels and the tracks
2029        self.epicure.change_labels_frommerge( ind_tomodif, new_labels, remove_labels=[labela, labelb] )
2030        if self.epicure.verbose > 0:
2031            ut.show_info("Merged spatially "+str(labela)+" with "+str(labelb)+" from frame "+str(int(posa[0]))+" to frame "+str(lastcommon)+"\n New track label is "+str(new_label))
2032        if self.epicure.verbose > 1:
2033            ut.show_duration(start_time, "Merging spatially tracks in ")
2034
2035
2036    def tracks_temporal_merging( self, labela, posa, labelb, posb ):
2037        """ 
2038        Merge track with label a with track of label b if consecutives frames. 
2039        It does not check if label are close in distance, assume it is.
2040        """
2041
2042        if self.epicure.forbid_gaps:
2043            if abs(int(posb[0]) - int(posa[0])) != 1:
2044                if self.epicure.verbose > 0:
2045                    print("Frames to merge are not consecutives, refused")
2046                return
2047
2048        ## If frame b is before frame a, swap so that a is first 
2049        if posa[0] > posb[0]:
2050            posc = np.copy(posa)
2051            posa = posb
2052            posb = posc
2053            labelc = labela
2054            labela = labelb
2055            labelb = labelc
2056
2057        ## Check that posa is last frame of label a and pos b first frame of label b
2058        if int(posa[0]) != self.epicure.tracking.get_last_frame( labela ):
2059            if self.epicure.verbose > 0:
2060                print("Clicked label "+str(labela)+" at frame "+str(posa[0])+" was not the last frame of the track -> splitting it")
2061            self.epicure.split_track( labela, int(posa[0])+1 )
2062
2063        if posb[0] != self.epicure.tracking.get_first_frame( labelb ):
2064            if self.epicure.verbose > 0:
2065                print("Clicked label "+str(labelb)+" at frame "+str(posb[0])+" is not the first frame of the track -> splitting it")
2066            labelb = self.epicure.split_track( labelb, int(posb[0]) )
2067
2068        self.epicure.replace_label( labelb, labela, int(posb[0]) )
2069        
2070
2071    def get_parents(self, twoframes, labels):
2072        """ Get parent of all labels """
2073        return self.epicure.tracking.find_parents( labels, twoframes )
2074    
2075    def get_position_label_2D(self, img, labels, parent_labels):
2076        """ Get position of each label to update with parent label """
2077        indmodif = None
2078        new_labels = []
2079        ## get possible free labels, to be sure that it will not take the same ones
2080        free_labels = self.epicure.get_free_labels(len(labels))
2081        for i, lab in enumerate(labels):
2082            parent_label = parent_labels[i]
2083            if parent_label is None:
2084                parent_label = free_labels[i]
2085                parent_labels[i] = parent_label
2086            curmodif = np.argwhere( img==lab )
2087            if indmodif is None:
2088                indmodif = curmodif
2089            else:
2090                indmodif = np.vstack((indmodif, curmodif))
2091            new_labels = new_labels + ([parent_label]*curmodif.shape[0])
2092        return indmodif, new_labels, parent_labels
2093
2094    def keep_orphans( self, img, frame, keep_labels=[]):
2095        """ Keep only labels that doesn't have a follower (track is finishing at that frame) """
2096        ## remove the labels to track
2097        labs = np.unique(img[0]).tolist() #np.setdiff1d( img[0], labels ).tolist()
2098        if 0 in labs:
2099            labs.remove(0)
2100        ## Check that it's not present at current frame
2101        torem = [ lab for lab in labs if (lab not in keep_labels) and (self.epicure.tracking.is_in_frame( lab, frame ) ) ]
2102        if len(torem) == 0:
2103            return img
2104        mask = np.isin(img[0], torem)
2105        img[0][mask] = 0
2106        return img
2107
2108    def inherit_parent_labels(self, myframe, labels, bbox, frame, keep_labels):
2109        """ Get parent labels if any and indices to modify with it """
2110        if ( self.epicure.tracked == 0 ) or (frame<=0):
2111            parent_labels = [None]*len(labels)
2112            indmodif, new_labels, parent_labels = self.get_position_label_2D(myframe, labels, parent_labels)
2113        else:
2114            twoframes = ut.crop_twoframes( self.epicure.seglayer.data, bbox, frame )
2115            twoframes[1] = np.copy(myframe) # merge of the labels and 0 outside
2116            twoframes = self.keep_orphans( twoframes, frame, keep_labels=keep_labels)
2117            
2118            parent_labels = self.get_parents( twoframes, labels )
2119        
2120            indmodif, new_labels, parent_labels = self.get_position_label_2D(twoframes[1], labels, parent_labels)
2121
2122        if self.epicure.verbose > 0:
2123            print("Set value (from parent or new): "+str(np.unique(new_labels)))
2124        ## back to movie position
2125        indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2126        return indmodif, new_labels, parent_labels
2127    
2128    def inherit_child_labels(self, myframe, labels, bbox, frame, parent_labels, keep_labels):
2129        """ Get child labels if any and indices to modify with it """
2130        if (self.epicure.tracked == 0 ) or (frame>=self.epicure.nframes-1):
2131            return [], []
2132        else:
2133            twoframes = np.copy( ut.cropBBox2D(self.epicure.seglayer.data[frame+1], bbox) )
2134            ## check if the new value to set is present in the following frame, in that case don't do any propagation
2135            for par in parent_labels:
2136                if np.any( twoframes==par ):
2137                    if self.epicure.verbose > 1:
2138                        print("Propagating: not because new value present in labels: "+str(par))
2139                    return [], []
2140
2141            twoframes = np.stack( (twoframes, np.copy(myframe)) )
2142            twoframes = self.keep_orphans(twoframes, frame, keep_labels=keep_labels)
2143            child_labels = self.get_parents( twoframes, labels )
2144            
2145            if self.epicure.verbose > 0:
2146                print("Propagate  the new value to: "+str(child_labels))
2147            if child_labels is None:
2148                return [], []
2149        
2150        # get position of each child label to update with current label
2151        indmodif = []
2152        new_labels = []
2153        for i, lab in enumerate(child_labels):
2154            if lab is not None:
2155                if lab == parent_labels[i]:
2156                    ## going to propagate to itself, no need
2157                    continue
2158                after_frame = frame+1
2159                last_frame = self.epicure.tracking.get_last_frame( parent_labels[i] )
2160                if (last_frame is not None) and (last_frame >= after_frame):
2161                    ## the label to propagate is present somewhere after the current frame
2162                    self.epicure.split_track( parent_labels[i], after_frame )
2163                inds = self.epicure.get_label_indexes( lab, after_frame )
2164                if len(indmodif) == 0:
2165                    indmodif = inds
2166                else:
2167                    indmodif = np.vstack((indmodif, inds))
2168                new_labels = new_labels + np.repeat(parent_labels[i], len(inds)).tolist()
2169        return indmodif, new_labels
2170
2171    def propagate_label_change(self, myframe, labels, bbox, frame, keep_labels):
2172        """ Propagate the new labelling to match parent/child labels """
2173        start_time = ut.start_time()
2174        indmodif = ut.ind_boundaries( myframe )
2175        indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2176        #ut.show_info("Boundaries in "+"{:.3f}".format((time.time()-start_time)/60)+" min")
2177        new_labels = np.repeat(0, len(indmodif)).tolist()
2178
2179        ## get parent labels if any for each label
2180        indmodif2, new_labels2, parent_labels = self.inherit_parent_labels(myframe, labels, bbox, frame, keep_labels)
2181        if indmodif2 is not None:
2182            indmodif = np.vstack((indmodif, indmodif2))
2183            new_labels = new_labels+new_labels2
2184        if self.epicure.verbose > 1:
2185            ut.show_duration(start_time, "Propagation, parents found, ")
2186
2187        ## propagate the change: get child labels if any for each label
2188        indmodif_child, new_labels_child = self.inherit_child_labels(myframe, labels, bbox, frame, parent_labels, keep_labels)
2189        if len(indmodif_child) > 0:
2190            indmodif = np.vstack((indmodif, indmodif_child))
2191            new_labels = new_labels + new_labels_child
2192        if self.epicure.verbose > 1:
2193            ut.show_duration(start_time, "Propagation, childs found, ")
2194        
2195        ## go, do the update
2196        self.epicure.change_labels(indmodif, new_labels)
2197
2198    ############# Test
2199    def interpolate_labels( self, labela, framea, labelb, frameb ):
2200        """ 
2201            Interpolate the label shape in between two labels 
2202            Based on signed distance transform, like Fiji ROIs interpolation
2203        """
2204        if self.epicure.verbose > 1:
2205            print("Interpolating between "+str(labela)+" and "+str(labelb))
2206            print("From frame "+str(framea)+" to frame "+str(frameb))
2207            start_time = ut.start_time()
2208        
2209        sega = self.epicure.seglayer.data[framea]
2210        maska = np.isin( sega, [labela] )
2211        segb = self.epicure.seglayer.data[frameb]
2212        maskb = np.isin( segb, [labelb] )
2213
2214        ## get merged bounding box, and crop around it
2215        mask = maska | maskb
2216        props = ut.labels_properties(mask*1)
2217        bbox = ut.extendBBox2D( props[0].bbox, extend_factor=1.2, imshape=mask.shape )
2218
2219        maska = ut.cropBBox2D( maska, bbox )
2220        maskb = ut.cropBBox2D( maskb, bbox )
2221
2222        ## get signed distance transform of each label
2223        dista = edt.sdf( maska )
2224        distb = edt.sdf( maskb )
2225
2226        inds = None
2227        new_labels = []
2228        for frame in range(framea+1, frameb):
2229            p = (frame-framea)/(frameb-framea)
2230            dist = (1-p) * dista + p * distb
2231            ## change only pixels that are 0
2232            frame_crop = ut.cropBBox2D( self.epicure.seglayer.data[frame], bbox )
2233            tochange = binary_dilation(dist>0, footprint=disk(radius=2)) * (frame_crop<=0)   # expand to touch neighbor label
2234            
2235            ## indexes and new values to change
2236            indmodif = np.argwhere( tochange > 0 ).tolist()
2237            indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2238            if inds is None:
2239                inds = indmodif
2240            else:
2241                inds = np.vstack( (inds, indmodif) )
2242            new_labels = new_labels + [labela]*len(indmodif)
2243
2244            ## be sure to remove the boundaries with neighbor labels
2245            bound_ind = ut.ind_boundaries( tochange )
2246            new_labels = new_labels + [0]*len(bound_ind)
2247            bound_ind = ut.toFullMoviePos( bound_ind, bbox, frame )
2248            inds = np.vstack( (inds, bound_ind) )
2249
2250        ## Go, apply the changes
2251        self.epicure.change_labels( inds, new_labels )
2252        ## change the second track to first track value
2253        self.epicure.replace_label( labelb, labela, frameb )
2254        if self.epicure.verbose > 1:
2255            ut.show_duration( start_time, "Interpolation took " )
2256        if self.epicure.verbose > 0:
2257            ut.show_info( "Interpolated label "+str(labela)+" from frame "+str(framea+1)+" to "+str(frameb-1) )

Handle user interaction to edit the segmentation

Editing(napari_viewer, epic)
27    def __init__(self, napari_viewer, epic):
28        """ Initialize the Edit panel interface """
29        super().__init__()
30        self.viewer = napari_viewer
31        self.epicure = epic
32        self.old_mouse_drag = None
33        self.tracklayer_name = "Tracks"
34        self.shapelayer_name = "ROIs"
35        self.grouplayer_name = "Groups"
36        self.updated_labels = None   ## keep which labels are being edited
37        self.seed_active = False ## if place seed option is on
38
39        layout = wid.vlayout()
40        
41        ## Option to use default napari painting options
42        #self.napari_painting = wid.add_check( "Default Napari painting tools (no checks)", checked=False, check_func=self.painting_tools, descr="Use the label painting of Napari instead of customized EpiCure ones (will not perform any sanity check)" )
43        #layout.addWidget( self.napari_painting )
44
45        ## Option to remove all border cells
46        clean_line, self.clean_vis, self.gCleaned = wid.checkgroup_help( name="Cleaning options", checked=False, descr="Show/hide options to clean the segmentation", help_link="Edit#cleaning-options", display_settings=self.epicure.display_colors, groupnb="group" )
47        layout.addLayout(clean_line)
48        self.create_cleaningBlock()
49        layout.addWidget(self.gCleaned)
50        self.gCleaned.hide()
51
52        ## handle grouping cells into categories
53        group_line, self.group_vis, self.gGroup = wid.checkgroup_help( name="Cell group options", checked=False, descr="Show/hide options to define cell groups", help_link="Edit#group-options", display_settings=self.epicure.display_colors, groupnb="group2"  )
54        layout.addLayout(group_line)
55        self.create_groupCellsBlock()
56        layout.addWidget(self.gGroup)
57        self.gGroup.hide()
58        
59        ## Selection option: crop, remove cells
60        select_line, self.select_vis, self.gSelect = wid.checkgroup_help( name="ROI options", checked=False, descr="Show/hide options to work on Regions", help_link="Edit#roi-options", display_settings=self.epicure.display_colors, groupnb="group3" )
61        layout.addLayout(select_line)
62        self.create_selectBlock()
63        layout.addWidget(self.gSelect)
64        self.gSelect.hide()
65        
66        ## Put seeds and do watershed from it
67        seed_line, self.seed_vis, self.gSeed = wid.checkgroup_help( name="Seeds options", checked=False, descr="Show/hide options to segment from seeds", help_link="Edit#seeds-options", display_settings=self.epicure.display_colors, groupnb="group4" )
68        layout.addLayout(seed_line)
69        self.create_seedsBlock()
70        layout.addWidget(self.gSeed)
71        self.gSeed.hide()
72        
73        self.setLayout(layout)
74        
75        ## interface done, ready to work 
76        self.create_shapelayer()
77        self.modify_cells()
78        self.key_tracking_binding()
79        self.add_overlay_message()
80
81        ## catch filling/painting operations
82        self.napari_fill = self.epicure.seglayer.fill
83        self.epicure.seglayer.fill = self.epicure_fill
84        self.napari_paint = self.epicure.seglayer.paint
85        self.epicure.seglayer.paint = self.lazy #self.epicure_paint
86        ### scale and radius for paiting
87        self.paint_scale = np.array([self.epicure.seglayer.scale[i+1] for i in range(2)], dtype=float)
88        self.epicure.seglayer.events.brush_size.connect( self.paint_radius )
89        self.paint_radius()
90        self.disk_one = disk(radius=1)
91        self.classif = ClassifyIntensity( self )
92        self.classif_event = ClassifyEvent( self )
93        self.scalexy = self.epicure.epi_metadata["ScaleXY"]

Initialize the Edit panel interface

viewer
epicure
old_mouse_drag
tracklayer_name
shapelayer_name
grouplayer_name
updated_labels
seed_active
napari_fill
napari_paint
paint_scale
disk_one
classif
classif_event
scalexy
def painting_tools(self):
 95    def painting_tools( self ):
 96        """ Choose which painting tools should be activated """
 97        if self.napari_painting.isChecked():
 98            self.epicure.seglayer.fill = self.napari_fill
 99            self.epicure.seglayer.paint = self.napari_paint
100        else:
101            self.epicure.seglayer.fill = self.epicure_fill
102            self.epicure.seglayer.paint = self.lazy

Choose which painting tools should be activated

def apply_settings(self, settings):
105    def apply_settings( self, settings ):
106        """ Load the prefered settings for Edit panel """
107        for setting, val in settings.items():
108            if setting == "Show group option":
109                self.group_vis.setChecked( val )
110            if setting == "Show clean option":
111                self.clean_vis.setChecked( val )
112            if setting ==  "Show ROI option":
113                self.select_vis.setChecked( val )
114            if setting == "Show seed option":
115                self.seed_vis.setChecked( val )
116            if setting == "Show groups":
117                self.group_show.setChecked( val )
118            if setting == "Border size":
119                self.border_size.setText( val )
120            if setting == "Seed method":
121                self.seed_method.setCurrentText( val )
122            if setting == "Seed max cell":
123                self.max_distance.setText( val )

Load the prefered settings for Edit panel

def get_current_settings(self):
126    def get_current_settings( self ):
127        """ Returns the current state of the Edit widget """
128        setting = {}
129        setting["Show group option"] = self.group_vis.isChecked()
130        setting["Show clean option"] = self.clean_vis.isChecked()
131        setting["Show ROI option"] = self.select_vis.isChecked()
132        setting["Show seed option"] = self.seed_vis.isChecked()
133        setting["Show groups"] = self.group_show.isChecked()
134        setting["Border size"] = self.border_size.text()
135        setting["Seed method"] = self.seed_method.currentText()
136        setting["Seed max cell"] = self.max_distance.text()
137        return setting

Returns the current state of the Edit widget

def paint_radius(self):
139    def paint_radius( self ):
140        """ Update painitng radius with brush size """
141        self.radius = np.floor(self.epicure.seglayer.brush_size / 2) + 0.5
142        self.brush_indices = sphere_indices(self.radius, tuple(self.paint_scale)) 

Update painitng radius with brush size

def setParent(self, epy):
144    def setParent(self, epy):
145        self.epicure = epy

setParent(self, parent: QWidget|None) setParent(self, parent: QWidget|None, f: Qt.WindowType)

def get_filename(self, endname):
147    def get_filename(self, endname):
148        return ut.get_filename(self.epicure.outdir, self.epicure.imgname+endname )
def get_values(self, coord):
150    def get_values(self, coord):
151        """ Get the label value under coord, the current frame, prepare the coords """
152        int_coord = tuple(np.round(coord).astype(int))
153        tframe = int(coord[0])
154        segdata = self.epicure.seglayer.data[tframe]
155        int_coord = int_coord[1:3]
156        # get value of the label that will be painted over
157        prev_label = int(segdata[int_coord])
158        return int_coord, tframe, segdata, prev_label

Get the label value under coord, the current frame, prepare the coords

def epicure_fill(self, coord, new_label, refresh=True):
161    def epicure_fill(self, coord, new_label, refresh=True):
162        """ Check if the filled cell is already registered """
163        if new_label == 0:
164            if self.epicure.verbose > 0:
165                ut.show_warning("Fill with 0 (background) not allowed \n Use Eraser tool (press <1>) to erase")
166                return
167        int_coord, tframe, segdata, prev_label = self.get_values( coord )
168
169        hascell = self.epicure.has_label( new_label )
170        if hascell:
171            ## already present, check that it is at the same place
172            ## label before
173            mask_before = segdata==new_label
174            if np.sum(mask_before) <= 0:
175                ut.show_warning("Label "+str(new_label)+" is already used in other frames. Choose another label")
176                return
177        
178        ## if try to fill an empty zone, ensure that it doesn't fill the skeletons
179        if prev_label == 0:
180            skel = ut.frame_to_skeleton( segdata )
181            skel_fill = max(np.max(segdata)+2, new_label+1)
182            segdata[skel] = skel_fill
183            skel = None
184            
185        if hascell:
186            # if contiguous replace only selected connected component, calculate how it would be changed
187            matches = (segdata == prev_label)
188            labeled_matches, num_features = label(matches, return_num=True)
189            if num_features != 1:
190                match_label = labeled_matches[int_coord]
191                matches = np.logical_and( matches, labeled_matches == match_label )
192           
193            # check if touch the already present cell
194            ok = self.touching_masks(mask_before, matches)
195            if not ok:
196                ut.show_warning("Label "+str(new_label)+" added do not touch already present cell. Choose another label or draw contiguously")
197                ## reset if necessary
198                if prev_label == 0:
199                    segdata[segdata==skel_fill] = 0  ## put skeleton back to 0
200                return
201            ut.setNewLabel( self.epicure.seglayer, (np.argwhere(matches)).tolist(), new_label, add_frame=tframe )
202            if prev_label == 0:
203                segdata[skel] = 0  ## put skeleton back to 0
204        else:
205            ## new cell, add it to the tracks list
206            self.napari_fill(coord, new_label, refresh=True)
207            if prev_label == 0:
208                segdata[segdata==skel_fill] = 0  ## put skeleton back to 0
209                ut.remove_boundaries(segdata)
210            self.epicure.add_label(new_label, tframe)
211        
212        ## Finish filling step to ensure everything's fine
213        self.epicure.seglayer.refresh()
214        ## put the active mode of the layer back to the zoom one
215        self.epicure.seglayer.mode = "pan_zoom"
216        if prev_label != 0: 
217            self.epicure.tracking.remove_one_frame( [prev_label], tframe, handle_gaps=self.epicure.forbid_gaps )

Check if the filled cell is already registered

def lazy(self, coord, new_label, refresh=True):
219    def lazy( self, coord, new_label, refresh=True ):
220        return
def epicure_paint(self, coords, new_label, tframe, hascell):
222    def epicure_paint( self, coords, new_label, tframe, hascell ):
223        """ Edit a label with paint tool, with several pixels at once """
224        mask_indices = None
225        ## convert the coords with brush size, check that is fully inside
226        for coord in coords:
227            int_coord = np.array( np.round(coord).astype(int)[1:3] ) 
228            for brush in self.brush_indices:
229                pt = int_coord + brush
230                if ut.inside_bounds( pt, self.epicure.imgshape2D ):
231                    if mask_indices is None:
232                        mask_indices = pt
233                    else:
234                        mask_indices = np.vstack( ( mask_indices, pt ) )
235        
236        ## crop around part of the image to update
237        bbox = ut.getBBoxFromPts( mask_indices, extend=0, imshape=self.epicure.imgshape2D )
238        if hascell:
239            ## extend around points a lot if the label is there already to avoid cutting it
240            extend = 4
241        else:
242            extend = 1.5
243        bbox = ut.extendBBox2D( bbox, extend_factor=extend, imshape=self.epicure.imgshape2D )
244        cropdata = ut.cropBBox2D( self.epicure.seglayer.data[tframe], bbox )
245        crop_indices = ut.positions2DIn2DBBox( mask_indices, bbox )
246        
247        ## get previous data before painting
248        prev_labels = np.unique( cropdata[ tuple(np.array(crop_indices).T) ] ).tolist()
249        if 0 in prev_labels:
250            prev_labels.remove(0)
251
252        if new_label > 0:    
253            if hascell:
254                ## check that label is in current frame
255                mask_before = cropdata==new_label
256                if not np.isin(1, mask_before):
257                    ut.show_warning("Label "+str(new_label)+" is already used in other frames. Choose another label")
258                    return
259
260                ## already present, check that it is at the same place
261                #### Test if painting touch previous label
262                mask_after = np.zeros(cropdata.shape)
263                mask_after[ tuple(np.array(crop_indices).T) ] = 1
264                ok = self.touching_masks(mask_before, mask_after)
265                if not ok:
266                    ut.show_warning("Label "+str(new_label)+" added do not touch already present cell. Choose another label or draw contiguously")
267                    return
268            else:
269                ## drawing new cell, fill it at the end
270                if self.epicure.verbose > 2:
271                    print("Painting a new cell")
272
273        ## Paint and update everything    
274        painted = np.copy(cropdata)
275        painted[ tuple(np.array(crop_indices).T) ] = new_label
276        if new_label > 0:
277            if self.epicure.seglayer.preserve_labels:
278                painted = painted*(np.isin( cropdata, [0, new_label] ))
279                painted = binary_fill_holes( (painted==new_label) )
280                ## remove one-pixel thick lines
281                painted = binary_opening( painted )
282                crop_indices = np.argwhere( (painted>0) )
283            else:
284                painted = binary_fill_holes( painted==new_label )
285                crop_indices = np.argwhere(painted>0)    
286        ### if preseve label is on, there can be nothing left to paint
287        if len(crop_indices) <= 0:
288            return
289        mask_indices = ut.toFullMoviePos( crop_indices, bbox, tframe )
290        new_labels = np.repeat(new_label, len(mask_indices)).tolist()
291
292        ## Update label boundaries if necessary
293        cind_bound = ut.ind_boundaries( painted )
294        if self.epicure.seglayer.preserve_labels:
295            ind_bound = [ ind for ind in cind_bound if (cropdata[tuple(ind)] == new_label) ]
296        else:
297            ind_bound = [ ind for ind in cind_bound if cropdata[tuple(ind)] in prev_labels ]
298        if (new_label>0) and (len( ind_bound ) > 0):
299            bound_ind = ut.toFullMoviePos( ind_bound, bbox, tframe )
300            bound_labels = np.repeat(0, len(bound_ind)).tolist()
301            mask_indices = np.vstack( (mask_indices, bound_ind) )
302            new_labels = new_labels + bound_labels
303
304        ## Go, apply the change, and update the tracks
305        self.epicure.change_labels( mask_indices, new_labels )

Edit a label with paint tool, with several pixels at once

def create_cell_from_line(self, tframe, positions):
307    def create_cell_from_line( self, tframe, positions ):
308        """ Create new cell(s) from drawn line (junction) """
309        bbox = ut.getBBox2DFromPts( positions, extend=0, imshape=self.epicure.imgshape2D )
310        bbox = ut.extendBBox2D( bbox, extend_factor=2, imshape=self.epicure.imgshape2D )
311
312        segt = self.epicure.seglayer.data[tframe]
313        cropt = ut.cropBBox2D( segt, bbox )
314        crop_positions = ut.positionsIn2DBBox( positions, bbox )
315
316        line = np.zeros(cropt.shape, dtype="uint8")
317        ## fill the already filled pixels by other labels
318        line[ cropt > 0 ] = 1
319        ## expand from one pixel to fill the junction
320        line = binary_dilation( line )
321        ## fill the interpolated line
322        for i, pos in enumerate(crop_positions):
323            if cropt[round(pos[0]), round(pos[1])] == 0:
324                line[round(pos[0]), round(pos[1])] = 1
325            if (i > 0):
326                prev = (crop_positions[i-1][0], crop_positions[i-1][1])
327                cur = (pos[0], pos[1])
328                interp_coords = interpolate_coordinates(prev, cur, 1)
329                for ic in interp_coords:
330                    line[tuple(np.round(ic).astype(int))] = 1
331        
332        ## close the junction gaps, and the line eventually
333        line = binary_closing( line )
334        new_cells, nlabels = label( line, background=1, return_num=True, connectivity=1 )
335        ## no new cell to create
336        if nlabels <= 0:
337            return
338        ## get the new labels to relabel and add as new cells
339        labels = list( set( new_cells.flatten() ) )
340        if 0 in labels:
341            labels.remove(0)
342       
343        ## try to get new cell labels from previous and next slices
344        parents = [None]*len(labels)
345        if tframe > 0:
346            twoframes = ut.crop_twoframes( self.epicure.seglayer.data, bbox, tframe )
347            twoframes[1] = new_cells
348            twoframes = self.keep_orphans( twoframes, tframe )
349            parents = self.get_parents( twoframes, labels )
350        childs = [None]*len(labels)
351        if tframe < (self.epicure.nframes-1):
352            twoframes = np.copy( ut.cropBBox2D(self.epicure.seglayer.data[tframe+1], bbox) )
353            twoframes = np.stack( (twoframes, np.copy(new_cells)) )
354            twoframes = self.keep_orphans( twoframes, tframe )
355            childs = self.get_parents( twoframes, labels )
356        
357        free_labels = self.epicure.get_free_labels( nlabels )  
358        torelink = []
359        for i in range( len(labels) ):
360            if (parents[i] is not None) and (childs[i] is not None):
361                free_labels[i] = parents[i]
362                if self.epicure.verbose > 0:
363                    print("Link new cell with previous/next "+str(free_labels[i]))
364                #if childs[i] != parents[i]:
365                #    torelink.append( [free_labels[i], childs[i]] )
366            ## only one link found, take it
367            if (parents[i] is not None) and (childs[i] is None):
368                free_labels[i] = parents[i]
369                if self.epicure.verbose > 0:
370                    print("Link new cell with previous/next "+str(free_labels[i]))
371            if (parents[i] is None) and (childs[i] is not None):
372                free_labels[i] = childs[i]
373                if self.epicure.verbose > 0:
374                    print("Link new cell with previous/next "+str(free_labels[i]))
375
376        print("Added cells "+str(free_labels))
377
378        ## get the new indices and labels to draw
379        new_labels = []
380        indices = None
381        for i, lab in enumerate( labels ):
382            curindices = np.argwhere( new_cells == lab )
383            if indices is None:
384                indices = curindices
385            else:
386                indices = np.vstack((indices, curindices))
387            new_labels = new_labels + ([free_labels[i]]*curindices.shape[0])    
388        
389        ## add the label boundary
390        indbound = ut.ind_boundaries( new_cells )
391        indices = np.vstack( (indices, indbound) )
392        new_labels = new_labels + np.repeat( 0, len(indbound) ).tolist()
393        indices = ut.toFullMoviePos( indices, bbox, tframe )
394        self.epicure.change_labels( indices, new_labels )
395
396        ## relink child tracks if necessary
397        #for relink in torelink:
398        #    self.epicure.replace_label( relink[1], relink[0], tframe )

Create new cell(s) from drawn line (junction)

def touching_masks(self, maska, maskb):
400    def touching_masks(self, maska, maskb):
401        """ Check if the two mask touch """
402        maska = binary_dilation(maska, footprint=self.disk_one)
403        return np.sum(np.logical_and(maska, maskb))>0

Check if the two mask touch

def touching_indices(self, maska, indices):
405    def touching_indices(self, maska, indices):
406        """ Check if the indices touch the mask """
407        maska = binary_dilation(maska, footprint=self.disk_one)
408        return np.isin(1, maska[indices]) > 0

Check if the indices touch the mask

def modify_cells(self):
412    def modify_cells(self):
413        sl = self.epicure.shortcuts["Labels"]
414        self.epicure.overtext["labels"] = "---- Labels editing ---- \n"
415        self.epicure.overtext["labels"] += ut.print_shortcuts( sl )
416        
417        sgroup = self.epicure.shortcuts["Groups"]
418        self.epicure.overtext["grouped"] = "---- Group cells ---- \n"
419        self.epicure.overtext["grouped"] += ut.print_shortcuts( sgroup )
420        
421        sseed = self.epicure.shortcuts["Seeds"]
422        self.epicure.overtext["seed"] = "---- Seed options --- \n"
423        self.epicure.overtext["seed"] += ut.print_shortcuts( sseed )
424
425        @self.epicure.seglayer.mouse_drag_callbacks.append
426        def set_checked(layer, event):
427            if event.type == "mouse_press":
428                if (event.button == 1) and (len(event.modifiers) == 0):
429                    if layer.mode == "paint": 
430                        #and not self.napari_painting.isChecked():
431                        ### Overwrite the painting to check that everything stays within EpiCure constraints
432                        if self.shapelayer_name not in self.viewer.layers:
433                            self.create_shapelayer()
434                        shape_lay = self.viewer.layers[self.shapelayer_name]
435                        shape_lay.mode = "add_path"
436                        shape_lay.visible = True
437                        @thread_worker
438                        def refresh_image():                       
439                            shape_lay.refresh()
440                            return
441                        pos = np.array( [shape_lay.world_to_data(event.position)] )
442                        yield
443                        ## record all the successives position of the mouse while clicked
444                        iter = 0
445                        while (event.type == 'mouse_move'): # and (len(pos)<200):
446                            pos = np.vstack( (pos, np.array(shape_lay.world_to_data(event.position))) )
447                            if iter == 5:
448                                shape_lay.data = pos
449                                shape_lay.shape_type = "path"
450                                refresh_image()
451                                #shape_lay.refresh()
452                                iter = 0
453                            iter = iter + 1
454                            yield
455                        pos = np.vstack( (pos, np.array(shape_lay.world_to_data(event.position))) )    
456                        tframe = int( pos[0][0] )
457                        ## painting a new or extending a cell
458                        new_label = layer.selected_label
459                        hascell = None
460                        if new_label > 0:
461                            hascell = self.epicure.has_label( new_label )
462                        ## paint the selected pixels following EpiCure constraints
463                        self.epicure_paint( pos, new_label, tframe, hascell )
464                        shape_lay.data = []
465                        shape_lay.refresh()
466                        shape_lay.visible = False
467
468        @self.epicure.seglayer.mouse_drag_callbacks.append
469        def set_checked(layer, event):
470            if event.type == "mouse_press":
471                if ut.shortcut_click_match( sgroup["add group"], event ):
472                    if self.group_choice.currentText() == "":
473                        ut.show_warning("Write a group name before")
474                        return
475                    if self.epicure.verbose > 0:
476                        print("Mark cell in group "+self.group_choice.currentText())
477                    self.add_cell_to_group(event)
478                    return
479                
480                if ut.shortcut_click_match( sgroup["remove group"], event ):
481                    if self.epicure.verbose > 0:
482                        print("Remove cell from its group")
483                    self.remove_cell_group(event)
484                    return
485
486        @self.epicure.seglayer.bind_key("Control-z", overwrite=False)
487        def undo_operations(seglayer):
488            if self.epicure.verbose > 0:
489                print("Undo previous action")
490            img_before = np.copy(self.epicure.seg)
491            self.epicure.seglayer.undo()
492            self.epicure.update_changed_labels_img( img_before, self.epicure.seglayer.data )
493
494        @self.epicure.seglayer.bind_key( sl["unused paint"]["key"], overwrite=True )
495        def set_nextlabel(layer):
496            lab = self.epicure.get_free_label()
497            ut.show_info( "Unused label "+": "+str(lab) )
498            ut.set_label(layer, lab)
499        
500        @self.epicure.seglayer.bind_key( sl["unused fill"]["key"], overwrite=True )
501        def set_nextlabel_paint(layer):
502            lab = self.epicure.get_free_label()
503            ut.show_info( "Unused label "+": "+str(lab) )
504            ut.set_label(layer, lab)
505            layer.mode = "FILL"
506        
507        @self.epicure.seglayer.bind_key( sl["swap mode"]["key"], overwrite=True )
508        def key_swap(layer):
509            """ Active key bindings for label swapping options """
510            ut.show_info("Begin swap mode: Control and click to swap two labels")
511            self.old_mouse_drag, self.old_key_map = ut.clear_bindings( self.epicure.seglayer )
512
513            @self.epicure.seglayer.mouse_drag_callbacks.append
514            def click(layer, event):
515                """ Swap the labels from first to last position of the pressed mouse """
516                if event.type == "mouse_press":
517                    if len(event.modifiers) > 0:
518                        start_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
519                        start_pos = event.position
520                        yield
521                        while event.type == 'mouse_move':
522                            yield
523                        end_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
524                        end_pos = event.position
525                        tframe = int(event.position[0])
526                    
527                        if start_label == 0 or end_label == 0:
528                            if self.epicure.verbose > 0:
529                                print("One position is not a cell, do nothing")
530                            return
531
532                        if (event.button == 1) and ("Control" in event.modifiers):
533                            # Left-click: swap labels at each end of the click
534                            if self.epicure.verbose > 0:
535                                print("Swap cell "+str(start_label)+" and "+str(end_label))
536                            self.swap_labels(tframe, start_label, end_label)
537                    
538                ut.reactive_bindings( self.epicure.seglayer, self.old_mouse_drag, self.old_key_map )
539                ut.show_info("End swap")
540        
541        @self.epicure.seglayer.bind_key( sseed["new seed"]["key"], overwrite=True )
542        def place_seed(layer):
543            if self.seed_active:
544                ## if option is currently on, stop it
545                self.end_place_seed()
546                return
547            if "Seeds" not in self.viewer.layers:
548                self.create_seedlayer()
549                ut.set_active_layer( self.viewer, "Segmentation" )
550            ## desactivate other click-binding
551            self.old_mouse_drag = self.epicure.seglayer.mouse_drag_callbacks.copy()
552            self.epicure.seglayer.mouse_drag_callbacks = []
553            self.seed_active = True
554            ut.show_info("Left-click to place a new seed")
555
556            @self.epicure.seglayer.mouse_drag_callbacks.append
557            def click(layer, event):
558                if (event.type == "mouse_press") and (len(event.modifiers)==0) and (event.button==1):
559                    ## single left-click place a seed
560                    if "Seeds" not in self.viewer.layers:
561                        self.reset_seeds()
562                    self.place_seed(event.position)
563                else:
564                    self.end_place_seed()
565
566        @self.epicure.seglayer.bind_key( sl["draw junction mode"]["key"], overwrite=True )
567        def manual_junction(layer):
568            """ Launch the manual drawing junction mode """
569            self.drawing_junction_mode()
570
571        @self.epicure.seglayer.mouse_drag_callbacks.append
572        def click(layer, event):
573            if event.type == "mouse_press":
574                zoom = self.viewer.camera.zoom ## in case a napari shortcut changes the zoom
575                center = self.viewer.camera.center ## same
576                ## erase cell option
577                if ut.shortcut_click_match( sl["erase"], event ):
578                    # single right-click: erase the cell
579                    tframe = ut.current_frame(self.viewer)
580                    erased = ut.setLabelValue(self.epicure.seglayer, self.epicure.seglayer, event, 0, tframe, tframe)
581                    ## delete also in track data
582                    if erased is not None:
583                        self.epicure.delete_track( erased, tframe )
584                    ut.reset_view( self.viewer, zoom, center )
585                    return
586                        
587                merging = ut.shortcut_click_match( sl["merge"], event )
588                splitting = ut.shortcut_click_match( sl["split accross"], event )
589                if merging or splitting:
590                    # get the start and last labels
591                    start_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
592                    start_pos = self.epicure.seglayer.world_to_data( event.position )
593                    yield
594                    while event.type == 'mouse_move':
595                        yield
596                    end_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
597                    end_pos = self.epicure.seglayer.world_to_data( event.position )
598                    tframe = int(end_pos[0])
599                    
600                    if start_label == 0 or end_label == 0:
601                        if self.epicure.verbose > 0:
602                            print("One position is not a cell, do nothing")
603                        ut.reset_view( self.viewer, zoom, center )
604                        return
605
606                    if merging:
607                        ## Merge labels at each end of the click
608                        if start_label != end_label:
609                            if self.epicure.verbose > 0:
610                                print("Merge cell "+str(start_label)+" with "+str(end_label))
611                            self.merge_labels(tframe, start_label, end_label)
612                            ut.reset_view( self.viewer, zoom, center )
613                            return
614                    
615                    if splitting:
616                        ## split label at each end of the click
617                        if start_label == end_label:
618                            if self.epicure.verbose > 0:
619                                print("Split cell "+str(start_label))
620                            self.split_label(tframe, start_label, start_pos, end_pos)
621                            ut.reset_view( self.viewer, zoom, center )
622                        else:
623                            if self.epicure.verbose > 0:
624                                print("Not the same cell already, do nothing")
625                    ut.reset_view( self.viewer, zoom, center )
626                    return
627
628                drawing_split = ut.shortcut_click_match( sl["split draw"], event )
629                redrawing = ut.shortcut_click_match( sl["redraw junction"], event )
630                if drawing_split or redrawing:
631                    if self.shapelayer_name not in self.viewer.layers:
632                        self.create_shapelayer()
633                    shape_lay = self.viewer.layers[self.shapelayer_name]
634                    shape_lay.mode = "add_path"
635                    shape_lay.visible = True
636                    shape_lay.data = []
637                    scaled_pos = shape_lay.world_to_data(event.position)
638                    pos = [scaled_pos]
639                    yield
640                    ## record all the successives position of the mouse while clicked
641                    while event.type == 'mouse_move':
642                        scaled_pos = shape_lay.world_to_data(event.position)
643                        pos.append( scaled_pos )
644                        shape_lay.data = np.array( pos )
645                        shape_lay.shape_type = "path"
646                        shape_lay.refresh()
647                        yield
648                    scaled_pos = shape_lay.world_to_data(event.position)
649                    pos.append( scaled_pos )
650                    ut.set_active_layer(self.viewer, "Segmentation")
651                    tframe = int(event.position[0])
652                    if redrawing:
653                        ##  modify junction along the drawn line
654                        if self.epicure.verbose > 0:
655                            print("Correct junction with the drawn line ")
656                        self.redraw_along_line(tframe, pos)
657                        shape_lay.data = []
658                        shape_lay.refresh()
659                        shape_lay.visible = False
660                        ut.reset_view( self.viewer, zoom, center )
661                        return
662                    if drawing_split:
663                        ## split labels along the drawn line
664                        if self.epicure.verbose > 0:
665                            print("Split cell along the drawn line ")
666                        self.split_along_line(tframe, pos)
667                        shape_lay.data = []
668                        shape_lay.refresh()
669                        shape_lay.visible = False
670                        ut.reset_view( self.viewer, zoom, center )
671                        return
672                    ut.reset_view( self.viewer, zoom, center )
673                    return
def drawing_junction_mode(self):
675    def drawing_junction_mode( self ):
676        """ Active mouse bindings for manually drawing the junction, and try to fill defined area """
677            
678        sl = self.epicure.shortcuts["Labels"]
679        ut.show_info("Begin drawing junction: Control-Left-click to draw the junction and create new cell(s) from it")
680        self.old_mouse_drag, self.old_key_map = ut.clear_bindings( self.epicure.seglayer )
681        
682        @self.epicure.seglayer.bind_key( sl["draw junction mode"]["key"], overwrite=True )
683        def stop_draw_junction_mode( layer ):
684            ut.reactive_bindings( self.epicure.seglayer, self.old_mouse_drag, self.old_key_map )
685            ut.show_info("End drawing mode")
686        
687        @self.epicure.seglayer.mouse_drag_callbacks.append
688        def click(layer, event):
689            if ut.shortcut_click_match( sl["drawing junction"], event ):
690                shape_lay = self.viewer.layers[self.shapelayer_name]
691                shape_lay.mode = "add_path"
692                shape_lay.visible = True
693                scaled_position = shape_lay.world_to_data( event.position )
694                pos = [scaled_position]
695                yield
696                ## record all the successives position of the mouse while clicked
697                i = 0
698                while event.type == 'mouse_move':
699                    scaled_position = shape_lay.world_to_data( event.position )
700                    pos.append( scaled_position )
701                    if i%5 == 0:
702                        # refresh display every n steps
703                        shape_lay.data = np.array( pos ) 
704                        shape_lay.shape_type = "path"
705                        shape_lay.refresh()
706                    i = i + 1
707                    yield
708                scaled_position = shape_lay.world_to_data( event.position )
709                pos.append(scaled_position)
710                ut.set_active_layer(self.viewer, "Segmentation")
711                tframe = int(event.position[0])
712                self.create_cell_from_line( tframe, pos )        
713                shape_lay.data = []
714                shape_lay.refresh()
715                shape_lay.visible = False
716                ut.reactive_bindings( self.epicure.seglayer, self.old_mouse_drag, self.old_key_map )
717                ut.show_info("End drawing mode")

Active mouse bindings for manually drawing the junction, and try to fill defined area

def split_label(self, tframe, startlab, start_pos, end_pos):
719    def split_label(self, tframe, startlab, start_pos, end_pos):
720        """ Split the label in two cells based on the two seeds """
721        segt = self.epicure.seglayer.data[tframe]
722        labelBB = ut.getBBox2D(segt, startlab)
723        labelBB = ut.extendBBox2D( labelBB, extend_factor=1.25, imshape=self.epicure.imgshape2D )
724
725        mov = self.viewer.layers["Movie"].data[tframe]
726        imgBB = ut.cropBBox2D(mov, labelBB)
727        segBB = ut.cropBBox2D(segt, labelBB)
728        maskBB = np.zeros(segBB.shape, dtype="uint8")
729        maskBB[segBB==startlab] = 1
730        spos = ut.positionIn2DBBox( start_pos, labelBB )
731        epos = ut.positionIn2DBBox( end_pos, labelBB )
732
733        markers = np.zeros(maskBB.shape, dtype=self.epicure.dtype)
734        markers[spos] = startlab
735        markers[epos] = self.epicure.get_free_label()
736        splitted = watershed( imgBB, markers=markers, mask=maskBB )
737        if (np.sum(splitted==startlab) < self.epicure.minsize) or (np.sum(splitted==markers[epos]) < self.epicure.minsize):
738            if self.epicure.verbose > 0:
739                print("Sorry, split failed, one cell smaller than "+str(self.epicure.minsize)+" pixels")
740        else:
741            if len(np.unique(splitted)) > 2:
742                curframe = np.zeros(segBB.shape, dtype="uint8")
743                labels = []
744                for i, splitlab in enumerate(np.unique(splitted)):
745                    if splitlab > 0:
746                        curframe[splitted==splitlab] = i+1
747                        labels.append(i+1)
748
749                curframe = ut.remove_boundaries(curframe)
750                ## apply the split and propagate the label to descendant label
751                self.propagate_label_change( curframe, labels, labelBB, tframe, [startlab] )
752            else:
753                if self.epicure.verbose > 0:
754                    print("Split failed, no boundary in pixel intensities found")

Split the label in two cells based on the two seeds

def redraw_along_line(self, tframe, positions):
757    def redraw_along_line(self, tframe, positions):
758        """ Redraw the two labels separated by a line drawn manually """
759        bbox = ut.getBBox2DFromPts( positions, extend=0, imshape=self.epicure.imgshape2D )
760        #bbox = ut.extendBBox2D( bbox, extend_factor=1.25, imshape=self.epicure.imgshape2D )
761
762        segt = self.epicure.seglayer.data[tframe]
763        cropt = ut.cropBBox2D( segt, bbox )
764        crop_positions = ut.positionsIn2DBBox( positions, bbox )
765
766        # get the value of the cells to update (most frequent label along the line)
767        curlabels = []
768        prev_pos = None
769        # Find closest zero elements in the inverted image (same as closest non-zero for image)
770        
771        crop_zeros = distance_transform_edt(cropt, return_distances=False, return_indices=True)
772
773        for pos in crop_positions:
774            if (prev_pos is None) or ((round(pos[0]) != round(prev_pos[0])) and (round(pos[1]) != round(prev_pos[1]) )):
775                ## find closest pixel that is 0 (on a junction)
776                juncpoint = crop_zeros[:, round(pos[0]), round(pos[1])]
777                labs = np.unique( cropt[ (juncpoint[0]-2):(juncpoint[0]+2), (juncpoint[1]-2):(juncpoint[1]+2) ] )
778                for clab in labs:
779                    if clab > 0:
780                        curlabels.append(clab)
781                prev_pos = pos
782                
783        sort_curlabel = sorted(set(curlabels), key=curlabels.count)
784        ## external junction: only one cell
785        if len(sort_curlabel) < 2:
786            if self.epicure.verbose > 0:
787                print("Only one cell along the junction: can't do it")
788                return
789        flabel = sort_curlabel[-1]
790        slabel = sort_curlabel[-2]
791        if self.epicure.verbose > 0:
792            print("Cells to update: "+str(flabel)+" "+str(slabel))
793        
794        ## crop around selected label
795        bbox, _ = ut.getBBox2DMerge( segt, flabel, slabel )
796        bbox = ut.extendBBox2D( bbox, extend_factor=1.25, imshape=self.epicure.imgshape2D )
797        init_cropt = ut.cropBBox2D( segt, bbox )
798        curlabel = flabel
799        ## merge the two labels together
800        binlab = np.isin( init_cropt, [flabel, slabel] )*1
801        footprint = disk(radius=2)
802        cropt = flabel*binary_closing(binlab, footprint)
803        crop_positions = ut.positionsIn2DBBox( positions, bbox )
804
805        # draw the line only in the cell to split
806        line = np.zeros(cropt.shape, dtype="uint8")
807        for i, pos in enumerate(crop_positions):
808            if cropt[round(pos[0]), round(pos[1])] == curlabel:
809                line[round(pos[0]), round(pos[1])] = 1
810            if (i > 0):
811                prev = (crop_positions[i-1][0], crop_positions[i-1][1])
812                cur = (pos[0], pos[1])
813                interp_coords = interpolate_coordinates(prev, cur, 1)
814                for ic in interp_coords:
815                    line[tuple(np.round(ic).astype(int))] = 1
816        self.move_in_crop( curlabel, init_cropt, cropt, crop_positions, line, bbox, tframe, retry=0)

Redraw the two labels separated by a line drawn manually

def move_in_crop( self, curlabel, init_cropt, cropt, crop_positions, line, bbox, frame, retry):
818    def move_in_crop(self, curlabel, init_cropt, cropt, crop_positions, line, bbox, frame, retry):
819        """ Move the junction in the cropped region """
820        dis = retry
821        footprint = disk(radius=dis)
822        dilline = binary_dilation(line, footprint=footprint)
823
824        # get the two splitted regions and relabel one of them
825        clab = np.zeros(cropt.shape, dtype="uint8")
826        clab[cropt==curlabel] = 1
827        clab[dilline] = 0
828        labels = label(clab, background=0, connectivity=1)
829        if (np.max(labels) == 2) & (np.sum(labels==1)>self.epicure.minsize) & (np.sum(labels==2)>self.epicure.minsize):
830            ## get new image with the 2 cells to retrack
831            labels = ut.touching_labels(labels, expand=dis+1)
832            indmodif = []
833            newlabels = []
834            for i in range(2):
835                imodif = ( (labels==(i+1)) & (cropt==curlabel) )
836                val, counts = np.unique( init_cropt[ imodif ], return_counts=True) 
837                init_label = val[np.argmax(counts)]
838                imodif = np.argwhere(imodif).tolist()
839                indmodif = indmodif + imodif
840                newlabels = newlabels + np.repeat( init_label, len(imodif) ).tolist()
841            
842            indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
843            
844            # remove the boundary between the two updated labels only
845            cind_bound = ut.ind_boundaries( labels )
846            ind_bound = [ ind for ind in cind_bound if cropt[tuple(ind)]==curlabel ]
847            ind_bound = ut.toFullMoviePos( ind_bound, bbox, frame )
848            indmodif = np.vstack((indmodif, ind_bound))
849            newlabels = newlabels + np.repeat(0, len(ind_bound)).tolist()
850            
851            self.epicure.change_labels( indmodif, newlabels )
852            ## udpate the centroid of the modified labels
853            #for clabel in np.unique(newlabels):
854            #    if clabel > 0:
855            #        self.epicure.update_centroid( clabel, frame )
856        else:
857            if (retry > 6) :
858                if self.epicure.verbose > 0:
859                    print("Update failed "+str(np.max(labels)))
860                return
861            retry = retry + 1
862            self.move_in_crop(curlabel, init_cropt, cropt, crop_positions, line, bbox, frame, retry=retry)

Move the junction in the cropped region

def split_along_line(self, tframe, positions):
864    def split_along_line(self, tframe, positions):
865        """ Split a label along a line drawn manually """
866        bbox = ut.getBBox2DFromPts( positions, extend=0, imshape=self.epicure.imgshape2D )
867        bbox = ut.extendBBox2D( bbox, extend_factor=1.25, imshape=self.epicure.imgshape2D )
868
869        segt = self.epicure.seglayer.data[tframe]
870        cropt = ut.cropBBox2D( segt, bbox )
871        crop_positions = ut.positionsIn2DBBox( positions, bbox )
872
873        # get the value of the cell to split (most frequent label along the line)
874        curlabels = []
875        prev_pos = None
876        for pos in crop_positions:
877            if (prev_pos is None) or ((round(pos[0]) != round(prev_pos[0])) and (round(pos[1]) != round(prev_pos[1]) )):
878                clab = cropt[round(pos[0]), round(pos[1])]
879                curlabels.append(clab)
880                prev_pos = pos
881                
882        curlabel = max(set(curlabels), key=curlabels.count)
883        if self.epicure.verbose > 0:
884            print("Cell to split: "+str(curlabel))
885        if curlabel == 0:
886            if self.epicure.verbose > 0:
887                print("Refusing to split background")
888            return               
889                        
890        ## crop around selected label
891        bbox = ut.getBBox2D(segt, curlabel)
892        bbox = ut.extendBBox2D( bbox, extend_factor=1.5, imshape=self.epicure.imgshape2D )
893        cropt = ut.cropBBox2D( segt, bbox )
894        crop_positions = ut.positionsIn2DBBox( positions, bbox )
895
896        # draw the line only in the cell to split
897        line = np.zeros(cropt.shape, dtype="uint8")
898        for i, pos in enumerate(crop_positions):
899            if cropt[round(pos[0]), round(pos[1])] == curlabel:
900                line[round(pos[0]), round(pos[1])] = 1
901            if (i > 0):
902                prev = (crop_positions[i-1][0], crop_positions[i-1][1])
903                cur = (pos[0], pos[1])
904                interp_coords = interpolate_coordinates(prev, cur, 1)
905                for ic in interp_coords:
906                    line[tuple(np.round(ic).astype(int))] = 1
907        self.split_in_crop( curlabel, cropt, crop_positions, line, bbox, tframe, retry=0)

Split a label along a line drawn manually

def split_in_crop(self, curlabel, cropt, crop_positions, line, bbox, frame, retry):
909    def split_in_crop(self, curlabel, cropt, crop_positions, line, bbox, frame, retry):
910        """ Find the split to do in the cropped region """
911        dis = retry
912        footprint = disk(radius=dis)
913        dilline = binary_dilation(line, footprint=footprint)
914
915        # get the two splitted regions and relabel one of them
916        clab = np.zeros(cropt.shape, dtype="uint8")
917        clab[cropt==curlabel] = 1
918        clab[dilline] = 0
919        labels = label(clab, background=0, connectivity=1)
920        if (np.max(labels) == 2) & (np.sum(labels==1)>self.epicure.minsize) & (np.sum(labels==2)>self.epicure.minsize):
921            ## get new image with the 2 cells to retrack
922            labels = ut.touching_labels(labels, expand=dis+1)
923            curframe = np.zeros( cropt.shape, dtype="uint8" )
924            for i in range(2):
925                curframe[ (labels==(i+1)) & (cropt==curlabel) ] = i+1
926            
927            curframe = ut.remove_boundaries(curframe)
928            self.propagate_label_change( curframe, [1,2], bbox, frame, [curlabel] )
929
930        else:
931            if (retry > 6) :
932                if self.epicure.verbose > 0:
933                    print("Split failed "+str(np.max(labels)))
934                return
935            retry = retry + 1
936            self.split_in_crop(curlabel, cropt, crop_positions, line, bbox, frame, retry=retry)

Find the split to do in the cropped region

def merge_labels(self, tframe, startlab, endlab, extend_factor=1.25):
938    def merge_labels(self, tframe, startlab, endlab, extend_factor=1.25):
939        """ Merge the two given labels """
940        start_time = ut.start_time()
941        segt = self.epicure.seglayer.data[tframe]
942        
943        ## Crop around labels to work on smaller field of view
944        bbox, merged = ut.getBBox2DMerge( segt, startlab, endlab )
945        
946        ## keep only the region of interest
947        bbox = ut.extendBBox2D( bbox, extend_factor, self.epicure.imgshape2D )
948        segt_crop = ut.cropBBox2D( segt, bbox )
949
950        ## check that labels can be merged
951        touch = ut.checkTouchingLabels( segt_crop, startlab, endlab )
952        if not touch:
953            ut.show_warning("Labels not touching, I refuse to merge them")
954            return
955
956        ## merge the two labels together
957        joinlab = ut.cropBBox2D( merged, bbox )
958        footprint = disk(radius=2)
959        joinlab = endlab * binary_closing(joinlab, footprint)
960        
961        if self.epicure.verbose > 1:
962            ut.show_duration(start_time, "Merged in ")
963
964        ## update and propagate the change
965        self.propagate_label_change(joinlab, [endlab], bbox, tframe, [startlab, endlab])
966        if self.epicure.verbose > 1:
967            ut.show_duration(start_time, "Merged and propagated in ")

Merge the two given labels

def touching_labels(self, img, lab, olab):
969    def touching_labels(self, img, lab, olab):
970        """ Check if the two labels are neighbors or not """
971        flab = find_boundaries(img==lab)
972        folab = find_boundaries(img==olab)
973        return np.sum(np.logical_and(flab, folab))>0

Check if the two labels are neighbors or not

def swap_labels(self, tframe, lab, olab):
975    def swap_labels(self, tframe, lab, olab):
976        """ Swap two labels """
977        segt = self.epicure.seglayer.data[tframe]
978        ## Get the two labels position to swap
979        modiflab = np.argwhere(segt==lab).tolist()
980        modifolab = np.argwhere(segt==olab).tolist()
981        newlabs = np.repeat(olab, len(modiflab)).tolist() + np.repeat(lab, len(modifolab)).tolist()
982        ## Change the labels
983        ut.setNewLabel( self.epicure.seglayer, modiflab+modifolab, newlabs, add_frame=tframe )
984        ## Update the tracks and graph with swap
985        self.epicure.swap_labels( lab, olab, tframe )
986        self.epicure.seglayer.refresh()

Swap two labels

def remove_border(self):
 991    def remove_border(self):
 992        """ Remove all cells that touch the border """
 993        start_time = ut.start_time()
 994        self.viewer.window._status_bar._toggle_activity_dock(True)
 995        size = int(self.border_size.text())
 996        if size == 0:
 997            for i in progress(range(0, self.epicure.nframes)):
 998                img = np.copy( self.epicure.seglayer.data[i] )
 999                resimg = clear_border( img )
1000                self.epicure.seglayer.data[i] = resimg
1001                self.epicure.removed_labels( img, resimg, i )
1002        else:
1003            maxx = self.epicure.imgshape2D[0] - size - 1
1004            maxy = self.epicure.imgshape2D[1] - size - 1
1005            for i in progress(range(0, self.epicure.nframes)):
1006                frame = self.epicure.seglayer.data[i]
1007                img = np.copy( frame ) 
1008                crop_img = img[ size:maxx, size:maxy ]
1009                crop_img = clear_border( crop_img )
1010                frame[0:size, :] = 0
1011                frame[:, 0:size] = 0
1012                frame[maxx:, :] = 0
1013                frame[:, maxy:] = 0
1014                frame[size:maxx, size:maxy] = crop_img
1015                ## update the tracks after the potential disappearance of some cells
1016                self.epicure.removed_labels( img, frame, i )
1017        
1018        self.viewer.window._status_bar._toggle_activity_dock(False)
1019        self.epicure.seglayer.refresh()
1020        if self.epicure.verbose > 0:
1021            ut.show_duration( start_time, "Border cells removed in ")

Remove all cells that touch the border

def remove_smalls(self):
1025    def remove_smalls( self ):
1026        """ Remove all cells smaller than given area (in nb pixels) """
1027        start_time = ut.start_time()
1028        self.viewer.window._status_bar._toggle_activity_dock(True)
1029        for i in progress(range(0, self.epicure.nframes)):
1030            self.remove_small_cells( np.copy(self.epicure.seglayer.data[i]), i)
1031        self.viewer.window._status_bar._toggle_activity_dock(False)
1032        if self.epicure.verbose > 0:
1033            ut.show_duration( start_time, "Small cells removed in ")

Remove all cells smaller than given area (in nb pixels)

def remove_small_cells(self, img, frame):
1035    def remove_small_cells(self, img, frame):
1036        """ Remove if few the cell is only few pixels """
1037        #init_labels = set(np.unique(img))
1038        minarea = int(self.small_size.text())
1039        props = ut.labels_properties( img )
1040        resimg = np.copy( img )
1041        for prop in props:
1042            if prop.area < minarea:
1043                (resimg[prop.slice])[prop.image] = 0
1044        ## update the tracks after the potential disappearance of some cells
1045        self.epicure.seglayer.data[frame] = resimg
1046        self.epicure.removed_labels( img, resimg, frame )

Remove if few the cell is only few pixels

def merge_inside_cells(self):
1048    def merge_inside_cells( self ):
1049        """ Merge cell that falls inside another cell with ut """
1050        start_time = ut.start_time()
1051        self.viewer.window._status_bar._toggle_activity_dock(True)
1052        for i in progress(range(0, self.epicure.nframes)):
1053            self.merge_inside_cell(self.epicure.seglayer.data[i], i)
1054        self.viewer.window._status_bar._toggle_activity_dock(False)
1055        if self.epicure.verbose > 0:
1056            ut.show_duration( start_time, "Inside cells merged in ")

Merge cell that falls inside another cell with ut

def merge_inside_cell(self, img, frame):
1058    def merge_inside_cell( self, img, frame ):
1059        """ Merge cells that fits inside the convex hull of a cell with it """
1060        graph = ut.connectivity_graph( img, distance=3)
1061        adj_bg = []
1062        
1063        nodes = list(graph.nodes)
1064        for label in nodes:
1065            nneighbor = len(graph.adj[label])
1066            if nneighbor == 1:
1067                neigh_label = graph.adj[label]
1068                for lab in neigh_label.keys():
1069                    nlabel = int( lab )
1070                # both labels are still present in the current frame
1071                if nlabel>0 and sum( np.isin( [label, nlabel], self.epicure.seglayer.data[frame] ) ) == 2:
1072                    self.merge_labels( frame, label, nlabel, 1.05 )
1073                    if self.epicure.verbose > 0:
1074                        print( "Merged label "+str(label)+" into label "+str(nlabel)+" at frame "+str(frame) )

Merge cells that fits inside the convex hull of a cell with it

def create_shapelayer(self):
1078    def create_shapelayer( self ):
1079        """ Create the layer that handle temporary drawings """
1080        shapes = []
1081        shap = self.viewer.add_shapes( shapes, name=self.shapelayer_name, ndim=3, blending="additive", opacity=1, edge_width=2, scale=self.viewer.layers["Segmentation"].scale )
1082        shap.text.visible = False
1083        shap.visible = False

Create the layer that handle temporary drawings

def show_hide_seedMapBlock(self):
1087    def show_hide_seedMapBlock(self):
1088        self.gSeed.setVisible(not self.gSeed.isVisible())
1089        if not self.gSeed.isVisible():
1090            ut.remove_layer(self.viewer, "Seeds")
def create_seedsBlock(self):
1092    def create_seedsBlock(self):
1093        seed_layout = wid.vlayout()
1094        reset_color = self.epicure.get_resetbtn_color()
1095        seed_createbtn = wid.add_button( btn="Create seeds layer", btn_func=self.reset_seeds, descr="Create/reset the layer to add seeds", color=reset_color )
1096        seed_layout.addWidget(seed_createbtn)
1097        seed_loadbtn = wid.add_button( btn="Load seeds from previous time point", btn_func=self.get_seeds_from_prev, descr="Place seeds in background area where cells are in previous time point" )
1098        seed_layout.addWidget(seed_loadbtn)
1099        
1100        ## choose method and segment from seeds
1101        gseg, gseg_layout = wid.group_layout( "Seed based segmentation" )
1102        seed_btn = wid.add_button( btn="Segment cells from seeds", btn_func=self.segment_from_points, descr="Segment new cells from placed seeds" )
1103        gseg_layout.addWidget(seed_btn)
1104        method_line, self.seed_method = wid.list_line( label="Method", descr="Seed based segmentation method to segment some cells" )
1105        self.seed_method.addItem("Intensity-based (watershed)")
1106        self.seed_method.addItem("Distance-based")
1107        self.seed_method.addItem("Diffusion-based")
1108        gseg_layout.addLayout( method_line )
1109        maxdist, self.max_distance = wid.value_line( label="Max cell radius", default_value="100.0", descr="Max cell radius allowed in new cell creation" )
1110        gseg_layout.addLayout(maxdist)
1111        gseg.setLayout(gseg_layout)
1112        
1113        seed_layout.addWidget(gseg)
1114        self.gSeed.setLayout(seed_layout)
def create_seedlayer(self):
1116    def create_seedlayer(self):
1117        pts = []
1118        ## handle change of parameter name in napari versions
1119        if ut.version_napari_above("0.4.19"):
1120            self.viewer.add_points( np.array(pts), face_color="blue", size = 7,  border_width=0, name="Seeds", scale=self.viewer.layers["Segmentation"].scale )
1121        else:
1122            self.viewer.add_points( np.array(pts), face_color="blue", size = 7,  edge_width=0, name="Seeds", scale=self.viewer.layers["Segmentation"].scale )
def reset_seeds(self):
1124    def reset_seeds(self):
1125        ut.remove_layer(self.viewer, "Seeds")
1126        self.create_seedlayer()
def get_seeds_from_prev(self):
1128    def get_seeds_from_prev(self):
1129        #self.reset_seeds()
1130        if "Seeds" not in self.viewer.layers:
1131            self.create_seedlayer()
1132        tframe = int(self.viewer.cursor.position[0])
1133        segt = self.epicure.seglayer.data[tframe]
1134        if tframe > 0:
1135            pts = self.viewer.layers["Seeds"].data
1136            segp = self.epicure.seglayer.data[tframe-1]
1137            props = ut.labels_properties(segp)
1138            for prop in props:
1139                cent = prop.centroid
1140                ## create a seed in the centroid only in empty spaces
1141                if int(segt[int(cent[0]), int(cent[1])]) == 0:
1142                    pts = np.append(pts, [[tframe, cent[0], cent[1]]], axis=0)
1143            self.viewer.layers["Seeds"].data = pts
1144            self.viewer.layers["Seeds"].refresh()
def end_place_seed(self):
1146    def end_place_seed(self):
1147        """ Finish placing seeds mode """
1148        if not self.seed_active:
1149            return
1150        if self.old_mouse_drag is not None:
1151            self.epicure.seglayer.mouse_drag_callbacks = self.old_mouse_drag
1152            self.seed_active = False
1153            ut.show_info("End seed")
1154        ut.set_active_layer( self.viewer, "Segmentation" )

Finish placing seeds mode

def place_seed(self, event_pos):
1156    def place_seed(self, event_pos):
1157        """ Add a seed under the cursor """
1158        tframe = int(self.viewer.cursor.position[0])
1159        segt = self.epicure.seglayer.data[tframe]
1160        pts = self.viewer.layers["Seeds"].data
1161        cent = self.viewer.layers["Seeds"].world_to_data( event_pos )
1162        ## create a seed in the centroid only in empty spaces
1163        if int(segt[int(cent[1]), int(cent[2])]) == 0:
1164            pts = np.append(pts, [[tframe, cent[1], cent[2]]], axis=0)
1165            self.viewer.layers["Seeds"].data = pts
1166            self.viewer.layers["Seeds"].refresh()
1167        ut.set_active_layer( self.viewer, "Segmentation" )

Add a seed under the cursor

def segment_from_points(self):
1170    def segment_from_points(self):
1171        """ Do cells segmentation from seed points """
1172        if not "Seeds" in self.viewer.layers:
1173            ut.show_warning("No seeds placed")
1174            return
1175        self.end_place_seed()
1176        if len(self.viewer.layers["Seeds"].data) <= 0:
1177            ut.show_warning("No seeds placed")
1178            return
1179
1180        ## get crop of the image around seeds
1181        tframe = ut.current_frame(self.viewer)
1182        segBB, markers, maskBB, labelBB = self.crop_around_seeds( tframe )
1183        ## save current labels to compare afterwards
1184        before_seeding = np.copy(segBB)
1185
1186        ## segment current seeds from points with selected method
1187        if self.seed_method.currentText() == "Intensity-based (watershed)":
1188            self.watershed_from_points( tframe, segBB, markers, maskBB, labelBB )
1189        if self.seed_method.currentText() == "Distance-based":
1190            self.distance_from_points( tframe, segBB, markers, maskBB, labelBB )
1191        if self.seed_method.currentText() == "Diffusion-based":
1192            self.diffusion_from_points( tframe, segBB, markers, maskBB, labelBB )
1193
1194        ## finish segmentation: thin to have one pixel boundaries, update all
1195        skelBB = ut.frame_to_skeleton( segBB, connectivity=1 )
1196        segBB[ skelBB>0 ] = 0
1197        self.reset_seeds()
1198        ## update the list of tracks with the potential new cells
1199        self.epicure.added_labels_oneframe( tframe, before_seeding, segBB )
1200        #self.end_place_seed()
1201        ut.set_active_layer( self.viewer, "Segmentation" )
1202        self.epicure.seglayer.refresh()

Do cells segmentation from seed points

def crop_around_seeds(self, tframe):
1204    def crop_around_seeds( self, tframe ):
1205        """ Get cropped image around the seeds """
1206        ## crop around the seeds, with a margin
1207        seeds = self.viewer.layers["Seeds"].data
1208        segt = self.epicure.seglayer.data[tframe]
1209        extend = int(float(self.max_distance.text())*1.1)
1210        labelBB = ut.getBBox2DFromPts( seeds, extend, segt.shape )
1211        segBB = ut.cropBBox2D(segt, labelBB)
1212        ## mask where there are cells
1213        maskBB = np.copy(segBB)
1214        maskBB = 1*(maskBB==0)
1215        maskBB = np.uint8(maskBB)
1216        ## fill the borders
1217        maskBB = binary_erosion(maskBB, footprint=self.disk_one)
1218        ## place labels in the seed positions
1219        pos = ut.positionsIn2DBBox( seeds, labelBB )
1220        markers = np.zeros(maskBB.shape, dtype="int32")
1221        freelabs = self.epicure.get_free_labels( len(pos) )
1222        for freelab, p in zip(freelabs, pos):
1223            markers[p] = freelab
1224        return segBB, markers, maskBB, labelBB

Get cropped image around the seeds

def diffusion_from_points(self, tframe, segBB, markers, maskBB, labelBB):
1226    def diffusion_from_points(self, tframe, segBB, markers, maskBB, labelBB):
1227        """ Segment from seeds with a diffusion based method (gradient intensity slows it) """
1228        movt = self.viewer.layers["Movie"].data[tframe]
1229        imgBB = ut.cropBBox2D(movt, labelBB)
1230        markers[maskBB==0] = -1 ## block filled area 
1231        ## fill from seeds with diffusion method
1232        splitted = random_walker( imgBB, labels=markers, beta=700, tol=0.01 )
1233        new_labels = list(np.unique(markers))
1234        new_labels.remove(-1)
1235        new_labels.remove(0)
1236        i = 0
1237        lablist = set( splitted.flatten() )
1238        #print(lablist)
1239        #print(new_labels)
1240        for lab in lablist:
1241            if lab > 0:
1242                mask = (splitted == lab)
1243                labels_mask = label(mask)                       
1244                ## keep only biggest region if the label is splitted
1245                regions = ut.labels_properties(labels_mask)
1246                if len(regions) > 2:
1247                    regions.sort(key=lambda x: x.area, reverse=True)
1248                    if len(regions) > 1:
1249                        for rg in regions[1:]:
1250                            splitted[rg.coords[:,0], rg.coords[:,1]] = 0
1251                splitted[splitted==lab] = new_labels[i]
1252                i = i + 1
1253        segBB[(maskBB>0)*(splitted>0)] = splitted[(maskBB>0)*(splitted>0)]
1254        return segBB

Segment from seeds with a diffusion based method (gradient intensity slows it)

def watershed_from_points(self, tframe, segBB, markers, maskBB, labelBB):
1256    def watershed_from_points(self, tframe, segBB, markers, maskBB, labelBB):
1257        """ Performs watershed from the seed points """
1258        movt = self.viewer.layers["Movie"].data[tframe] 
1259        imgBB = ut.cropBBox2D(movt, labelBB)
1260        splitted = watershed( imgBB, markers=markers, mask=maskBB )
1261        segBB[splitted>0] = splitted[splitted>0]
1262        return segBB

Performs watershed from the seed points

def distance_from_points(self, tframe, segBB, markers, maskBB, labelBB):
1264    def distance_from_points(self, tframe, segBB, markers, maskBB, labelBB):
1265        """ Segment cells from seed points with Voronoi method """
1266        # iteratif to block when meet other fixed labels 
1267        maxdist = float(self.max_distance.text())
1268        dist = 0
1269        while dist <= maxdist:
1270            markers = ut.touching_labels( markers, expand=1 )
1271            markers[maskBB==0] = 0
1272            dist = dist + 1
1273        segBB[(maskBB>0) * (markers>0)] = markers[(maskBB>0) * (markers>0)]
1274        return segBB

Segment cells from seed points with Voronoi method

def create_cleaningBlock(self):
1280    def create_cleaningBlock(self):
1281        """ GUI for cleaning segmentation """
1282        clean_layout = wid.vlayout()
1283        ## cells on border
1284        border_line, self.border_size = wid.button_parameter_line( btn="Remove border cells", btn_func=self.remove_border, value="1", descr_btn="Remove all cell at a distance <= value (in pixels)", descr_value="Distance of the cells to be removed (in pixels)" )
1285        clean_layout.addLayout(border_line)
1286        
1287        ## too small cells
1288        small_line, self.small_size = wid.button_parameter_line( btn="Remove mini cells", btn_func=self.remove_smalls, value="4", descr_btn="Remove all cells smaller than given value (in pixels^2)", descr_value="Minimal cell area (in pixels^2)" )
1289        clean_layout.addLayout(small_line)
1290
1291        ## Cell inside another cell
1292        inside_btn = wid.add_button( btn="Cell inside another: merge", btn_func=self.merge_inside_cells, descr="Merge all small cells fully contained inside another cell to this cell" )
1293        clean_layout.addWidget(inside_btn)
1294
1295        ## sanity check
1296        sanity_btn = wid.add_button( btn="Sanity check", btn_func=self.sanity_check, descr="Check that labels and tracks are consistent with EpiCure restrictions, and try to fix some errors" )
1297        clean_layout.addWidget(sanity_btn)
1298
1299        ## reset labels
1300        reset_color = self.epicure.get_resetbtn_color()
1301        reset_btn = wid.add_button( btn="Reset all", btn_func=self.reset_all, descr="Reset all tracks, groups, suspects..", color=reset_color )
1302        clean_layout.addWidget(reset_btn)
1303
1304        self.gCleaned.setLayout(clean_layout)

GUI for cleaning segmentation

def sanity_check(self):
1308    def sanity_check(self):
1309        """ Check if everything looks okayish, in case some bug or weird editions broke things """
1310        self.viewer.window._status_bar._toggle_activity_dock(True)
1311        progress_bar = progress(total=6)
1312        progress_bar.set_description("Sanity check:")
1313        progress_bar.update(0)
1314        ## check layers presence
1315        ut.show_info("Check and reopen if necessary EpiCure layers")
1316        self.epicure.check_layers()
1317        ## check that each label is unique
1318        progress_bar.update(1)
1319        progress_bar.set_description("Sanity check: label unicity")
1320        label_list = np.unique(self.epicure.seglayer.data)
1321        if self.epicure.verbose > 0:
1322            print("Checking label unicity...")
1323        self.check_unique_labels( label_list, progress_bar )
1324        ## check and update if necessary tracks 
1325        progress_bar.update(2)
1326        if self.epicure.forbid_gaps:
1327            progress_bar.set_description("Sanity check: track gaps")
1328            ut.show_info("Check if some tracks contain gaps")
1329            gaped = self.epicure.handle_gaps( track_list=None )
1330        ## check that labels and tracks correspond
1331        progress_bar.set_description("Sanity check: label-track")
1332        progress_bar.update(3)
1333        if self.epicure.verbose > 0:
1334            print("Checking labels-tracks correspondance...")
1335        track_list = self.epicure.tracking.get_track_list()
1336        untracked = list(set(label_list) - set(track_list))
1337        if 0 in untracked:
1338            untracked.remove(0)
1339        if len(untracked) > 0:
1340            ut.show_warning("! Labels "+str(untracked)+" not in Tracks -- Adding it now")
1341            for untrack in untracked:
1342                self.epicure.add_one_label_to_track( untrack )
1343        
1344        ## update label list with changes that might have been done
1345        label_list = np.unique(self.epicure.seglayer.data)
1346        track_list = self.epicure.tracking.get_track_list()
1347        ## check if all tracks have associated labels in the image
1348        phantom_tracks = list(set(track_list) - set(label_list))
1349        if len(phantom_tracks) > 0:
1350            print("! Phantom tracks "+str(phantom_tracks)+" found")
1351            self.epicure.delete_tracks(phantom_tracks)
1352            print("-> Phantom tracks deleted from Tracks")
1353        
1354        ## checking events
1355        progress_bar.set_description("Sanity check: extrusions")
1356        progress_bar.update(5)
1357        if self.epicure.verbose > 0:
1358            print("Checking extrusion = end of track...")
1359        self.epicure.check_extrusions_sanity()
1360        
1361        ## finished
1362        if self.epicure.verbose > 0:
1363            print("Checking finished")
1364        progress_bar.close()
1365        self.viewer.window._status_bar._toggle_activity_dock(False)

Check if everything looks okayish, in case some bug or weird editions broke things

def check_unique_labels(self, label_list, progress_bar):
1367    def check_unique_labels(self, label_list, progress_bar):
1368        """ Check that all labels are contiguous and not present several times (only by frame) """
1369        found = 0
1370        s = generate_binary_structure(2,2)
1371        pbtmp = progress(total=len(label_list), desc="Check labels", nest_under=progress_bar)
1372        for i, lab in enumerate(label_list):
1373            pbtmp.update(i)
1374            if lab > 0:
1375                for frame in self.epicure.seglayer.data:
1376                    if lab in frame:
1377                        labs, num_objects = ndlabel(binary_dilation(frame==lab, footprint=s), structure=s)
1378                        if num_objects > 1:
1379                            ut.show_warning("! Problem, label "+str(lab)+" found several times")
1380                            found = found + 1
1381                            continue
1382        pbtmp.close()
1383        if found <= 0:
1384            ut.show_info("Labels unicity ok")

Check that all labels are contiguous and not present several times (only by frame)

def reset_all(self):
1389    def reset_all( self ):
1390        """ Reset labels through skeletonization, reset tracks, suspects, groups """
1391        if self.epicure.verbose > 0:
1392            ut.show_info( "Resetting everything ")
1393        self.viewer.window._status_bar._toggle_activity_dock(True)
1394        progress_bar = progress(total=5)
1395        ## get skeleton and relabel (ensure label unicity)
1396        progress_bar.update(1)
1397        progress_bar.set_description("Reset: relabel")
1398        self.epicure.reset_data()
1399        self.epicure.tracking.reset()
1400        self.epicure.reset_labels()
1401        progress_bar.update(2)
1402        progress_bar.set_description("Reset: reinit tracks")
1403        self.epicure.tracked = 0
1404        self.epicure.load_tracks(progress_bar)
1405        if self.epicure.verbose > 0:
1406            print("Resetting done")
1407        progress_bar.close()
1408        self.viewer.window._status_bar._toggle_activity_dock(False)

Reset labels through skeletonization, reset tracks, suspects, groups

def create_selectBlock(self):
1415    def create_selectBlock(self):
1416        """ GUI for handling selection with shapes """
1417        select_layout = wid.vlayout()
1418        ## create/select the ROI
1419        draw_btn = wid.add_button( btn="Draw/Select ROI", btn_func=self.draw_shape, descr="Draw or select a ROI to apply region action on" )
1420        select_layout.addWidget(draw_btn)
1421        remove_sel_btn = wid.add_button( btn="Remove cells inside ROI", btn_func=self.remove_cells_inside, descr="Remove all cells inside the selected/first ROI" )
1422        select_layout.addWidget(remove_sel_btn)
1423        remove_line, self.keep_new_cells = wid.button_check_line( btn="Remove cells outside ROI", btn_func=self.remove_cells_outside, check="Keep new cells", checked=True, checkfunc=None, descr_btn="Remove all cells outside the current ROI", descr_check="Keep new cells tah appear in the ROI in later frames" )
1424        select_layout.addLayout(remove_line)
1425
1426        self.gSelect.setLayout(select_layout)

GUI for handling selection with shapes

def draw_shape(self):
1428    def draw_shape(self):
1429        """ Draw/select a shape in the Shapes layer """
1430        if self.shapelayer_name not in self.viewer.layers:
1431            self.create_shapelayer()
1432        ut.set_active_layer(self.viewer, self.shapelayer_name)
1433        lay = self.viewer.layers[self.shapelayer_name]
1434        lay.visible = True
1435        lay.opacity = 0.5

Draw/select a shape in the Shapes layer

def get_selection(self):
1437    def get_selection(self):
1438        """ Get the active (or first) selection """
1439        if self.shapelayer_name not in self.viewer.layers:
1440            return None
1441        lay = self.viewer.layers[self.shapelayer_name]
1442        selected = lay.selected_data
1443        if len(selected) == 0:
1444            if len(lay.shape_type) == 1:
1445                if self.epicure.verbose > 1:
1446                    print("No shape selected, use the only one present")
1447                lay.selected_data.add(0)
1448                selected = lay.selected_data
1449            else:
1450                ut.show_warning("No shape selected, do nothing")
1451                return None
1452        return lay.data[list(selected)[0]] 

Get the active (or first) selection

def get_labels_inside(self):
1454    def get_labels_inside(self):
1455        """ Get the list of labels inside the current ROI """
1456        current_shape = self.get_selection()
1457        if current_shape is None:
1458            return None
1459        self.current_bbox = ut.getBBox2DFromPts(current_shape, 30, self.epicure.imgshape2D)
1460        self.current_cropshape = ut.positionsIn2DBBox(current_shape, self.current_bbox )
1461        tframe = ut.current_frame(self.viewer)
1462        segt = self.epicure.seglayer.data[tframe]
1463        croped = ut.cropBBox2D(segt, self.current_bbox)
1464        labprops = ut.labels_properties(croped)
1465        inside = points_in_poly( [lab.centroid for lab in labprops], self.current_cropshape )
1466        toedit = [lab.label for i, lab in enumerate(labprops) if inside[i] ]
1467        return toedit

Get the list of labels inside the current ROI

def remove_cells_outside(self):
1469    def remove_cells_outside(self):
1470        """ Remove all labels centroids outside the selected ROI """
1471        tokeep = self.get_labels_inside()
1472        if self.keep_new_cells.isChecked():
1473            tframe = ut.current_frame(self.viewer)
1474            segt = self.epicure.seglayer.data[tframe]
1475            toremove = set(np.unique(segt).flatten()) - set(tokeep)
1476            self.epicure.remove_labels(list(toremove))
1477        else:
1478            self.epicure.keep_labels(tokeep)
1479        lay = self.viewer.layers[self.shapelayer_name]
1480        lay.remove_selected()
1481        self.epicure.finish_update()

Remove all labels centroids outside the selected ROI

def remove_cells_inside(self):
1483    def remove_cells_inside(self):
1484        """ Remove all labels centroids inside the selected ROI """
1485        toremove = self.get_labels_inside()
1486        self.epicure.remove_labels(toremove)
1487        lay = self.viewer.layers[self.shapelayer_name]
1488        lay.remove_selected()
1489        self.epicure.finish_update()

Remove all labels centroids inside the selected ROI

def lock_cells_inside(self):
1491    def lock_cells_inside(self):
1492        """ Check all cells inside the selected ROI into current group """
1493        tocheck = self.get_labels_inside()
1494        for lab in tocheck:
1495            self.check_label(lab)
1496        if self.epicure.verbose > 0:
1497            print(str(len(tocheck))+" cells checked in group "+str(self.check_group.text()))
1498        lay = self.viewer.layers[self.shapelayer_name]
1499        lay.remove_selected()
1500        self.epicure.finish_update()

Check all cells inside the selected ROI into current group

def group_classify_intensity(self):
1502    def group_classify_intensity( self ):
1503        """ Calls the interface to classify cells by intensity """
1504        self.classif.update()
1505        self.classif.show()

Calls the interface to classify cells by intensity

def group_classify_event(self):
1507    def group_classify_event( self ):
1508        """ Calls the interface to classify cells by event interaction """
1509        self.classif_event.update()
1510        self.classif_event.show()

Calls the interface to classify cells by event interaction

def group_event_cells(self, event_type):
1512    def group_event_cells( self, event_type ):
1513        """ Classify the cells that finished with the selected event into the event group """
1514        events = self.epicure.inspecting.get_events_from_type( event_type )
1515        if len( events ) > 0:
1516            tids = []
1517            for evt_sid in events:
1518                pos, label = self.epicure.inspecting.get_event_infos( evt_sid )
1519                if label not in tids:
1520                    tids.append(label)
1521            group_name = "Cells_"+event_type
1522            if event_type == "extrusion":
1523                group_name = "Extruding"
1524            if event_type == "division":    
1525                group_name = "Dividing"
1526            self.group_choice.setCurrentText(group_name)
1527            self.epicure.reset_group( group_name ) 
1528            self.redraw_clear_group( group_name )
1529            self.group_labels( tids )

Classify the cells that finished with the selected event into the event group

def group_positive_cells(self, layer_name, meth, min_frame, max_frame, threshold):
1532    def group_positive_cells( self, layer_name, meth, min_frame, max_frame, threshold ):
1533        """ Classify the cells with mean intensity in the given frame range above threshold into the current group """
1534        if self.group_choice.currentText() == "":
1535            ut.show_warning("Write a group name before")
1536            return
1537        layer = self.viewer.layers[layer_name]
1538        frames = np.arange(min_frame, max_frame+1)
1539        if (min_frame == 0) and (max_frame == self.epicure.nframes-1):
1540            frames = None
1541        tracks, mean_int = self.epicure.tracking.measure_intensity_features( "intensity_"+meth, intimg=layer.data, frames=frames )
1542        tids = tracks[ mean_int > threshold ]
1543        self.redraw_clear_group( group=None )
1544        self.group_labels( tids )

Classify the cells with mean intensity in the given frame range above threshold into the current group

def group_cells_inside(self):
1546    def group_cells_inside(self):
1547        """ Put all cells inside the selected ROI into current group """
1548        if self.group_choice.currentText() == "":
1549            ut.show_warning("Write a group name before")
1550            return
1551        tocheck = self.get_labels_inside()
1552        if tocheck is None:
1553            if self.epicure.verbose > 0:
1554                print("No cell to add to group")
1555            return
1556        self.group_labels( tocheck )
1557        if self.epicure.verbose > 0:
1558            print(str(len(tocheck))+" cells assigend to group "+str(self.group_choice.currentText()))
1559        lay = self.viewer.layers[self.shapelayer_name]
1560        lay.remove_selected()
1561        self.epicure.finish_update()

Put all cells inside the selected ROI into current group

def create_groupCellsBlock(self):
1566    def create_groupCellsBlock(self):
1567        """ Create subpanel of Cell group options """
1568        group_layout = wid.vlayout()
1569        groupgr, self.group_choice = wid.list_line( label="Group name", descr="Choose/Set the current group name" )
1570        group_layout.addLayout(groupgr)
1571        self.group_choice.setEditable(True)
1572
1573        self.group_show = wid.add_check( check="Show groups", checked=False, check_func=self.see_groups, descr="Add a layer with the cells colored by group" )
1574        group_layout.addWidget(self.group_show)
1575
1576        reset_line, self.reset_list = wid.button_list( btn="Reset group", func=self.reset_group, descr="Remove chosen group (or all) and cell assignation to this group" )
1577        group_layout.addLayout( reset_line )
1578        self.update_group_lists()
1579        group_sel_btn = wid.add_button( btn="Cells inside ROI to group", btn_func=self.group_cells_inside, descr="Add all cells inside ROI to the current group" )
1580        group_layout.addWidget(group_sel_btn)
1581
1582        ## add button for intensity classifier interface
1583        group_class_btn = wid.add_button( btn="Group from track intensity..", btn_func=self.group_classify_intensity, descr="Open interface to group cells based on their mean intensity" )
1584        group_layout.addWidget( group_class_btn )
1585        
1586        ## add button for events classifier interface
1587        group_event_btn = wid.add_button( btn="Group from events..", btn_func=self.group_classify_event, descr="Open interface to group cells according to if they are related to an event (dividing cell, extruding cell..)" )
1588        group_layout.addWidget( group_event_btn )
1589
1590        self.gGroup.setLayout(group_layout)

Create subpanel of Cell group options

def load_checked(self):
1592    def load_checked(self):
1593        cfile = self.get_filename("_checked.txt")
1594        with open(cfile) as infile:
1595            labels = infile.read().split(";")
1596        for lab in labels:
1597            self.check_load_label(lab)
1598        ut.show_info("Checked cells loaded")
def reset_group(self):
1600    def reset_group( self ):
1601        gr = self.reset_list.currentText()
1602        if gr != "All":
1603            self.redraw_clear_group( gr )
1604        self.epicure.reset_group( gr )
1605        if gr == "All":
1606            self.see_groups()
def update_group_choice(self, group):
1608    def update_group_choice( self, group ):
1609        """ Check if group has been added in the list choices of group """
1610        if self.group_choice.findText( group ) < 0:
1611            ## not added yet. If user is typing the name and did not press enter, it can be still in edition mode, so not added
1612            self.group_choice.addItem( group )

Check if group has been added in the list choices of group

def update_group_lists(self):
1614    def update_group_lists( self ):
1615        """ Update list of groups for reset button """
1616        curchoice = self.group_choice.currentText()
1617        curreset = self.reset_list.currentText()
1618        self.group_choice.clear()
1619        self.reset_list.clear()
1620        self.reset_list.addItem("All")
1621        for group in self.epicure.groups.keys():
1622            self.update_group_choice( group )
1623            self.reset_list.addItem( group )
1624        self.reset_list.setCurrentText("All")
1625        if self.reset_list.findText( curreset ) >= 0:
1626            self.reset_list.setCurrentText(curreset)
1627        if self.group_choice.findText( curchoice ) >= 0:
1628            self.group_choice.setCurrentText( curchoice )

Update list of groups for reset button

def save_groups(self):
1630    def save_groups(self):
1631        groupfile = self.get_filename("_groups.txt")
1632        with open(groupfile, 'w') as out:
1633            out.write(";".join(group.write_group() for group in self.epicure.groups))
1634        ut.show_info("Cell groups saved in "+groupfile)
def see_groups(self):
1636    def see_groups(self):
1637        if self.group_show.isChecked():
1638            ut.remove_layer(self.viewer, self.grouplayer_name)
1639            grouped = self.epicure.draw_groups()
1640            self.viewer.add_labels(grouped, name=self.grouplayer_name, opacity=0.75, blending="additive", scale=self.viewer.layers["Segmentation"].scale)
1641            ut.set_active_layer(self.viewer, "Segmentation")
1642        else:
1643            ut.remove_layer(self.viewer, self.grouplayer_name)
1644            ut.set_active_layer(self.viewer, "Segmentation")
def group_labels(self, labels):
1646    def group_labels( self, labels ):
1647        """ Add label(s) to group """
1648        if self.group_choice.currentText() == "":
1649            ut.show_warning("Write group name before")
1650            return
1651        group = self.group_choice.currentText()
1652        self.group_ingroup( labels, group )

Add label(s) to group

def check_label(self, label):
1654    def check_label(self, label):
1655        """ Mark label as checked """
1656        group = self.check_group.text()
1657        self.check_ingroup(label, group)

Mark label as checked

def group_ingroup(self, labels, group):
1660    def group_ingroup(self, labels, group):
1661        """ Add the given label to chosen group """
1662        self.epicure.cells_ingroup( labels, group )
1663        if self.grouplayer_name in self.viewer.layers:
1664            self.redraw_label_group( labels, group )

Add the given label to chosen group

def check_load_label(self, labelstr):
1666    def check_load_label(self, labelstr):
1667        """ Read the label to check from file """
1668        res = labelstr.split("-")
1669        cellgroup = res[0]
1670        celllabel = int(res[1])
1671        self.check_ingroup(celllabel, cellgroup)

Read the label to check from file

def add_cell_to_group(self, event):
1673    def add_cell_to_group(self, event):
1674        """ Add cell under click to the current group """
1675        label = ut.getCellValue( self.epicure.seglayer, event ) 
1676        self.group_labels( [label] )

Add cell under click to the current group

def remove_cell_group(self, event):
1678    def remove_cell_group(self, event):
1679        """ Remove the cell from the group it's in if any """
1680        label = ut.getCellValue( self.epicure.seglayer, event ) 
1681        self.epicure.cell_removegroup( label )
1682        if self.grouplayer_name in self.viewer.layers:
1683            self.redraw_label_group( [label], 0 )

Remove the cell from the group it's in if any

def redraw_clear_group(self, group=None):
1685    def redraw_clear_group( self, group=None ):
1686        """ Clear all the cells from group in the current group layer """
1687        if group is None:
1688            if self.group_choice.currentText() == "":
1689                ut.show_warning("Write group name before")
1690                return
1691            group = self.group_choice.currentText()
1692        if self.grouplayer_name in self.viewer.layers:
1693            lay = self.viewer.layers[self.grouplayer_name]
1694            igroup = self.epicure.get_group_index(group) + 1
1695            if igroup == 0:
1696                ## the group was not present, igroup is -1
1697                return
1698            lay.data[lay.data == igroup] = 0
1699            lay.refresh()
1700            ut.set_active_layer(self.viewer, "Segmentation")

Clear all the cells from group in the current group layer

def redraw_label_group(self, labels, group):
1702    def redraw_label_group(self, labels, group):
1703        """ Update the Group layer for label """
1704        lay = self.viewer.layers[self.grouplayer_name]
1705        if group == 0:
1706            lay.data[ np.isin( self.epicure.seg, labels ) ] = 0
1707        else:
1708            igroup = self.epicure.get_group_index(group) + 1
1709            lay.data[ np.isin( self.epicure.seg, labels)  ] = igroup
1710        lay.refresh()

Update the Group layer for label

def add_overlay_message(self):
1713    def add_overlay_message(self):
1714        text = self.epicure.text + "\n"
1715        ut.setOverlayText(self.viewer, text, size=10)
def add_extrusion(self, labela, frame):
1718    def add_extrusion( self, labela, frame ):
1719        """ Add an extrusion event, given the label and frame """
1720
1721        if (frame != self.epicure.tracking.get_last_frame( labela )):
1722            if self.epicure.verbose > 0:
1723                print("Clicked label is not the last of the track, don't add extrusion")
1724                return
1725
1726        ## add extrusion to event list (if active)
1727        self.epicure.inspecting.add_extrusion( labela, frame )

Add an extrusion event, given the label and frame

def add_division(self, labela, labelb, frame):
1729    def add_division( self, labela, labelb, frame ):
1730        """ Add a division event, given the labels of the two daughter cells """
1731        if frame == 0:
1732            if self.epicure.verbose > 0:
1733                print("Cannot define a division before the first frame")
1734            return False
1735
1736        if (frame != self.epicure.tracking.get_first_frame( labela )) or (frame != self.epicure.tracking.get_first_frame(labelb) ):
1737            if self.epicure.verbose > 0:
1738                print("One daughter track is not starting at current frame, don't add division")
1739                return False
1740
1741        ## merge the two labels to find their parent
1742        bbox, merge = ut.getBBox2DMerge( self.epicure.seglayer.data[frame], labela, labelb )
1743        twoframes = ut.crop_twoframes( self.epicure.seglayer.data, bbox, frame )
1744        crop_merge = ut.cropBBox2D( merge, bbox )
1745        twoframes[1] = crop_merge # merge of the labels and 0 outside
1746            
1747        ## keep only parent labels that stop at the previous frame
1748        twoframes = self.keep_orphans(twoframes, frame)
1749        ## do mini-tracking to assign most likely parent
1750        parent = self.get_parents( twoframes, [1] )
1751        if self.epicure.verbose > 0:
1752            print( "Found parent "+str(parent[0])+" to clicked cells "+str(labela)+" and "+str(labelb) )
1753        ## add division to graph
1754        if parent is not None and parent[0] is not None:
1755            self.epicure.tracking.add_division( labela, labelb, parent[0] )
1756            ## add division to event list (if active)
1757            self.epicure.inspecting.add_division_event( labela, labelb, parent[0], frame )
1758            return True
1759        return False

Add a division event, given the labels of the two daughter cells

def key_tracking_binding(self):
1762    def key_tracking_binding(self):
1763        """ active key bindings for tracking options """
1764        self.epicure.overtext["trackedit"] = "---- Track editing ---- \n"
1765        strack = self.epicure.shortcuts["Tracks"]
1766        etrack = self.epicure.shortcuts["Events"]
1767        self.epicure.overtext["trackedit"] += ut.print_shortcuts( strack )
1768        
1769        @self.epicure.seglayer.mouse_drag_callbacks.append
1770        def manual_add_extrusion(layer, event):
1771            ### add an event of an extrusion under the click
1772            if ut.shortcut_click_match( etrack["add extrusion"], event ):
1773                # get the start and last labels
1774                labela = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1775                tframe = int(event.position[0])
1776                    
1777                if labela == 0:
1778                    if self.epicure.verbose > 0:
1779                        print("Clicked position is not a cell, do nothing")
1780                    return
1781                self.add_extrusion( labela, tframe )
1782        
1783        @self.epicure.seglayer.mouse_drag_callbacks.append
1784        def manual_add_division(layer, event):
1785            ### add an event of a division, selecting the two daughter cells
1786            if ut.shortcut_click_match( etrack["add division"], event ):
1787                # get the start and last labels
1788                labela = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1789                start_pos = event.position
1790                yield
1791                while event.type == 'mouse_move':
1792                    yield
1793                labelb = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1794                end_pos = event.position
1795                tframe = int(event.position[0])
1796                    
1797                if labela == 0 or labelb == 0:
1798                    if self.epicure.verbose > 0:
1799                        print("One position is not a cell, do nothing")
1800                    return
1801                self.add_division( labela, labelb, tframe )
1802        
1803        @self.epicure.seglayer.bind_key( strack["lineage color"]["key"], overwrite=True )
1804        def color_tracks_lineage(seglayer):
1805            if self.tracklayer_name in self.viewer.layers:
1806                self.epicure.tracking.color_tracks_by_lineage()
1807        
1808        @self.epicure.seglayer.bind_key( strack["show"]["key"], overwrite=True )
1809        def see_tracks(seglayer):
1810            if self.tracklayer_name in self.viewer.layers:
1811                tlayer = self.viewer.layers[self.tracklayer_name]
1812                tlayer.visible = not tlayer.visible
1813
1814        @self.epicure.seglayer.bind_key( strack["mode"]["key"], overwrite=True)
1815        def edit_track(layer):
1816            self.label_tr = None 
1817            self.start_label = None
1818            self.interp_labela = None
1819            self.interp_labelb = None
1820            ut.show_info("Tracks editing mode")
1821            self.old_mouse_drag, self.old_key_map = ut.clear_bindings(self.epicure.seglayer)
1822
1823            @self.epicure.seglayer.mouse_drag_callbacks.append
1824            def click(layer, event):
1825                """ Edit tracking """
1826                if event.type == "mouse_press":
1827                  
1828                    """ Merge two tracks, spatially or temporally: left click, select the first label """
1829                    if ut.shortcut_click_match( strack["merge first"], event ):
1830                        self.start_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1831                        self.start_pos = event.position
1832                        # move one frame after for next cell to link
1833                        #ut.set_frame( self.epicure.viewer, event.position[0]+1 )
1834                        return
1835                    """ Merge two tracks, spatially or temporally: right click, select the second label """
1836                    if ut.shortcut_click_match( strack["merge second"], event ):
1837                        if self.start_label is None:
1838                            if self.epicure.verbose > 0:
1839                                print("No left click done before right click, don't merge anything")
1840                            return
1841                        end_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
1842                        end_pos = event.position
1843                        if self.epicure.verbose > 0:
1844                            print("Merging track "+str(self.start_label)+" with track "+str(end_label))
1845                        
1846                        if self.start_label is None or self.start_label == 0 or end_label == 0:
1847                            if self.epicure.verbose > 0:
1848                                print("One position is not a cell, do nothing")
1849                            return
1850                        ## ready, merge
1851                        self.merge_tracks( self.start_label, self.start_pos, end_label, end_pos )
1852                        self.end_track_edit()
1853                        return
1854
1855                    ### Split the track in 2: new label for the next frames 
1856                    if ut.shortcut_click_match( strack["split track"], event ):
1857                        start_frame = int(event.position[0])
1858                        label = ut.getCellValue(self.epicure.seglayer, event) 
1859                        self.epicure.split_track( label, start_frame )
1860                        self.end_track_edit()
1861                        return
1862                        
1863                    ### Swap the two track from the current frame 
1864                    if ut.shortcut_click_match( strack["swap"], event ):
1865                        start_frame = int(event.position[0])
1866                        label = ut.getCellValue(self.epicure.seglayer, event) 
1867                        yield
1868                        while event.type == 'mouse_move':
1869                            yield
1870                        end_label = self.epicure.seglayer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)                           
1871                            
1872                        if label == 0 or end_label == 0:
1873                            if self.epicure.verbose > 0:
1874                                print("One position is not a cell, do nothing")
1875                            return
1876
1877                        self.epicure.swap_tracks( label, end_label, start_frame )
1878                            
1879                        if self.epicure.verbose > 0:
1880                            ut.show_info("Swapped track "+str(label)+" with track "+str(end_label)+" from frame "+str(start_frame))
1881                        self.end_track_edit()
1882                        return
1883
1884                    # Manual tracking: get a new label and spread it to clicked cells on next frames
1885                    if ut.shortcut_click_match( strack["start manual"], event ):
1886                        zpos = int(event.position[0])
1887                        if self.label_tr is None:
1888                            ## first click: get the track label
1889                            self.label_tr = ut.getCellValue(self.epicure.seglayer, event) 
1890                        else:
1891                            old_label = ut.setCellValue(self.epicure.seglayer, self.epicure.seglayer, event, self.label_tr, layer_frame=zpos, label_frame=zpos)
1892                            self.epicure.tracking.remove_one_frame( old_label, zpos, handle_gaps=self.epicure.forbid_gaps )
1893                            self.epicure.add_label( [self.label_tr], zpos )
1894                        ## advance to next frame, ready for a click
1895                        self.viewer.dims.set_point(0, zpos+1)
1896                        ## if reach the end, stops here for this track
1897                        if (zpos+1) >= self.epicure.seglayer.data.shape[0]:
1898                            self.end_track_edit()
1899                        return
1900                    
1901                    ## Finish manual tracking
1902                    if ut.shortcut_click_match( strack["end manual"], event ):
1903                        self.end_track_edit()
1904                        return
1905                   
1906                    ## Interpolate between two labels: get first label
1907                    if ut.shortcut_click_match( strack["interpolate first"], event ):
1908                        ## left click, first cell
1909                        self.interp_labela = ut.getCellValue(self.epicure.seglayer, event) 
1910                        self.interp_framea = int(event.position[0])
1911                        return
1912                    
1913                    ## Interpolate between two labels: get second label and interpolate
1914                    if ut.shortcut_click_match( strack["interpolate second"], event ):
1915                        ## right click, second cell
1916                        labelb = ut.getCellValue(self.epicure.seglayer, event) 
1917                        interp_frameb = int(event.position[0])
1918                        if self.interp_labela is not None:
1919                            if abs(self.interp_framea - interp_frameb) <= 1:
1920                                print("No frames to interpolate, exit")
1921                                self.end_track_edit()
1922                                return
1923                            if self.interp_framea < interp_frameb:
1924                                self.interpolate_labels(self.interp_labela, self.interp_framea, labelb, interp_frameb)
1925                            else:
1926                                self.interpolate_labels(labelb, interp_frameb, self.interp_labela, self.interp_framea )
1927                            self.end_track_edit()
1928                            return
1929                        else:
1930                            print("No cell selected with left click before. Exit mode")
1931                            self.end_track_edit()
1932                            return
1933                        
1934                    ## Delete all the labels of the track until its end
1935                    if ut.shortcut_click_match( strack["delete"], event ):
1936                        tframe = int(event.position[0])
1937                        label = ut.getCellValue(self.epicure.seglayer, event)
1938                        if label > 0:
1939                            self.epicure.replace_label( label, 0, tframe )
1940                            if self.epicure.verbose > 0:
1941                                print("Track "+str(label)+" deleted from frame "+str(tframe))
1942                        self.end_track_edit()
1943                        return
1944
1945                ## A right click or other click stops it
1946                self.end_track_edit()
1947
1948            #@self.epicure.seglayer.mouse_double_click_callbacks.append
1949            #def double_click(layer, event):
1950            #    """ Edit tracking : double click options """
1951            #    if event.type == "mouse_double_click":      
1952                    
1953        
1954            @self.epicure.seglayer.bind_key( strack["mode"]["key"], overwrite=True )
1955            def end_edit_track(layer):
1956                self.end_track_edit()

active key bindings for tracking options

def end_track_edit(self):
1958    def end_track_edit(self):
1959        self.start_label = None
1960        self.interp_labela = None
1961        self.interp_labelb = None
1962        ut.reactive_bindings( self.epicure.seglayer, self.old_mouse_drag, self.old_key_map )
1963        ut.show_info("End track edit mode")
def merge_tracks(self, labela, posa, labelb, posb):
1965    def merge_tracks(self, labela, posa, labelb, posb):
1966        """ 
1967            Merge track with label a with track of label b, temporally or spatially 
1968        """
1969        if labela == labelb:
1970            if self.epicure.verbose > 0:
1971                print("Already the same track" )
1972                return
1973        if int(posb[0]) == int(posa[0]):
1974            self.tracks_spatial_merging( labela, posa, labelb )
1975        else:
1976            self.tracks_temporal_merging( labela, posa, labelb, posb )

Merge track with label a with track of label b, temporally or spatially

def tracks_spatial_merging(self, labela, posa, labelb):
1978    def tracks_spatial_merging( self, labela, posa, labelb ):
1979        """ Merge spatially two tracks: labels have to be touching all along the common frames """
1980        start_time = ut.start_time()
1981        ## get last common frame
1982        lasta = self.epicure.tracking.get_last_frame( labela )
1983        lastb = self.epicure.tracking.get_last_frame( labelb )
1984        lastcommon = min(lasta, lastb)
1985
1986        ## if longer than the last common, split the label(s) that continue
1987        if lasta > lastcommon:
1988            if self.epicure.tracking.get_first_frame( labela ) < int(posa[0]):
1989                self.epicure.split_track( labela, lastcommon+1 )
1990        if lastb > lastcommon:
1991            if self.epicure.tracking.get_first_frame( labelb ) < int(posa[0]):
1992                self.epicure.split_track( labelb, lastcommon+1 )
1993
1994        ## Looks, ok, create a new track and merge the two tracks in it
1995        new_label = self.epicure.get_free_label()
1996        new_labels = []
1997        ind_tomodif = None
1998        footprint = disk(radius=3)
1999        for frame in range( int(posa[0]), lastcommon+1 ):
2000            bbox, merged = ut.getBBox2DMerge( self.epicure.seg[frame], labela, labelb )
2001            bbox = ut.extendBBox2D( bbox, 1.05, self.epicure.imgshape2D )
2002            
2003            ## check if labels are touching at each frame
2004            segt_crop = ut.cropBBox2D( self.epicure.seg[frame], bbox )
2005            touched = ut.checkTouchingLabels( segt_crop, labela, labelb )
2006            if not touched:
2007                print("Labels "+str(labela)+" and "+str(labelb)+" are not always touching. Refusing to merge them")
2008                return 
2009            
2010            ## merge the two labels together
2011            joinlab = ut.cropBBox2D( merged, bbox )
2012            joinlab = new_label * binary_closing(joinlab, footprint)
2013           
2014            ## get the index and new values to change
2015            indmodif = ut.ind_boundaries( joinlab )
2016            #indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2017            new_labels = new_labels + [0]*len(indmodif)
2018            curmodif = np.transpose( np.nonzero( joinlab == new_label ) )
2019            new_labels = new_labels + [new_label]*len(curmodif)
2020            indmodif = np.vstack((indmodif, curmodif))
2021            indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2022            if ind_tomodif is None:
2023                ind_tomodif = indmodif
2024            else:
2025                ind_tomodif = np.vstack((ind_tomodif, indmodif))
2026            #ind_tomodif = np.vstack((ind_tomodif, curmodif))
2027        
2028        ## update the labels and the tracks
2029        self.epicure.change_labels_frommerge( ind_tomodif, new_labels, remove_labels=[labela, labelb] )
2030        if self.epicure.verbose > 0:
2031            ut.show_info("Merged spatially "+str(labela)+" with "+str(labelb)+" from frame "+str(int(posa[0]))+" to frame "+str(lastcommon)+"\n New track label is "+str(new_label))
2032        if self.epicure.verbose > 1:
2033            ut.show_duration(start_time, "Merging spatially tracks in ")

Merge spatially two tracks: labels have to be touching all along the common frames

def tracks_temporal_merging(self, labela, posa, labelb, posb):
2036    def tracks_temporal_merging( self, labela, posa, labelb, posb ):
2037        """ 
2038        Merge track with label a with track of label b if consecutives frames. 
2039        It does not check if label are close in distance, assume it is.
2040        """
2041
2042        if self.epicure.forbid_gaps:
2043            if abs(int(posb[0]) - int(posa[0])) != 1:
2044                if self.epicure.verbose > 0:
2045                    print("Frames to merge are not consecutives, refused")
2046                return
2047
2048        ## If frame b is before frame a, swap so that a is first 
2049        if posa[0] > posb[0]:
2050            posc = np.copy(posa)
2051            posa = posb
2052            posb = posc
2053            labelc = labela
2054            labela = labelb
2055            labelb = labelc
2056
2057        ## Check that posa is last frame of label a and pos b first frame of label b
2058        if int(posa[0]) != self.epicure.tracking.get_last_frame( labela ):
2059            if self.epicure.verbose > 0:
2060                print("Clicked label "+str(labela)+" at frame "+str(posa[0])+" was not the last frame of the track -> splitting it")
2061            self.epicure.split_track( labela, int(posa[0])+1 )
2062
2063        if posb[0] != self.epicure.tracking.get_first_frame( labelb ):
2064            if self.epicure.verbose > 0:
2065                print("Clicked label "+str(labelb)+" at frame "+str(posb[0])+" is not the first frame of the track -> splitting it")
2066            labelb = self.epicure.split_track( labelb, int(posb[0]) )
2067
2068        self.epicure.replace_label( labelb, labela, int(posb[0]) )

Merge track with label a with track of label b if consecutives frames. It does not check if label are close in distance, assume it is.

def get_parents(self, twoframes, labels):
2071    def get_parents(self, twoframes, labels):
2072        """ Get parent of all labels """
2073        return self.epicure.tracking.find_parents( labels, twoframes )

Get parent of all labels

def get_position_label_2D(self, img, labels, parent_labels):
2075    def get_position_label_2D(self, img, labels, parent_labels):
2076        """ Get position of each label to update with parent label """
2077        indmodif = None
2078        new_labels = []
2079        ## get possible free labels, to be sure that it will not take the same ones
2080        free_labels = self.epicure.get_free_labels(len(labels))
2081        for i, lab in enumerate(labels):
2082            parent_label = parent_labels[i]
2083            if parent_label is None:
2084                parent_label = free_labels[i]
2085                parent_labels[i] = parent_label
2086            curmodif = np.argwhere( img==lab )
2087            if indmodif is None:
2088                indmodif = curmodif
2089            else:
2090                indmodif = np.vstack((indmodif, curmodif))
2091            new_labels = new_labels + ([parent_label]*curmodif.shape[0])
2092        return indmodif, new_labels, parent_labels

Get position of each label to update with parent label

def keep_orphans(self, img, frame, keep_labels=[]):
2094    def keep_orphans( self, img, frame, keep_labels=[]):
2095        """ Keep only labels that doesn't have a follower (track is finishing at that frame) """
2096        ## remove the labels to track
2097        labs = np.unique(img[0]).tolist() #np.setdiff1d( img[0], labels ).tolist()
2098        if 0 in labs:
2099            labs.remove(0)
2100        ## Check that it's not present at current frame
2101        torem = [ lab for lab in labs if (lab not in keep_labels) and (self.epicure.tracking.is_in_frame( lab, frame ) ) ]
2102        if len(torem) == 0:
2103            return img
2104        mask = np.isin(img[0], torem)
2105        img[0][mask] = 0
2106        return img

Keep only labels that doesn't have a follower (track is finishing at that frame)

def inherit_parent_labels(self, myframe, labels, bbox, frame, keep_labels):
2108    def inherit_parent_labels(self, myframe, labels, bbox, frame, keep_labels):
2109        """ Get parent labels if any and indices to modify with it """
2110        if ( self.epicure.tracked == 0 ) or (frame<=0):
2111            parent_labels = [None]*len(labels)
2112            indmodif, new_labels, parent_labels = self.get_position_label_2D(myframe, labels, parent_labels)
2113        else:
2114            twoframes = ut.crop_twoframes( self.epicure.seglayer.data, bbox, frame )
2115            twoframes[1] = np.copy(myframe) # merge of the labels and 0 outside
2116            twoframes = self.keep_orphans( twoframes, frame, keep_labels=keep_labels)
2117            
2118            parent_labels = self.get_parents( twoframes, labels )
2119        
2120            indmodif, new_labels, parent_labels = self.get_position_label_2D(twoframes[1], labels, parent_labels)
2121
2122        if self.epicure.verbose > 0:
2123            print("Set value (from parent or new): "+str(np.unique(new_labels)))
2124        ## back to movie position
2125        indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2126        return indmodif, new_labels, parent_labels

Get parent labels if any and indices to modify with it

def inherit_child_labels(self, myframe, labels, bbox, frame, parent_labels, keep_labels):
2128    def inherit_child_labels(self, myframe, labels, bbox, frame, parent_labels, keep_labels):
2129        """ Get child labels if any and indices to modify with it """
2130        if (self.epicure.tracked == 0 ) or (frame>=self.epicure.nframes-1):
2131            return [], []
2132        else:
2133            twoframes = np.copy( ut.cropBBox2D(self.epicure.seglayer.data[frame+1], bbox) )
2134            ## check if the new value to set is present in the following frame, in that case don't do any propagation
2135            for par in parent_labels:
2136                if np.any( twoframes==par ):
2137                    if self.epicure.verbose > 1:
2138                        print("Propagating: not because new value present in labels: "+str(par))
2139                    return [], []
2140
2141            twoframes = np.stack( (twoframes, np.copy(myframe)) )
2142            twoframes = self.keep_orphans(twoframes, frame, keep_labels=keep_labels)
2143            child_labels = self.get_parents( twoframes, labels )
2144            
2145            if self.epicure.verbose > 0:
2146                print("Propagate  the new value to: "+str(child_labels))
2147            if child_labels is None:
2148                return [], []
2149        
2150        # get position of each child label to update with current label
2151        indmodif = []
2152        new_labels = []
2153        for i, lab in enumerate(child_labels):
2154            if lab is not None:
2155                if lab == parent_labels[i]:
2156                    ## going to propagate to itself, no need
2157                    continue
2158                after_frame = frame+1
2159                last_frame = self.epicure.tracking.get_last_frame( parent_labels[i] )
2160                if (last_frame is not None) and (last_frame >= after_frame):
2161                    ## the label to propagate is present somewhere after the current frame
2162                    self.epicure.split_track( parent_labels[i], after_frame )
2163                inds = self.epicure.get_label_indexes( lab, after_frame )
2164                if len(indmodif) == 0:
2165                    indmodif = inds
2166                else:
2167                    indmodif = np.vstack((indmodif, inds))
2168                new_labels = new_labels + np.repeat(parent_labels[i], len(inds)).tolist()
2169        return indmodif, new_labels

Get child labels if any and indices to modify with it

def propagate_label_change(self, myframe, labels, bbox, frame, keep_labels):
2171    def propagate_label_change(self, myframe, labels, bbox, frame, keep_labels):
2172        """ Propagate the new labelling to match parent/child labels """
2173        start_time = ut.start_time()
2174        indmodif = ut.ind_boundaries( myframe )
2175        indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2176        #ut.show_info("Boundaries in "+"{:.3f}".format((time.time()-start_time)/60)+" min")
2177        new_labels = np.repeat(0, len(indmodif)).tolist()
2178
2179        ## get parent labels if any for each label
2180        indmodif2, new_labels2, parent_labels = self.inherit_parent_labels(myframe, labels, bbox, frame, keep_labels)
2181        if indmodif2 is not None:
2182            indmodif = np.vstack((indmodif, indmodif2))
2183            new_labels = new_labels+new_labels2
2184        if self.epicure.verbose > 1:
2185            ut.show_duration(start_time, "Propagation, parents found, ")
2186
2187        ## propagate the change: get child labels if any for each label
2188        indmodif_child, new_labels_child = self.inherit_child_labels(myframe, labels, bbox, frame, parent_labels, keep_labels)
2189        if len(indmodif_child) > 0:
2190            indmodif = np.vstack((indmodif, indmodif_child))
2191            new_labels = new_labels + new_labels_child
2192        if self.epicure.verbose > 1:
2193            ut.show_duration(start_time, "Propagation, childs found, ")
2194        
2195        ## go, do the update
2196        self.epicure.change_labels(indmodif, new_labels)

Propagate the new labelling to match parent/child labels

def interpolate_labels(self, labela, framea, labelb, frameb):
2199    def interpolate_labels( self, labela, framea, labelb, frameb ):
2200        """ 
2201            Interpolate the label shape in between two labels 
2202            Based on signed distance transform, like Fiji ROIs interpolation
2203        """
2204        if self.epicure.verbose > 1:
2205            print("Interpolating between "+str(labela)+" and "+str(labelb))
2206            print("From frame "+str(framea)+" to frame "+str(frameb))
2207            start_time = ut.start_time()
2208        
2209        sega = self.epicure.seglayer.data[framea]
2210        maska = np.isin( sega, [labela] )
2211        segb = self.epicure.seglayer.data[frameb]
2212        maskb = np.isin( segb, [labelb] )
2213
2214        ## get merged bounding box, and crop around it
2215        mask = maska | maskb
2216        props = ut.labels_properties(mask*1)
2217        bbox = ut.extendBBox2D( props[0].bbox, extend_factor=1.2, imshape=mask.shape )
2218
2219        maska = ut.cropBBox2D( maska, bbox )
2220        maskb = ut.cropBBox2D( maskb, bbox )
2221
2222        ## get signed distance transform of each label
2223        dista = edt.sdf( maska )
2224        distb = edt.sdf( maskb )
2225
2226        inds = None
2227        new_labels = []
2228        for frame in range(framea+1, frameb):
2229            p = (frame-framea)/(frameb-framea)
2230            dist = (1-p) * dista + p * distb
2231            ## change only pixels that are 0
2232            frame_crop = ut.cropBBox2D( self.epicure.seglayer.data[frame], bbox )
2233            tochange = binary_dilation(dist>0, footprint=disk(radius=2)) * (frame_crop<=0)   # expand to touch neighbor label
2234            
2235            ## indexes and new values to change
2236            indmodif = np.argwhere( tochange > 0 ).tolist()
2237            indmodif = ut.toFullMoviePos( indmodif, bbox, frame )
2238            if inds is None:
2239                inds = indmodif
2240            else:
2241                inds = np.vstack( (inds, indmodif) )
2242            new_labels = new_labels + [labela]*len(indmodif)
2243
2244            ## be sure to remove the boundaries with neighbor labels
2245            bound_ind = ut.ind_boundaries( tochange )
2246            new_labels = new_labels + [0]*len(bound_ind)
2247            bound_ind = ut.toFullMoviePos( bound_ind, bbox, frame )
2248            inds = np.vstack( (inds, bound_ind) )
2249
2250        ## Go, apply the changes
2251        self.epicure.change_labels( inds, new_labels )
2252        ## change the second track to first track value
2253        self.epicure.replace_label( labelb, labela, frameb )
2254        if self.epicure.verbose > 1:
2255            ut.show_duration( start_time, "Interpolation took " )
2256        if self.epicure.verbose > 0:
2257            ut.show_info( "Interpolated label "+str(labela)+" from frame "+str(framea+1)+" to "+str(frameb-1) )

Interpolate the label shape in between two labels Based on signed distance transform, like Fiji ROIs interpolation

class ClassifyIntensity(PyQt6.QtWidgets.QWidget):
2263class ClassifyIntensity( QWidget ):
2264    """ Interface to group cells based on their mean intensity """
2265    def __init__( self, edit ):
2266        super().__init__()
2267        self.edit = edit
2268        poplayout = wid.vlayout()
2269
2270        ## Show in which group cells will be added
2271        self.group_name = wid.label_line( "Positive cells will be added to group: "+str(self.edit.group_choice.currentText() ) )
2272        poplayout.addWidget( self.group_name )
2273
2274        ## Choose the intensity layer
2275        line, self.layer_choice = wid.list_line( label="Measure intensity from: ", descr="Choose the layer to use for intensity classification" )
2276        for lay in self.edit.viewer.layers:
2277            if lay.name in [ "Events", "Tracks", "ROIs" ]:
2278                continue
2279            self.layer_choice.addItem( lay.name )
2280            print(lay.name)
2281        poplayout.addLayout( line )
2282
2283        ## Choose the method to use for intensity measurement
2284        method_line, self.method_choice = wid.list_line( label="Method to measure intensity along track: ", descr="Choose the method to measure intensity" )
2285        meths = ["mean", "median", "max", "min", "sum"]
2286        for meth in meths:
2287            self.method_choice.addItem( meth)        
2288        poplayout.addLayout( method_line )
2289
2290        ## Choose frames to use for classification 
2291        frame_lab = wid.label_line( "Measure intensity on frame(s):" )
2292        min_frame_line, self.min_frame = wid.ranged_value_line( label="From frame: ", descr="First frame to use for classification", minval=0, maxval=self.edit.epicure.nframes-1, step=1, val=0 )
2293        poplayout.addLayout( min_frame_line )
2294        max_frame_line, self.max_frame = wid.ranged_value_line( label="To frame: ", descr="Last frame to use for classification", minval=0, maxval=self.edit.epicure.nframes-1, step=1, val=self.edit.epicure.nframes-1 )
2295        poplayout.addLayout( max_frame_line )
2296
2297        ## Choose the threshold for classification
2298        thres_line, self.threshold = wid.value_line( label="Track intensity threshold: ", default_value="100", descr="Threshold of measured intensity of a track to be considered as positive" )
2299        poplayout.addLayout( thres_line )
2300
2301        go_btn = wid.add_button( "Add positive cells to group", self.classify, "Start the classification of positive cells" )
2302        poplayout.addWidget( go_btn )
2303
2304        self.setLayout( poplayout )
2305
2306    def update( self ):
2307        """ Update the parameters with current GUI state """
2308        self.group_name.setText( "Positive cells will be added to group: "+str(self.edit.group_choice.currentText() ) )
2309        self.layer_choice.clear()
2310        for lay in self.edit.viewer.layers:
2311            if lay.name in [ "Events", "Tracks", "ROIs" ]:
2312                continue
2313            self.layer_choice.addItem( lay.name )
2314
2315    def classify( self ):
2316        self.edit.group_positive_cells( self.layer_choice.currentText(), self.method_choice.currentText(), self.min_frame.value(), self.max_frame.value(), float(self.threshold.text()) )

Interface to group cells based on their mean intensity

ClassifyIntensity(edit)
2265    def __init__( self, edit ):
2266        super().__init__()
2267        self.edit = edit
2268        poplayout = wid.vlayout()
2269
2270        ## Show in which group cells will be added
2271        self.group_name = wid.label_line( "Positive cells will be added to group: "+str(self.edit.group_choice.currentText() ) )
2272        poplayout.addWidget( self.group_name )
2273
2274        ## Choose the intensity layer
2275        line, self.layer_choice = wid.list_line( label="Measure intensity from: ", descr="Choose the layer to use for intensity classification" )
2276        for lay in self.edit.viewer.layers:
2277            if lay.name in [ "Events", "Tracks", "ROIs" ]:
2278                continue
2279            self.layer_choice.addItem( lay.name )
2280            print(lay.name)
2281        poplayout.addLayout( line )
2282
2283        ## Choose the method to use for intensity measurement
2284        method_line, self.method_choice = wid.list_line( label="Method to measure intensity along track: ", descr="Choose the method to measure intensity" )
2285        meths = ["mean", "median", "max", "min", "sum"]
2286        for meth in meths:
2287            self.method_choice.addItem( meth)        
2288        poplayout.addLayout( method_line )
2289
2290        ## Choose frames to use for classification 
2291        frame_lab = wid.label_line( "Measure intensity on frame(s):" )
2292        min_frame_line, self.min_frame = wid.ranged_value_line( label="From frame: ", descr="First frame to use for classification", minval=0, maxval=self.edit.epicure.nframes-1, step=1, val=0 )
2293        poplayout.addLayout( min_frame_line )
2294        max_frame_line, self.max_frame = wid.ranged_value_line( label="To frame: ", descr="Last frame to use for classification", minval=0, maxval=self.edit.epicure.nframes-1, step=1, val=self.edit.epicure.nframes-1 )
2295        poplayout.addLayout( max_frame_line )
2296
2297        ## Choose the threshold for classification
2298        thres_line, self.threshold = wid.value_line( label="Track intensity threshold: ", default_value="100", descr="Threshold of measured intensity of a track to be considered as positive" )
2299        poplayout.addLayout( thres_line )
2300
2301        go_btn = wid.add_button( "Add positive cells to group", self.classify, "Start the classification of positive cells" )
2302        poplayout.addWidget( go_btn )
2303
2304        self.setLayout( poplayout )
edit
group_name
def update(self):
2306    def update( self ):
2307        """ Update the parameters with current GUI state """
2308        self.group_name.setText( "Positive cells will be added to group: "+str(self.edit.group_choice.currentText() ) )
2309        self.layer_choice.clear()
2310        for lay in self.edit.viewer.layers:
2311            if lay.name in [ "Events", "Tracks", "ROIs" ]:
2312                continue
2313            self.layer_choice.addItem( lay.name )

Update the parameters with current GUI state

def classify(self):
2315    def classify( self ):
2316        self.edit.group_positive_cells( self.layer_choice.currentText(), self.method_choice.currentText(), self.min_frame.value(), self.max_frame.value(), float(self.threshold.text()) )
class ClassifyEvent(PyQt6.QtWidgets.QWidget):
2318class ClassifyEvent( QWidget ):
2319    """ Interface to group cells based on their interaction with an event (dividing or extruding cells) """
2320
2321    def __init__( self, edit ):
2322        super().__init__()
2323        self.edit = edit
2324        poplayout = wid.vlayout()
2325
2326        ## Choose the event to use
2327        line, self.event_choice = wid.list_line( label="Select cells that ends with: ", descr="Choose the event to use to select the cells" )
2328        for evt in self.edit.epicure.event_class:
2329            self.event_choice.addItem( evt )
2330        poplayout.addLayout( line )
2331
2332        go_btn = wid.add_button( "Add selected cells to new group", self.classify, "Start the classification of cells" )
2333        poplayout.addWidget( go_btn )
2334
2335        self.setLayout( poplayout )
2336
2337    def classify( self ):
2338        """ Add all the cell that finish with the selected event to the group """
2339        self.edit.group_event_cells( self.event_choice.currentText() )

Interface to group cells based on their interaction with an event (dividing or extruding cells)

ClassifyEvent(edit)
2321    def __init__( self, edit ):
2322        super().__init__()
2323        self.edit = edit
2324        poplayout = wid.vlayout()
2325
2326        ## Choose the event to use
2327        line, self.event_choice = wid.list_line( label="Select cells that ends with: ", descr="Choose the event to use to select the cells" )
2328        for evt in self.edit.epicure.event_class:
2329            self.event_choice.addItem( evt )
2330        poplayout.addLayout( line )
2331
2332        go_btn = wid.add_button( "Add selected cells to new group", self.classify, "Start the classification of cells" )
2333        poplayout.addWidget( go_btn )
2334
2335        self.setLayout( poplayout )
edit
def classify(self):
2337    def classify( self ):
2338        """ Add all the cell that finish with the selected event to the group """
2339        self.edit.group_event_cells( self.event_choice.currentText() )

Add all the cell that finish with the selected event to the group