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() )
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
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
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
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
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
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
setParent(self, parent: QWidget|None) setParent(self, parent: QWidget|None, f: Qt.WindowType)
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
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
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
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
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
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
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
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
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)
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 )
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()
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
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
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
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
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)
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
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
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
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
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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")
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
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
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
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
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
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
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
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
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
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
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
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
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
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.
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
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
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)
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
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
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
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
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
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 )
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
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)
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 )