epicure.epicuring
EpiCure main class.
Open and initialize the files. Launch the main widget composed of the segmentation and tracking editing features. All other classes are linked to this one.
1""" 2 **EpiCure main class.** 3 4 Open and initialize the files. 5 Launch the main widget composed of the segmentation and tracking editing features. 6 All other classes are linked to this one. 7""" 8import numpy as np 9import os, time, pickle 10import napari 11import math 12from qtpy.QtWidgets import QVBoxLayout, QTabWidget, QWidget 13from napari.utils import progress 14from skimage.morphology import skeletonize 15from skimage.measure import regionprops 16from joblib import Parallel, delayed 17from pathlib import Path 18 19import epicure.Utils as ut 20from epicure.editing import Editing 21from epicure.tracking import Tracking 22from epicure.inspecting import Inspecting 23from epicure.outputing import Outputing 24from epicure.displaying import Displaying 25from epicure.preferences import Preferences 26import epicure.tm_loader as tm 27 28 29class EpiCure: 30 def __init__(self, viewer=None): 31 """ 32 Initialize the EpiCure viewer instance. 33 34 :param: viewer (napari.Viewer, optional): An existing napari Viewer instance to use. 35 If None, a new Viewer instance will be created with show=False. 36 Defaults to None. 37 """ 38 self.viewer = viewer 39 """ Napari viewer that is used for this session """ 40 if self.viewer is None: 41 self.viewer = napari.Viewer(show=False) 42 self.viewer.title = "Napari - EpiCure" 43 self.reset() 44 45 def reset(self): 46 """ Reset all the parameters to the default values """ 47 self.init_epicure_metadata() ## initialize metadata variables (scalings, channels) 48 self.img = None 49 """ data of the raw movie """ 50 self.inspecting = None 51 """ interface for inspection options """ 52 self.others = None 53 self.imgshape2D = None ## width, height of the image 54 self.nframes = None ## Number of time frames 55 self.thickness = 4 ## thickness of junctions, wider 56 self.minsize = 4 ## smallest number of pixels in a cell 57 self.verbose = 1 ## level of printing messages (None/few, normal, debug mode) 58 self.event_class = ["division", "extrusion", "suspect"] ## list of possible events 59 self.main_channel = 0 ## position of the main channel (raw movie) 60 61 self.overtext = dict() 62 self.help_index = 1 ## current display index of help overlay 63 self.blabla = None ## help window 64 self.groups = {} 65 self.tracked = 0 ## has done a tracking 66 self.process_parallel = False ## Do some operations in parallel (n frames in parallel) 67 self.nparallel = 4 ## number of parallel threads 68 self.dtype = np.uint32 ## label type, default 32 but if less labels, reduce it 69 self.outputing = None ## non initialized yet 70 71 self.forbid_gaps = False ## allow gaps in track or not 72 73 self.pref = Preferences() 74 self.shortcuts = self.pref.get_shortcuts() ## user specific shortcuts 75 self.settings = self.pref.get_settings() ## user specific preferences 76 ## display settings 77 self.display_colors = None ## settings for changing some display colors 78 if "Display" in self.settings: 79 if "Colors" in self.settings["Display"]: 80 self.display_colors = self.settings["Display"]["Colors"] 81 82 83 def init_epicure_metadata(self): 84 """ Fills metadata with default values """ 85 ## scalings and unit names 86 self.epi_metadata = {} 87 self.epi_metadata["ScaleXY"] = 1 88 self.epi_metadata["UnitXY"] = "um" 89 self.epi_metadata["ScaleT"] = 1 90 self.epi_metadata["UnitT"] = "min" 91 self.epi_metadata["MainChannel"] = 0 92 self.epi_metadata["Allow gaps"] = True 93 self.epi_metadata["Verbose"] = 1 94 self.epi_metadata["Scale bar"] = True 95 self.epi_metadata["MovieFile"] = "" 96 self.epi_metadata["SegmentationFile"] = "" 97 self.epi_metadata["EpithelialCells"] = True ## epithelial (packed) cells 98 self.epi_metadata["Reloading"] = False ## Never been epiCured yet 99 100 def get_resetbtn_color(self): 101 """Returns the color of Reset buttons if defined""" 102 if "Display" in self.settings: 103 if "Colors" in self.settings["Display"]: 104 if "Reset button" in self.settings["Display"]["Colors"]: 105 return self.settings["Display"]["Colors"]["Reset button"] 106 return None 107 108 def set_thickness(self, thick): 109 """ 110 Thickness of junctions (half thickness) 111 112 :param: thick set thickness value to input value 113 """ 114 self.thickness = thick 115 116 def movie_from_layer(self, layer, imgpath): 117 """ 118 Prepare the intensity movie from opened layer, and get metadata. 119 120 Resets the internal state, loads image data from the provided layer, 121 handles temporal and channel dimensions, and prepares the movie for processing. 122 123 It extracts metadata including file path and pixel scale, and attempts to handle various 124 image formats (2D, 3D, 4D with different dimension orders). 125 126 :param: layer: A napari layer object containing the image data and scale information. 127 The layer's data attribute should contain the image array. 128 :param: imgpath (str): Absolute or relative file path to the image file being loaded. 129 130 :return: 131 A tuple containing: 132 - caxis (int or None): The axis index corresponding to the channel dimension, 133 or None if no multiple channels are detected. 134 - cval (int): The number of channels found in the image, or 0 if no channels 135 are detected. 136 """ 137 self.reset() ## reload everything 138 self.epi_metadata["MovieFile"] = os.path.abspath(imgpath) 139 ## if the layer is scaled, should be the right scale 140 self.epi_metadata["ScaleXY"] = layer.scale[2] 141 self.img = layer.data 142 nchan = 0 143 if len(self.img.shape)>3: 144 ## Format TCYX in general 145 nchan = self.img.shape[1] 146 ## transform static image to movie (add temporal dimension) 147 if len(self.img.shape) == 2: 148 self.img = np.expand_dims(self.img, axis=0) 149 caxis = None 150 cval = 0 151 if nchan > 0 or len(self.img.shape) > 3: 152 if nchan > 0 and len(self.img.shape) > 3: 153 ## multiple chanels and multiple slices, order axis should be TCXY 154 caxis = 1 155 cval = nchan 156 else: 157 ## one image with multiple chanels 158 minshape = min(self.img.shape) 159 caxis = self.img.shape.index(minshape) 160 cval = minshape 161 self.mov = self.img 162 163 ## display the movie: rename the layer 164 ut.remove_layer(self.viewer, "Movie") 165 layer.name = "Movie" 166 167 self.imgshape = self.viewer.layers["Movie"].data.shape 168 self.imgshape2D = self.imgshape[1:3] 169 self.nframes = self.imgshape[0] 170 return caxis, cval 171 172 173 def load_movie(self, imgpath): 174 """ 175 Load the intensity movie, and get metadata 176 177 :param: imgpath: full path to where the movie file is 178 """ 179 self.reset() ## reload everything 180 self.epi_metadata["MovieFile"] = os.path.abspath(imgpath) 181 self.img, nchan, self.epi_metadata["ScaleXY"], self.epi_metadata["UnitXY"], self.epi_metadata["ScaleT"], self.epi_metadata["UnitT"] = ut.open_image( 182 self.epi_metadata["MovieFile"], get_metadata=True, verbose=self.verbose > 1 183 ) 184 ## transform static image to movie (add temporal dimension) 185 if len(self.img.shape) == 2: 186 self.img = np.expand_dims(self.img, axis=0) 187 caxis = None 188 cval = 0 189 if nchan > 0 or len(self.img.shape) > 3: 190 if nchan > 0 and len(self.img.shape) > 3: 191 ## multiple chanels and multiple slices, order axis should be TCXY 192 caxis = 1 193 cval = nchan 194 else: 195 ## one image with multiple chanels 196 minshape = min(self.img.shape) 197 caxis = self.img.shape.index(minshape) 198 cval = minshape 199 self.mov = self.img 200 201 ## display the movie 202 ut.remove_layer(self.viewer, "Movie") 203 mview = self.viewer.add_image(self.img, name="Movie", blending="additive", colormap="gray") 204 mview.contrast_limits = self.quantiles() 205 mview.gamma = 0.95 206 207 self.imgshape = self.viewer.layers["Movie"].data.shape 208 self.imgshape2D = self.imgshape[1:3] 209 self.nframes = self.imgshape[0] 210 return caxis, cval 211 212 213 def quantiles(self): 214 """ Returns the quantiles 1% and 99.999% of the raw image to set the display """ 215 return tuple(np.quantile(self.img, [0.01, 0.9999])) 216 217 def set_verbose(self, verbose): 218 """ 219 Set verbose level 220 221 :param: verbose: amount of message that will be displayed in the Terminal console, from 0 (none) to 4 (a lot, for debugging) 222 """ 223 self.verbose = verbose 224 self.epi_metadata["Verbose"] = verbose 225 226 def set_gaps_option(self, allow_gap): 227 """Set the mode for gap allowing/forbid in tracks 228 229 :param: allow_gap: boolean. Indicates if gap in tracks (missing cell in one or more frames) should be allowed or not. 230 """ 231 self.epi_metadata["Allow gaps"] = allow_gap 232 self.forbid_gaps = not allow_gap 233 234 def set_epithelia(self, epithelia): 235 """ 236 Set the mode for cell packing (touching or not especially) 237 238 :param: epithelia: boolean, True if cells are touching 239 """ 240 self.epi_metadata["EpithelialCells"] = epithelia 241 242 def set_scalebar(self, show_scalebar): 243 """ 244 Show or not the scale bar, and set its value 245 246 :param: show_scalebar: boolean, set the visibility of the scale bar 247 """ 248 self.epi_metadata["Scale bar"] = show_scalebar 249 if self.viewer is not None: 250 self.viewer.scale_bar.visible = show_scalebar 251 self.viewer.scale_bar.unit = self.epi_metadata["UnitXY"] 252 for lay in self.viewer.layers: 253 lay.scale = [1, self.epi_metadata["ScaleXY"], self.epi_metadata["ScaleXY"]] 254 self.viewer.reset_view() 255 256 def set_scales(self, scalexy, scalet, unitxy, unitt): 257 """ 258 Set the scaling units for outputs. Put the values in Epicure metadata object 259 260 :param: scalexy: size of one pixel in X,Y directions 261 :param: scalet: duration of one frame (acquisition frequency) 262 :param: unitxy: name of the unit in which the scale is given 263 :param: unitt: name of the temporal unit in which the scale is given 264 """ 265 self.epi_metadata["ScaleXY"] = scalexy 266 self.epi_metadata["ScaleT"] = scalet 267 self.epi_metadata["UnitXY"] = unitxy 268 self.epi_metadata["UnitT"] = unitt 269 if self.viewer is not None: 270 self.viewer 271 if self.verbose > 0: 272 ut.show_info("Movie scales set to " + str(self.epi_metadata["ScaleXY"]) + " " + self.epi_metadata["UnitXY"] + " and " + str(self.epi_metadata["ScaleT"]) + " " + self.epi_metadata["UnitT"]) 273 274 def set_chanel(self, chan, chanaxis): 275 """ 276 Update the movie to the correct chanel 277 278 :param: chan: channel in which the raw movie is 279 :param: chanaxis: in which axis is the color channels information (usually format is TCYX, so will be 1) 280 """ 281 self.img = np.rollaxis(np.copy(self.mov), chanaxis, 0)[chan] 282 if len(self.img.shape) == 2: 283 self.img = np.expand_dims(self.img, axis=0) 284 ## udpate the image shape informations 285 self.imgshape = self.img.shape 286 self.imgshape2D = self.imgshape[1:3] 287 self.nframes = self.imgshape[0] 288 self.main_channel = chan 289 if self.viewer is not None: 290 mview = self.viewer.layers["Movie"] 291 mview.data = self.img 292 mview.contrast_limits = self.quantiles() 293 mview.gamma = 0.95 294 mview.refresh() 295 296 def add_other_chanels(self, chan, chanaxis): 297 """ Open other channels if option selected """ 298 others_raw = np.delete(self.mov, chan, axis=chanaxis) 299 self.others = [] 300 self.others_chanlist = [] 301 if self.others is not None: 302 others_raw = np.rollaxis(others_raw, chanaxis, 0) 303 for ochan in range(others_raw.shape[0]): 304 purechan = ochan 305 if purechan >= chan: 306 purechan = purechan + 1 307 self.others_chanlist.append(purechan) 308 if len(others_raw[ochan].shape) == 2: 309 expanded = np.expand_dims(others_raw[ochan], axis=0) 310 self.others.append( expanded ) 311 else: 312 self.others.append( others_raw[ochan] ) 313 mview = self.viewer.add_image( self.others[ochan], name="MovieChannel_"+str(purechan), blending="additive", colormap="gray" ) 314 mview.contrast_limits=tuple(np.quantile(self.others[ochan],[0.01, 0.9999])) 315 mview.gamma=0.95 316 mview.visible = False 317 318 def import_trackmate(self, segpath, verbose=0): 319 """ Load segmentation and tracks from TrackMate XML file """ 320 if verbose > 1: 321 print("Importing segmentation and tracks from TrackMate XML file") 322 np.set_printoptions(suppress=True, floatmode="maxprec_equal") 323 324 img_data_tag = tm._get_ImageData_tag(segpath) 325 metadata = tm._get_metadata(img_data_tag) 326 seg_shape = (int(metadata["nframes"]), int(metadata["height"]), int(metadata["width"])) 327 segmentation = np.zeros(seg_shape, dtype=np.uint16)-1 328 positions, tracks = tm._parse_Model_tag(segpath, metadata, segmentation) 329 label_mapping = tm._build_label_mapping(positions, tracks) 330 positions = tm.relabel_positions(label_mapping, positions) 331 tracks = tm.relabel_tracks(label_mapping, tracks) 332 segmentation = tm.relabel_segmentation(label_mapping, segmentation) 333 return segmentation, tracks 334 335 336 def load_segmentation(self, seg_input): 337 """Load the segmentation file""" 338 start_time = ut.start_time() 339 self.graph = None ## no loaded graph 340 ## compatibility to string input, the path to the image or a dictionnary 341 if isinstance(seg_input, dict): 342 segpath = seg_input["File"] 343 else: 344 segpath = seg_input 345 self.epi_metadata["SegmentationFile"] = segpath 346 if isinstance(seg_input, dict) and "Layer" in seg_input: 347 ## take the segmentation data and close it 348 self.seg = seg_input["Layer"].data 349 ut.remove_layer(self.viewer, seg_input["Layer"]) 350 else: 351 if str(segpath).endswith(".xml"): 352 ## import a TrackMate file 353 self.seg, self.graph = self.import_trackmate(segpath, verbose=self.verbose>1) 354 else: 355 self.seg, _, _, _, _, _ = ut.open_image(segpath, get_metadata=False, verbose=self.verbose > 1) 356 self.seg = np.uint32(self.seg) 357 ## transform static image to movie (add temporal dimension) 358 if len(self.seg.shape) == 2: 359 self.seg = np.expand_dims(self.seg, axis=0) 360 ## ensure that the shapes are correctly set 361 self.imgshape = self.seg.shape 362 self.imgshape2D = self.seg.shape[1:3] 363 self.nframes = self.seg.shape[0] 364 ## if the segmentation is a junction file, transform it to a label image 365 if ut.is_binary(self.seg): 366 self.junctions_to_label() 367 self.tracked = 0 368 else: 369 self.has_been_tracked() 370 self.prepare_labels() 371 372 ## define a reference size of the movie to scale default parameters 373 self.reference_size = np.max(self.imgshape2D) 374 self.epi_metadata["Reloading"] = True ## has been formatted to EpiCure format 375 376 # display the segmentation file movie 377 if self.viewer is not None: 378 if "Movie" in self.viewer.layers: 379 scale = self.viewer.layers["Movie"].scale 380 else: 381 scale = (1,1,1) 382 self.seglayer = self.viewer.add_labels(self.seg, name="Segmentation", blending="additive", opacity=0.5, scale=scale) 383 self.viewer.dims.set_point(0, 0) 384 self.seglayer.brush_size = 4 ## default label pencil drawing size 385 if self.verbose > 0: 386 ut.show_duration(start_time, header="Segmentation loaded in ") 387 388 389 def load_tracks(self, progress_bar): 390 """From the segmentation, get all the metadata""" 391 tracked = "tracked" 392 self.tracking.init_tracks() 393 if self.tracked == 0: 394 tracked = "untracked" 395 else: 396 if self.graph is not None: 397 self.tracking.set_graph(self.graph) 398 if self.forbid_gaps: 399 progress_bar.set_description("check and fix track gaps") 400 self.handle_gaps(track_list=None, verbose=1) 401 ut.show_info("" + str(len(self.tracking.get_track_list())) + " " + tracked + " cells loaded") 402 403 def has_been_tracked(self): 404 """Look if has been tracked already (some labels are in several frames)""" 405 nb = 0 406 for frame in range(self.seg.shape[0]): 407 if frame > 0: 408 inter = np.intersect1d(np.unique(self.seg[frame - 1]), np.unique(self.seg[frame])) 409 if len(inter) > 1: 410 self.tracked = 1 411 return 412 self.tracked = 0 413 return 414 415 def suggest_segfile(self, outdir): 416 """Check if a segmentation file from EpiCure already exists""" 417 if (self.epi_metadata["SegmentationFile"] != "") and ut.found_segfile(self.epi_metadata["SegmentationFile"]): 418 return self.epi_metadata["SegmentationFile"] 419 imgname, imgdir, out = ut.extract_names(self.epi_metadata["MovieFile"], outdir, mkdir=False) 420 return ut.suggest_segfile(out, imgname) 421 422 def outname(self): 423 return os.path.join(self.outdir, self.imgname) 424 425 def set_names(self, outdir): 426 """Extract default names from imgpath""" 427 self.imgname, self.imgdir, self.outdir = ut.extract_names(self.epi_metadata["MovieFile"], outdir, mkdir=True) 428 429 def go_epicure(self, outdir="epics", segmentation_input=None): 430 """Initialize everything and start the main widget""" 431 self.set_names(outdir) 432 if segmentation_input is None: 433 segmentation_input = {} 434 segmentation_input["File"] = self.suggest_segfile(outdir) 435 self.viewer.window._status_bar._toggle_activity_dock(True) 436 progress_bar = progress(total=5) 437 progress_bar.set_description("Reading segmented image") 438 ## load the segmentation 439 self.load_segmentation(segmentation_input) 440 if isinstance(segmentation_input, dict): 441 self.epi_metadata["SegmentationFile"] = segmentation_input["File"] 442 else: 443 self.epi_metadata["SegmentationFile"] = segmentation_input 444 progress_bar.update(1) 445 ut.set_active_layer(self.viewer, "Segmentation") 446 447 ## setup the main interface and shortcuts 448 start_time = ut.start_time() 449 progress_bar.set_description("Active EpiCure shortcuts") 450 self.key_bindings() 451 progress_bar.update(2) 452 progress_bar.set_description("Prepare widget") 453 self.main_widget() 454 progress_bar.update(3) 455 progress_bar.set_description("Load tracks") 456 self.load_tracks(progress_bar) 457 progress_bar.update(4) 458 459 ## load graph if it exists 460 epiname = os.path.join(self.outdir, self.imgname + "_epidata.pkl") 461 if os.path.exists(epiname): 462 progress_bar.set_description("Load EpiCure informations") 463 self.load_epicure_data(epiname) 464 if self.verbose > 0: 465 ut.show_duration(start_time, header="Tracks and graph loaded in ") 466 progress_bar.update(5) 467 self.apply_settings() 468 progress_bar.close() 469 self.viewer.window._status_bar._toggle_activity_dock(False) 470 471 ###### Settings (preferences) save and load 472 def apply_settings(self): 473 """Apply all default or prefered settings""" 474 for sety, val in self.settings.items(): 475 if sety == "Display": 476 self.display.apply_settings(val) 477 if "Show help" in val: 478 index = int(val["Show help"]) 479 self.switchOverlayText(index) 480 if "Contour" in val: 481 contour = int(val["Contour"]) 482 self.seglayer.contour = contour 483 self.seglayer.refresh() 484 if "Colors" in val: 485 color = val["Colors"]["button"] 486 check_color = val["Colors"]["checkbox"] 487 line_edit_color = val["Colors"]["line edit"] 488 group_color = val["Colors"]["group"] 489 self.main_gui.setStyleSheet( 490 "QPushButton {background-color: " 491 + color 492 + "} QCheckBox::indicator {background-color: " 493 + check_color 494 + "} QLineEdit {background-color: " 495 + line_edit_color 496 + "} QGroupBox {color: grey; background-color: " 497 + group_color 498 + "} " 499 ) 500 self.display_colors = val["Colors"] 501 if sety == "events": 502 self.inspecting.apply_settings(val) 503 if sety == "Output": 504 self.outputing.apply_settings(val) 505 if sety == "Track": 506 self.tracking.apply_settings(val) 507 if sety == "Edit": 508 self.editing.apply_settings(val) 509 # case _: 510 # continue 511 ## match is not compatible with python 3.9 512 513 def update_settings(self): 514 """Returns all the prefered settings""" 515 disp = self.settings 516 ## load display current settings (layers visibility) 517 disp["Display"] = self.display.get_current_settings() 518 disp["Display"]["Show help"] = self.help_index 519 disp["Display"]["Contour"] = self.seglayer.contour 520 ## load suspect current settings 521 disp["events"] = self.inspecting.get_current_settings() 522 ## get outputs current settings 523 disp["Output"] = self.outputing.get_current_settings() 524 disp["Track"] = self.tracking.get_current_settings() 525 disp["Edit"] = self.editing.get_current_settings() 526 527 #### Main widget that contains the tabs of the sub widgets 528 529 def main_widget(self): 530 """Open the main widget interface""" 531 self.main_gui = QWidget() 532 533 layout = QVBoxLayout() 534 tabs = QTabWidget() 535 tabs.setObjectName("main") 536 layout.addWidget(tabs) 537 self.main_gui.setLayout(layout) 538 539 self.editing = Editing(self.viewer, self) 540 tabs.addTab(self.editing, "Edit") 541 self.inspecting = Inspecting(self.viewer, self) 542 tabs.addTab(self.inspecting, "Inspect") 543 self.tracking = Tracking(self.viewer, self) 544 tabs.addTab(self.tracking, "Track") 545 self.outputing = Outputing(self.viewer, self) 546 tabs.addTab(self.outputing, "Output") 547 self.display = Displaying(self.viewer, self) 548 tabs.addTab(self.display, "Display") 549 self.main_gui.setStyleSheet("QPushButton {background-color: rgb(40, 60, 75)} QCheckBox::indicator {background-color: rgb(40,52,65)}") 550 551 self.viewer.window.add_dock_widget(self.main_gui, name="Main") 552 553 def key_bindings(self): 554 """Activate shortcuts""" 555 self.text = "-------------- ShortCuts -------------- \n " 556 self.text += "!! Shortcuts work if Segmentation layer is active !! \n" 557 # for sctype, scvals in self.shortcuts.items(): 558 self.text += "\n---" + "General" + " options---\n" 559 sg = self.shortcuts["General"] 560 self.text += ut.print_shortcuts(sg) 561 self.text = self.text + "\n" 562 563 if self.verbose > 0: 564 print("Activating key shortcuts on segmentation layer") 565 print("Press <" + str(sg["show help"]["key"]) + "> to show/hide the main shortcuts") 566 print("Press <" + str(sg["show all"]["key"]) + "> to show ALL shortcuts") 567 ut.setOverlayText(self.viewer, self.text, size=12) 568 569 @self.seglayer.bind_key(sg["show help"]["key"], overwrite=True) 570 def switch_shortcuts(seglayer): 571 # index = (self.help_index+1)%(len(self.overtext.keys())+1) 572 # self.switchOverlayText(index) 573 index = (self.help_index + 1) % 2 574 self.switchOverlayText(index) 575 576 @self.seglayer.bind_key(sg["show all"]["key"], overwrite=True) 577 def list_all_shortcuts(seglayer): 578 self.switchOverlayText(0) ## hide display message in main window 579 text = "**************** EPICURE *********************** \n" 580 text += "\n" 581 text += self.text 582 text += "\n" 583 text += ut.napari_shortcuts() 584 for key, val in self.overtext.items(): 585 text += "\n" 586 text += val 587 self.update_text_window(text) 588 589 @self.seglayer.bind_key(sg["save segmentation"]["key"], overwrite=True) 590 def save_seglayer(seglayer): 591 self.save_epicures() 592 593 @self.viewer.bind_key(sg["save movie"]["key"], overwrite=True) 594 def save_movie(seglayer): 595 endname = "_frames.tif" 596 outname = os.path.join(self.outdir, self.imgname + endname) 597 self.save_movie(outname) 598 599 ########### Texts 600 601 def switchOverlayText(self, index): 602 """Switch overlay display text to index""" 603 self.help_index = index 604 if index == 0: 605 ut.showOverlayText(self.viewer, vis=False) 606 return 607 else: 608 ut.showOverlayText(self.viewer, vis=True) 609 # self.setCurrentOverlayText() 610 self.setGeneralOverlayText() 611 612 def init_text_window(self): 613 """Creates and opens a pop-up window with shortcut list""" 614 self.blabla = ut.create_text_window("EpiCure shortcuts") 615 616 def update_text_window(self, message): 617 """Update message in separate window""" 618 self.init_text_window() 619 self.blabla.value = message 620 621 def setGeneralOverlayText(self): 622 """set overlay help message to general message""" 623 text = self.text 624 ut.setOverlayText(self.viewer, text, size=12) 625 626 def setCurrentOverlayText(self): 627 """Set overlay help text message to current selected options list""" 628 text = self.text 629 dispkey = list(self.overtext.keys())[self.help_index - 1] 630 text += self.overtext[dispkey] 631 ut.setOverlayText(self.viewer, text, size=12) 632 633 def get_summary(self): 634 """Get a summary of the infos of the movie""" 635 summ = "----------- EpiCure summary ----------- \n" 636 summ += "--- Image infos \n" 637 summ += "Movie name: " + str(self.epi_metadata["MovieFile"]) + "\n" 638 summ += "Movie size (x,y): " + str(self.imgshape2D) + "\n" 639 if self.nframes is not None: 640 summ += "Nb frames: " + str(self.nframes) + "\n" 641 summ += "\n" 642 summ += "--- Segmentation infos \n" 643 summ += "Segmentation file: " + str(self.epi_metadata["SegmentationFile"]) + "\n" 644 summ += "Nb tracks: " + str(len(self.tracking.get_track_list())) + "\n" 645 tracked = "yes" 646 if self.tracked == 0: 647 tracked = "no" 648 summ += "Tracked: " + tracked + "\n" 649 nb_labels, mean_duration, mean_area = ut.summary_labels(self.seg) 650 summ += "Nb cells: " + str(nb_labels) + "\n" 651 summ += "Average track lengths: " + str(mean_duration) + " frames\n" 652 summ += "Average cell area: " + str(mean_area) + " pixels^2\n" 653 summ += "Nb suspect events: " + str(self.inspecting.nb_events(only_suspect=True)) + "\n" 654 summ += "Nb divisions: " + str(self.nb_divisions()) + "\n" 655 summ += "Nb extrusions: " + str(self.inspecting.nb_type("extrusion")) + "\n" 656 summ += "\n" 657 summ += "--- Parameter infos \n" 658 summ += "Junction thickness: " + str(self.thickness) + "\n" 659 return summ 660 661 def nb_divisions(self): 662 """ Return the number of divisions """ 663 return self.inspecting.nb_type("division") 664 665 def set_contour(self, width): 666 """ 667 Set the width of the contour of the cells to display the segmentation 668 669 :param: width: width of the contours of the segmentation (napari contour parameter). If 0 the cell will be filled by its label 670 """ 671 self.seglayer.contour = width 672 673 ############ Layers 674 675 def check_layers(self): 676 """Check that the necessary layers are present""" 677 if self.editing.shapelayer_name not in self.viewer.layers: 678 if self.verbose > 0: 679 print("Reput shape layer") 680 self.editing.create_shapelayer() 681 if self.inspecting.eventlayer_name not in self.viewer.layers: 682 if self.verbose > 0: 683 print("Reput event layer") 684 self.inspecting.create_eventlayer() 685 if "Movie" not in self.viewer.layers: 686 if self.verbose > 0: 687 print("Reput movie layer") 688 mview = self.viewer.add_image(self.img, name="Movie", blending="additive", colormap="gray", scale=[1, self.epi_metadata["ScaleXY"], self.epi_metadata["ScaleXY"]]) 689 # mview.reset_contrast_limits() 690 mview.contrast_limits = self.quantiles() 691 mview.gamma = 0.95 692 if "Segmentation" not in self.viewer.layers: 693 if self.verbose > 0: 694 print("Reput segmentation") 695 self.seglayer = self.viewer.add_labels(self.seg, name="Segmentation", blending="additive", opacity=0.5, scale=self.viewer.layers["Movie"].scale) 696 697 self.finish_update() 698 699 def finish_update(self, contour=None): 700 """ 701 After doing modifications on some layer(s), select back the main layer Segmentation as active (important for shortcut bindings) and refresh it 702 """ 703 if contour is not None: 704 self.seglayer.contour = contour 705 ut.set_active_layer(self.viewer, "Segmentation") 706 self.seglayer.refresh() 707 duplayers = ["PrevSegmentation"] 708 for dlay in duplayers: 709 if dlay in self.viewer.layers: 710 (self.viewer.layers[dlay]).refresh() 711 712 def read_epicure_metadata(self): 713 """Load saved infos from file""" 714 epiname = self.outname() + "_epidata.pkl" 715 if os.path.exists(epiname): 716 infile = open(epiname, "rb") 717 try: 718 epidata = pickle.load(infile) 719 if "EpiMetaData" in epidata.keys(): 720 for key, vals in epidata["EpiMetaData"].items(): 721 self.epi_metadata[key] = vals 722 infile.close() 723 except: 724 ut.show_warning("Could not read EpiCure metadata file " + epiname) 725 726 def save_epicures(self, imtype="float32"): 727 """ 728 Save all the current data: the segmentation, the metadata (metadata of the image, last parameters used), the events and some display settings. 729 """ 730 outname = os.path.join(self.outdir, self.imgname + "_labels.tif") 731 ut.writeTif(self.seg, outname, self.epi_metadata["ScaleXY"], imtype, what="Segmentation") 732 epiname = os.path.join(self.outdir, self.imgname + "_epidata.pkl") 733 outfile = open(epiname, "wb") 734 self.epi_metadata["MainChannel"] = self.main_channel 735 epidata = {} 736 epidata["EpiMetaData"] = self.epi_metadata 737 if self.groups is not None: 738 epidata["Group"] = self.groups 739 if self.tracking.graph is not None: 740 epidata["Graph"] = self.tracking.graph 741 if self.inspecting is not None and self.inspecting.events is not None: 742 epidata["Events"] = {} 743 if self.inspecting.events.data is not None: 744 epidata["Events"]["Points"] = self.inspecting.events.data 745 epidata["Events"]["Props"] = self.inspecting.events.properties 746 epidata["Events"]["Types"] = self.inspecting.event_types 747 # epidata["Events"]["Symbols"] = self.inspecting.events.symbol 748 # epidata["Events"]["Colors"] = self.inspecting.events.face_color 749 if "Movie" in self.viewer.layers: 750 ## to keep movie layer display settings for this file 751 epidata["Display"] = {} 752 epidata["Display"]["MovieContrast"] = self.viewer.layers["Movie"].contrast_limits 753 pickle.dump(epidata, outfile) 754 outfile.close() 755 756 def read_group_data(self, groups): 757 """Read the group EpiCure data from opened file""" 758 if self.verbose > 0: 759 print("Loaded cell groups info: " + str(list(groups.keys()))) 760 if self.verbose > 2: 761 print("Cell groups: " + str(groups)) 762 return groups 763 764 def read_graph_data(self, infile): 765 """ 766 Read the graph EpiCure data from opened pickle file 767 768 :param: infile: instance of pickle file being read. This will read the next part of the pickle file and load it in the track graph. 769 """ 770 try: 771 graph = pickle.load(infile) 772 if self.verbose > 0: 773 print("Graph (lineage) loaded") 774 return graph 775 except: 776 if self.verbose > 1: 777 print("No graph infos found") 778 return None 779 780 def read_events_data(self, infile): 781 """Read info of EpiCure events (suspects, divisions) from opened file""" 782 try: 783 events_pts = pickle.load(infile) 784 if events_pts is not None: 785 events_props = pickle.load(infile) 786 events_type = pickle.load(infile) 787 try: 788 symbols = pickle.load(infile) 789 colors = pickle.load(infile) 790 except: 791 if self.verbose > 1: 792 print("No events display info found") 793 symbols = None 794 colors = None 795 return events_pts, events_props, events_type 796 else: 797 return None, None, None 798 except: 799 if self.verbose > 1: 800 print("events info not complete") 801 return None, None, None 802 803 def load_epicure_data(self, epiname): 804 """Load saved infos from file""" 805 infile = open(epiname, "rb") 806 try: 807 if ut.is_windows(): 808 import pathlib 809 pathlib.PosixPath = pathlib.WindowsPath 810 #epidata = pickle.load( infile, encoding="utf8" ) 811 epidata = pickle.load( infile ) 812 #print(epidata) 813 if "EpiMetaData" in epidata.keys(): 814 # version of epicure file after Epicure 0.2.0 815 self.read_epidata(epidata) 816 infile.close() 817 else: 818 # version anterior of Epicure 0.2.0 819 self.load_epicure_data_old(epidata, infile) 820 except Exception as e: 821 if self.verbose > 1: 822 print(f" {type(e)} {e} - Could not read EpiCure data file {epiname}") 823 else: 824 ut.show_warning(f"Could not read EpiCure data file {epiname}") 825 print(f" {type(e)} {e} - Could not read EpiCure data file {epiname}") 826 827 def read_epidata(self, epidata): 828 """Read the dict of saved state and initialize all instances with it""" 829 for key, vals in epidata.items(): 830 if key == "EpiMetaData": 831 ## image data is read on the previous step 832 continue 833 if key == "Group": 834 ## Load groups information 835 self.groups = self.read_group_data(vals) 836 self.update_group_lists() 837 if key == "Graph": 838 ## Load graph (lineage) informations 839 self.tracking.graph = vals 840 if self.tracking.graph is not None: 841 self.tracking.tracklayer.refresh() 842 if self.verbose > 2: 843 print(f"Loaded track graph: {self.tracking.graph}") 844 if key == "Events": 845 ## Load events information 846 if "Points" in vals.keys(): 847 pts = vals["Points"] 848 if "Props" in vals.keys(): 849 props = vals["Props"] 850 if "Types" in vals.keys(): 851 event_types = vals["Types"] 852 # if "Symbols" in vals.keys(): 853 # symbols = vals["Symbols"] 854 # if "Colors" in vals.keys(): 855 # colors = vals["Colors"] 856 if pts is not None: 857 if len(pts) > 0: 858 self.inspecting.load_events(pts, props, event_types) 859 if len(pts) > 0 and self.verbose > 0: 860 print("events loaded") 861 ut.show_info("Loaded " + str(len(pts)) + " events") 862 if key == "Display": 863 if vals is not None: 864 ## load display setting 865 if "MovieContrast" in vals.keys(): 866 self.viewer.layers["Movie"].contrast_limits = vals["MovieContrast"] 867 868 def load_epicure_data_old(self, groups, infile): 869 """Load saved infos from file""" 870 ## Load groups information 871 self.groups = self.read_group_data(groups) 872 for group in self.groups.keys(): 873 self.editing.update_group_list(group) 874 self.outputing.update_selection_list() 875 ## Load graph (lineage) informations 876 self.tracking.graph = self.read_graph_data(infile) 877 if self.tracking.graph is not None: 878 self.tracking.tracklayer.refresh() 879 ## Load events information 880 pts, props, event_types = self.read_events_data(infile) 881 if pts is not None: 882 if len(pts) > 0: 883 self.inspecting.load_events(pts, props, event_types) 884 if len(pts) > 0 and self.verbose > 0: 885 print("events loaded") 886 ut.show_info("Loaded " + str(len(pts)) + " events") 887 infile.close() 888 889 def save_movie(self, outname): 890 """Save movie with current display parameters, except zoom""" 891 save_view = self.viewer.camera.copy() 892 save_frame = ut.current_frame(self.viewer) 893 ## place the view to see the whole image 894 self.viewer.reset_view() 895 # self.viewer.camera.zoom = 1 896 sizex = (self.imgshape2D[0] * self.viewer.camera.zoom) / 2 897 sizey = (self.imgshape2D[1] * self.viewer.camera.zoom) / 2 898 if os.path.exists(outname): 899 os.remove(outname) 900 901 ## take a screenshot of each frame 902 for frame in range(self.nframes): 903 self.viewer.dims.set_point(0, frame) 904 shot = self.viewer.window.screenshot(canvas_only=True, flash=False) 905 ## remove border: movie is at the center 906 centx = int(shot.shape[0] / 2) + 1 907 centy = int(shot.shape[1] / 2) + 1 908 shot = shot[ 909 int(centx - sizex) : int(centx + sizex), 910 int(centy - sizey) : int(centy + sizey), 911 ] 912 ut.appendToTif(shot, outname) 913 self.viewer.camera.update(save_view) 914 if save_frame is not None: 915 self.viewer.dims.set_point(0, save_frame) 916 ut.show_info("Movie " + outname + " saved") 917 918 def reset_data(self): 919 """Reset EpiCure data (group, suspect, graph)""" 920 self.inspecting.reset_all_events() 921 self.reset_groups() 922 self.tracking.graph = None 923 924 def junctions_to_label(self): 925 """convert epyseg/skeleton result (junctions) to labels map""" 926 ## ensure that skeleton is thin enough 927 for z in range(self.seg.shape[0]): 928 self.skel_one_frame(z) 929 self.seg = ut.reset_labels(self.seg, closing=True) 930 931 def skel_one_frame(self, z): 932 """From segmentation of junctions of one frame, get it as a correct skeleton""" 933 skel = skeletonize(self.seg[z] / np.max(self.seg[z])) 934 skel = ut.copy_border(skel, self.seg[z]) 935 self.seg[z] = np.invert(skel) 936 937 def reset_labels(self): 938 """Reset all labels, ensure unicity""" 939 if self.epi_metadata["EpithelialCells"]: 940 ### packed (contiguous cells), ensure that they are separated by one pixel only 941 skel = self.get_skeleton() 942 skel = np.uint32(skel) 943 self.seg = skel 944 self.seglayer.data = skel 945 self.junctions_to_label() 946 self.seglayer.data = self.seg 947 else: 948 self.get_cells() 949 950 def check_extrusions_sanity(self): 951 """Check that extrusions seem to be correct (last of tracks )""" 952 extrusions = self.inspecting.get_events_from_type("extrusion") 953 nrem = 0 954 if (extrusions is not None) and (extrusions != []): 955 for extr_id in extrusions: 956 pos, label = self.inspecting.get_event_infos(extr_id) 957 last_frame = self.tracking.get_last_frame(label) 958 if pos[0] != last_frame: 959 if self.verbose > 1: 960 print("Extrusion " + str(extr_id) + " at frame " + str(pos[0]) + " not at the end of track " + str(label)) 961 print("Removing it") 962 self.inspecting.remove_one_event(extr_id) 963 nrem = nrem + 1 964 print("Removed " + str(nrem) + " extrusions that dit not correspond to the end of tracks") 965 966 def prepare_labels(self): 967 """Process the labels to be in a correct Epicurable format""" 968 if self.epi_metadata["EpithelialCells"]: 969 if self.epi_metadata["Reloading"]: 970 ## if opening an already EpiCured movie, assume it's in correct format 971 return 972 ### packed (contiguous cells), ensure that they are separated by one pixel only 973 self.thin_boundaries() 974 else: 975 self.get_cells() 976 977 def get_cells(self): 978 """Non jointive cells: check label unicity""" 979 for frame in self.seg: 980 if ut.non_unique_labels(frame): 981 self.seg = ut.reset_labels(self.seg, closing=True) 982 return 983 984 def thin_boundaries(self): 985 """ " Assure that all boundaries are only 1 pixel thick""" 986 if self.process_parallel: 987 self.seg = Parallel(n_jobs=self.nparallel)(delayed(ut.thin_seg_one_frame)(zframe) for zframe in self.seg) 988 self.seg = np.array(self.seg) 989 else: 990 for z in range(self.seg.shape[0]): 991 self.seg[z] = ut.thin_seg_one_frame(self.seg[z]) 992 993 def add_skeleton(self): 994 """add a layer containing the skeleton movie of the segmentation""" 995 # display the segmentation file movie 996 if self.viewer is not None: 997 skel = np.zeros(self.seg.shape, dtype="uint8") 998 skel[self.seg == 0] = 1 999 skel = self.get_skeleton(viewer=self.viewer) 1000 ut.remove_layer(self.viewer, "Skeleton") 1001 skellayer = self.viewer.add_image(skel, name="Skeleton", blending="additive", opacity=1, scale=self.viewer.layers["Movie"].scale) 1002 skellayer.reset_contrast_limits() 1003 skellayer.contrast_limits = (0, 1) 1004 1005 def get_skeleton(self, viewer=None): 1006 """convert labels movie to skeleton (thin boundaries)""" 1007 if self.seg is None: 1008 return None 1009 parallel = 0 1010 if self.process_parallel: 1011 parallel = self.nparallel 1012 return ut.get_skeleton(self.seg, viewer=viewer, verbose=self.verbose, parallel=parallel) 1013 1014 ############ Label functions 1015 1016 def get_free_labels(self, nlab): 1017 """Get the nlab smallest unused labels""" 1018 used = set(self.tracking.get_track_list()) 1019 return ut.get_free_labels(used, nlab) 1020 1021 def get_free_label(self): 1022 """Return the first free label""" 1023 return self.get_free_labels(1)[0] 1024 1025 def has_label(self, label): 1026 """Check if label is present in the tracks""" 1027 return self.tracking.has_track(label) 1028 1029 def has_labels(self, labels): 1030 """Check if labels are present in the tracks""" 1031 return self.tracking.has_tracks(labels) 1032 1033 def nlabels(self): 1034 """Number of unique tracks""" 1035 return self.tracking.nb_tracks() 1036 1037 def get_labels(self): 1038 """Return list of labels in tracks""" 1039 return list(self.tracking.get_track_list()) 1040 1041 ########## Edit tracks 1042 def delete_tracks(self, tracks): 1043 """Remove all the tracks from the Track layer""" 1044 self.tracking.remove_tracks(tracks) 1045 1046 def delete_track(self, label, frame=None): 1047 """Remove (part of) the track""" 1048 if frame is None: 1049 self.tracking.remove_track(label) 1050 else: 1051 self.tracking.remove_one_frame(label, frame, handle_gaps=self.forbid_gaps) 1052 1053 def update_centroid(self, label, frame): 1054 """Track label has been change at given frame""" 1055 if label not in self.tracking.has_track(label): 1056 if self.verbose > 1: 1057 print("Track " + str(label) + " not found") 1058 return 1059 self.tracking.update_centroid(label, frame) 1060 1061 ########## Edit label 1062 def get_label_indexes(self, label, start_frame=0): 1063 """Returns the indexes where label is present in segmentation, starting from start_frame""" 1064 indmodif = [] 1065 if self.verbose > 2: 1066 start_time = ut.start_time() 1067 pos = self.tracking.get_track_column(track_id=label, column="fullpos") 1068 pos = pos[pos[:, 0] >= start_frame] 1069 ## if nothing in pos, pb with track data 1070 if pos is None or len(pos) == 0: 1071 ut.show_warning("Something wrong in the track data. Resetting track data (can take time)") 1072 self.tracking.reset_tracks() 1073 self.get_label_indexes(label, start_frame) 1074 1075 indmodif = np.argwhere(self.seg[pos[:, 0]] == label) 1076 indmodif = ut.shiftFrames(indmodif, pos[:, 0]) 1077 if self.verbose > 2: 1078 ut.show_duration(start_time, header="Label indexes found in ") 1079 return indmodif 1080 1081 def replace_label(self, label, new_label, start_frame=0): 1082 """Replace label with new_label from start_frame - Relabelling only""" 1083 indmodif = self.get_label_indexes(label, start_frame) 1084 new_labels = [new_label] * len(indmodif) 1085 self.change_labels(indmodif, new_labels, replacing=True) 1086 1087 def change_labels_frommerge(self, indmodif, new_labels, remove_labels): 1088 """Change the value at pixels indmodif to new_labels and update tracks/graph. Full remove of the two merged labels""" 1089 if len(indmodif) > 0: 1090 ## get effectively changed labels 1091 indmodif, new_labels, _ = ut.setNewLabel(self.seglayer, indmodif, new_labels, add_frame=None, return_old=False) 1092 if len(new_labels) > 0: 1093 self.update_added_labels(indmodif, new_labels) 1094 self.update_removed_labels(indmodif, remove_labels) 1095 self.seglayer.refresh() 1096 1097 def change_labels(self, indmodif, new_labels, replacing=False): 1098 """Change the value at pixels indmodif to new_labels and update tracks/graph 1099 1100 Assume that only label at current frame can have its shape modified. Other changed label is only relabelling at frames > current frame (child propagation) 1101 """ 1102 if len(indmodif) > 0: 1103 ## get effectively changed labels 1104 indmodif, new_labels, old_labels = ut.setNewLabel(self.seglayer, indmodif, new_labels, add_frame=None) 1105 if len(new_labels) > 0: 1106 if replacing: 1107 self.update_replaced_labels(indmodif, new_labels, old_labels) 1108 else: 1109 ## the only label to change are the current frame (smaller one), the other are only relabelling (propagation) 1110 cur_frame = np.min(indmodif[0]) 1111 to_reshape = indmodif[0] == cur_frame 1112 self.update_changed_labels((indmodif[0][to_reshape], indmodif[1][to_reshape], indmodif[2][to_reshape]), new_labels[to_reshape], old_labels[to_reshape]) 1113 to_relab = np.invert(to_reshape) 1114 self.update_replaced_labels((indmodif[0][to_relab], indmodif[1][to_relab], indmodif[2][to_relab]), new_labels[to_relab], old_labels[to_relab]) 1115 self.seglayer.refresh() 1116 1117 def get_mask(self, label, start=None, end=None): 1118 """Get mask of label from frame start to frame end""" 1119 if (start is None) or (end is None): 1120 start, end = self.tracking.get_extreme_frames(label) 1121 crop = self.seg[start : (end + 1)] 1122 mask = np.isin(crop, [label]) * 1 1123 return mask 1124 1125 def get_label_movie(self, label, extend=1.25): 1126 """Get movie centered on label""" 1127 start, end = self.tracking.get_extreme_frames(label) 1128 mask = self.get_mask(label, start, end) 1129 boxes = [] 1130 centers = [] 1131 max_box = 0 1132 for frame in mask: 1133 props = regionprops(frame) 1134 bbox = props[0].bbox 1135 boxes.append(bbox) 1136 centers.append(props[0].centroid) 1137 for i in range(2): 1138 max_box = max(max_box, bbox[i + 2] - bbox[i]) 1139 1140 box_size = int(max_box * extend) 1141 movie = np.zeros((end - start + 1, box_size, box_size)) 1142 for i, frame in enumerate(range(start, end + 1)): 1143 xmin = int(centers[i][0] - box_size / 2) 1144 xminshift = 0 1145 if xmin < 0: 1146 xminshift = -xmin 1147 xmin = 0 1148 xmax = xmin + box_size - xminshift 1149 xmaxshift = box_size 1150 if xmax > self.imgshape2D[0]: 1151 xmaxshift = self.imgshape2D[0] - xmax 1152 xmax = self.imgshape2D[0] 1153 1154 ymin = int(centers[i][1] - max_box / 2) 1155 yminshift = 0 1156 if ymin < 0: 1157 yminshift = -ymin 1158 ymin = 0 1159 ymax = ymin + box_size - yminshift 1160 ymaxshift = box_size 1161 if ymax > self.imgshape2D[1]: 1162 ymaxshift = self.imgshape2D[1] - ymax 1163 ymax = self.imgshape2D[1] 1164 1165 movie[i, xminshift:xmaxshift, yminshift:ymaxshift] = self.img[frame, xmin:xmax, ymin:ymax] 1166 return movie 1167 1168 ### Check individual cell features 1169 def cell_radius(self, label, frame): 1170 """Approximate the cell radius at given frame""" 1171 area = np.sum(self.seg[frame] == label) 1172 radius = math.sqrt(area / math.pi) 1173 return radius 1174 1175 def cell_area(self, label, frame): 1176 """Approximate the cell radius at given frame""" 1177 area = np.sum(self.seg[frame] == label) 1178 return area 1179 1180 def cell_on_border(self, label, frame): 1181 """Check if a given cell is on border of the image""" 1182 bbox = ut.getBBox2D(self.seg[frame], label) 1183 out = ut.outerBBox2D(bbox, self.imgshape2D, margin=3) 1184 return out 1185 1186 ###### Synchronize tracks whith labels changed 1187 def add_label(self, labels, frame=None): 1188 """Add a label to the tracks""" 1189 if frame is not None: 1190 if np.isscalar(labels): 1191 labels = [labels] 1192 self.tracking.add_one_frame(labels, frame, refresh=True) 1193 else: 1194 if self.verbose > 1: 1195 print("TODO add label no frame") 1196 1197 def add_one_label_to_track(self, label): 1198 """Add the track data of a given label if missing""" 1199 iframe = 0 1200 while (iframe < self.nframes) and (label not in self.seg[iframe]): 1201 iframe = iframe + 1 1202 while (iframe < self.nframes) and (label in self.seg[iframe]): 1203 self.tracking.add_one_frame([label], iframe) 1204 iframe = iframe + 1 1205 1206 def update_label(self, label, frame): 1207 """Update the given label at given frame""" 1208 self.tracking.update_track_on_frame([label], frame) 1209 1210 def update_changed_labels(self, indmodif, new_labels, old_labels, full=False): 1211 """Check what had been modified, and update tracks from it, looking frame by frame""" 1212 ## check all the old_labels if still present or not 1213 if self.verbose > 1: 1214 start_time = time.time() 1215 frames = np.unique(indmodif[0]) 1216 all_deleted = [] 1217 debug_verb = self.verbose > 2 1218 if debug_verb: 1219 print("Updating labels in frames " + str(frames)) 1220 for frame in frames: 1221 keep = indmodif[0] == frame 1222 ## check old labels if totally removed or not 1223 deleted = np.setdiff1d(old_labels[keep], self.seg[frame]) 1224 left = np.setdiff1d(old_labels[keep], deleted) 1225 if deleted.shape[0] > 0: 1226 self.tracking.remove_one_frame(deleted, frame, handle_gaps=False, refresh=False) 1227 if self.forbid_gaps: 1228 all_deleted = all_deleted + list(set(deleted) - set(all_deleted)) 1229 if left.shape[0] > 0: 1230 self.tracking.update_track_on_frame(left, frame) 1231 ## now check new labels 1232 nlabels = np.unique(new_labels[keep]) 1233 if nlabels.shape[0] > 0: 1234 self.tracking.update_track_on_frame(nlabels, frame) 1235 if debug_verb: 1236 print("Labels deleted at frame " + str(frame) + " " + str(deleted) + " or added " + str(nlabels)) 1237 1238 def update_added_labels(self, indmodif, new_labels): 1239 """Update tracks of labels that have been fully added""" 1240 if self.verbose > 1: 1241 start_time = time.time() 1242 1243 ## Deleted labels 1244 frames = np.unique(indmodif[0]) 1245 self.tracking.add_tracks_fromindices(indmodif, new_labels) 1246 if self.forbid_gaps: 1247 ## Check if some gaps has been created in tracks (remove middle(s) frame(s)) 1248 added = list(set(new_labels)) 1249 if len(added) > 0: 1250 self.handle_gaps(added, verbose=0) 1251 1252 if self.verbose > 1: 1253 ut.show_duration(start_time, "updated added tracks in ") 1254 1255 def update_removed_labels(self, indmodif, old_labels): 1256 """Update tracks of labels that have been fully removed""" 1257 if self.verbose > 1: 1258 start_time = time.time() 1259 1260 ## Deleted labels 1261 frames = np.unique(indmodif[0]) 1262 self.tracking.remove_on_frames(np.unique(old_labels), frames) 1263 if self.forbid_gaps: 1264 ## Check if some gaps has been created in tracks (remove middle(s) frame(s)) 1265 deleted = list(set(old_labels)) 1266 if len(deleted) > 0: 1267 self.handle_gaps(deleted, verbose=0) 1268 1269 if self.verbose > 1: 1270 ut.show_duration(start_time, "updated removed tracks in ") 1271 1272 def update_replaced_labels(self, indmodif, new_labels, old_labels): 1273 """Old_labels were fully replaced by new_labels on some frames, update tracks from it""" 1274 if self.verbose > 1: 1275 start_time = time.time() 1276 1277 ## Deleted labels 1278 frames = np.unique(indmodif[0]) 1279 self.tracking.replace_on_frames(np.unique(old_labels), np.unique(new_labels), frames) 1280 if self.forbid_gaps: 1281 ## Check if some gaps has been created in tracks (remove middle(s) frame(s)) 1282 deleted = list(set(old_labels)) 1283 if len(deleted) > 0: 1284 self.handle_gaps(deleted, verbose=0) 1285 1286 if self.verbose > 1: 1287 ut.show_duration(start_time, "updated replaced tracks in ") 1288 1289 def handle_gaps(self, track_list, verbose=None): 1290 """Check and fix gaps in tracks""" 1291 if verbose is None: 1292 verbose = self.verbose 1293 gaped = self.tracking.check_gap(track_list, verbose=verbose) 1294 if len(gaped) > 0: 1295 if self.verbose > 0: 1296 print("Relabelling tracks with gaps") 1297 self.fix_gaps(gaped) 1298 1299 def fix_gaps(self, gaps): 1300 """Fix when some gaps has been created in tracks""" 1301 for gap in gaps: 1302 gap_frames = self.tracking.gap_frames(gap) 1303 cur_gap = gap 1304 for gapy in gap_frames: 1305 new_value = self.get_free_label() 1306 self.replace_label(cur_gap, new_value, gapy) 1307 cur_gap = new_value 1308 1309 def swap_labels(self, lab, olab, frame): 1310 """Exchange two labels""" 1311 self.tracking.swap_frame_id(lab, olab, frame) 1312 1313 def swap_tracks(self, lab, olab, start_frame): 1314 """Exchange two tracks""" 1315 ## split the two labels to unused value 1316 tmp_labels = self.get_free_labels(2) 1317 for i, laby in enumerate([lab, olab]): 1318 self.replace_label(laby, tmp_labels[i], start_frame) 1319 1320 ## replace the two initial labels, in inversed order 1321 self.replace_label(tmp_labels[0], olab, start_frame) 1322 self.replace_label(tmp_labels[1], lab, start_frame) 1323 1324 def split_track(self, label, frame): 1325 """Split a track at given frame""" 1326 new_label = self.get_free_label() 1327 self.replace_label(label, new_label, frame) 1328 if self.verbose > 0: 1329 ut.show_info("Split track " + str(label) + " from frame " + str(frame)) 1330 return new_label 1331 1332 def update_changed_labels_img(self, img_before, img_after, added=True, removed=True): 1333 """Update tracks from changes between the two labelled images""" 1334 if self.verbose > 1: 1335 print("Updating changed labels from images") 1336 indmodif = np.argwhere(img_before != img_after).tolist() 1337 if len(indmodif) <= 0: 1338 return 1339 indmodif = tuple(np.array(indmodif).T) 1340 new_labels = img_after[indmodif] 1341 old_labels = img_before[indmodif] 1342 self.update_changed_labels(indmodif, new_labels, old_labels) 1343 1344 def added_labels_oneframe(self, frame, img_before, img_after): 1345 """Update added tracks between the two labelled images at frame""" 1346 ## Look for added labels 1347 added_labels = np.setdiff1d(img_after, img_before) 1348 self.tracking.add_one_frame(added_labels, frame, refresh=True) 1349 1350 def removed_labels(self, img_before, img_after, frame=None): 1351 """Update removed tracks between the two labelled images""" 1352 ## Look for added labels 1353 deleted_labels = np.setdiff1d(img_before, img_after) 1354 if frame is None: 1355 self.tracking.remove_tracks(deleted_labels) 1356 else: 1357 self.tracking.remove_one_frame(track_id=deleted_labels.tolist(), frame=frame, handle_gaps=self.forbid_gaps) 1358 1359 def remove_label(self, label, force=False): 1360 """Remove a given label if allowed""" 1361 ut.changeLabel(self.seglayer, label, 0) 1362 self.tracking.remove_tracks(label) 1363 self.seglayer.refresh() 1364 1365 def remove_labels(self, labels, force=False): 1366 """Remove all allowed labels""" 1367 inds = [] 1368 for lab in labels: 1369 # if (force) or (not self.locked_label(label)): 1370 inds = inds + ut.getLabelIndexes(self.seglayer.data, lab, None) 1371 ut.setNewLabel(self.seglayer, inds, 0) 1372 self.tracking.remove_tracks(labels) 1373 1374 def keep_labels(self, labels, force=True): 1375 """Remove all other labels that are not in labels""" 1376 inds = [] 1377 toremove = list(set(self.tracking.get_track_list()) - set(labels)) 1378 # for lab in self.tracking.get_track_list(): 1379 # if lab not in labels: 1380 # if (force) or (not self.locked_label(label)): 1381 for lab in toremove: 1382 inds = inds + ut.getLabelIndexes(self.seglayer.data, lab, None) 1383 # toremove.append(lab) 1384 ut.setNewLabel(self.seglayer, inds, 0) 1385 self.tracking.remove_tracks(toremove) 1386 1387 def get_frame_features(self, frame): 1388 """Measure the label properties of given frame""" 1389 return regionprops(self.seg[frame]) 1390 1391 def updates_after_tracking(self): 1392 """When tracking has been done, update events, others""" 1393 self.inspecting.get_divisions() 1394 1395 ####################### 1396 ## Classified cells options 1397 def get_all_groups(self, numeric=False): 1398 """Add all groups info""" 1399 if numeric: 1400 groups = [0] * self.nlabels() 1401 else: 1402 groups = ["None"] * self.nlabels() 1403 for igroup, gr in self.groups.keys(): 1404 indexes = self.tracking.get_track_indexes(self.groups[gr]) 1405 if numeric: 1406 groups[indexes] = igroup + 1 1407 else: 1408 groups[indexes] = gr 1409 return groups 1410 1411 def get_groups(self, labels, numeric=False): 1412 """Add the group info of the given labels (repeated)""" 1413 if numeric: 1414 groups = [0] * len(labels) 1415 else: 1416 groups = ["Ungrouped"] * len(labels) 1417 for lab in np.unique(labels): 1418 gr = self.find_group(lab) 1419 if gr is None: 1420 continue 1421 if numeric: 1422 gr = self.groups.keys().index() + 1 1423 indexes = (np.argwhere(labels == lab)).flatten() 1424 for ind in indexes: 1425 groups[ind] = gr 1426 return groups 1427 1428 def cells_ingroup(self, labels, group): 1429 """Put the cell "label" in group group, add it if new group""" 1430 presents = self.has_labels(labels) 1431 labels = np.array(labels)[presents] 1432 if group not in self.groups.keys(): 1433 self.groups[group] = [] 1434 self.update_group_lists() 1435 ## add only non present label(s) 1436 grlabels = self.groups[group] 1437 self.groups[group] = list(set(grlabels + labels.tolist())) 1438 1439 def group_of_labels(self): 1440 """List the group of each label""" 1441 res = {} 1442 for group, labels in self.groups.items(): 1443 for label in labels: 1444 res[label] = group 1445 return res 1446 1447 def find_group(self, label): 1448 """Find in which group the label is""" 1449 for gr, labs in self.groups.items(): 1450 if label in labs: 1451 return gr 1452 return None 1453 1454 def cell_removegroup(self, label): 1455 """Detach the cell from its group""" 1456 if not self.has_label(label): 1457 if self.verbose > 1: 1458 print("Cell " + str(label) + " missing") 1459 group = self.find_group(label) 1460 if group is not None: 1461 self.groups[group].remove(label) 1462 if len(self.groups[group]) <= 0: 1463 del self.groups[group] 1464 self.update_group_lists() 1465 1466 def update_group_lists(self): 1467 """Update all the lists depending on the group names""" 1468 if self.outputing is not None: 1469 self.outputing.update_selection_list() 1470 if self.editing is not None: 1471 self.editing.update_group_lists() 1472 1473 def reset_group(self, group_name): 1474 """Reset/remove a given group""" 1475 if group_name == "All": 1476 self.reset_groups() 1477 return 1478 if group_name in self.groups.keys(): 1479 del self.groups[group_name] 1480 self.update_group_lists() 1481 1482 def reset_groups(self): 1483 """Remove all group information for all cells""" 1484 self.groups = {} 1485 self.update_group_lists() 1486 1487 def draw_groups(self): 1488 """Draw all the epicells colored by their group""" 1489 grouped = np.zeros(self.seg.shape, np.uint8) 1490 if (self.groups is None) or len(self.groups.keys()) == 0: 1491 return grouped 1492 for group, labels in self.groups.items(): 1493 igroup = self.get_group_index(group) + 1 1494 np.place(grouped, np.isin(self.seg, labels), igroup) 1495 return grouped 1496 1497 def get_group_index(self, group): 1498 """Get the index of group in the list of groups""" 1499 if group in list(self.groups.keys()): 1500 igroup = list(self.groups.keys()).index(group) 1501 return igroup 1502 return -1 1503 1504 ######### ROI 1505 def only_current_roi(self, frame): 1506 """Put 0 everywhere outside the current ROI""" 1507 roi_labels = self.editing.get_labels_inside() 1508 if roi_labels is None: 1509 return None 1510 # remove all other labels that are not in roi_labels 1511 roilab = np.copy(self.seg[frame]) 1512 np.place(roilab, np.isin(roilab, roi_labels, invert=True), 0) 1513 return roilab
30class EpiCure: 31 def __init__(self, viewer=None): 32 """ 33 Initialize the EpiCure viewer instance. 34 35 :param: viewer (napari.Viewer, optional): An existing napari Viewer instance to use. 36 If None, a new Viewer instance will be created with show=False. 37 Defaults to None. 38 """ 39 self.viewer = viewer 40 """ Napari viewer that is used for this session """ 41 if self.viewer is None: 42 self.viewer = napari.Viewer(show=False) 43 self.viewer.title = "Napari - EpiCure" 44 self.reset() 45 46 def reset(self): 47 """ Reset all the parameters to the default values """ 48 self.init_epicure_metadata() ## initialize metadata variables (scalings, channels) 49 self.img = None 50 """ data of the raw movie """ 51 self.inspecting = None 52 """ interface for inspection options """ 53 self.others = None 54 self.imgshape2D = None ## width, height of the image 55 self.nframes = None ## Number of time frames 56 self.thickness = 4 ## thickness of junctions, wider 57 self.minsize = 4 ## smallest number of pixels in a cell 58 self.verbose = 1 ## level of printing messages (None/few, normal, debug mode) 59 self.event_class = ["division", "extrusion", "suspect"] ## list of possible events 60 self.main_channel = 0 ## position of the main channel (raw movie) 61 62 self.overtext = dict() 63 self.help_index = 1 ## current display index of help overlay 64 self.blabla = None ## help window 65 self.groups = {} 66 self.tracked = 0 ## has done a tracking 67 self.process_parallel = False ## Do some operations in parallel (n frames in parallel) 68 self.nparallel = 4 ## number of parallel threads 69 self.dtype = np.uint32 ## label type, default 32 but if less labels, reduce it 70 self.outputing = None ## non initialized yet 71 72 self.forbid_gaps = False ## allow gaps in track or not 73 74 self.pref = Preferences() 75 self.shortcuts = self.pref.get_shortcuts() ## user specific shortcuts 76 self.settings = self.pref.get_settings() ## user specific preferences 77 ## display settings 78 self.display_colors = None ## settings for changing some display colors 79 if "Display" in self.settings: 80 if "Colors" in self.settings["Display"]: 81 self.display_colors = self.settings["Display"]["Colors"] 82 83 84 def init_epicure_metadata(self): 85 """ Fills metadata with default values """ 86 ## scalings and unit names 87 self.epi_metadata = {} 88 self.epi_metadata["ScaleXY"] = 1 89 self.epi_metadata["UnitXY"] = "um" 90 self.epi_metadata["ScaleT"] = 1 91 self.epi_metadata["UnitT"] = "min" 92 self.epi_metadata["MainChannel"] = 0 93 self.epi_metadata["Allow gaps"] = True 94 self.epi_metadata["Verbose"] = 1 95 self.epi_metadata["Scale bar"] = True 96 self.epi_metadata["MovieFile"] = "" 97 self.epi_metadata["SegmentationFile"] = "" 98 self.epi_metadata["EpithelialCells"] = True ## epithelial (packed) cells 99 self.epi_metadata["Reloading"] = False ## Never been epiCured yet 100 101 def get_resetbtn_color(self): 102 """Returns the color of Reset buttons if defined""" 103 if "Display" in self.settings: 104 if "Colors" in self.settings["Display"]: 105 if "Reset button" in self.settings["Display"]["Colors"]: 106 return self.settings["Display"]["Colors"]["Reset button"] 107 return None 108 109 def set_thickness(self, thick): 110 """ 111 Thickness of junctions (half thickness) 112 113 :param: thick set thickness value to input value 114 """ 115 self.thickness = thick 116 117 def movie_from_layer(self, layer, imgpath): 118 """ 119 Prepare the intensity movie from opened layer, and get metadata. 120 121 Resets the internal state, loads image data from the provided layer, 122 handles temporal and channel dimensions, and prepares the movie for processing. 123 124 It extracts metadata including file path and pixel scale, and attempts to handle various 125 image formats (2D, 3D, 4D with different dimension orders). 126 127 :param: layer: A napari layer object containing the image data and scale information. 128 The layer's data attribute should contain the image array. 129 :param: imgpath (str): Absolute or relative file path to the image file being loaded. 130 131 :return: 132 A tuple containing: 133 - caxis (int or None): The axis index corresponding to the channel dimension, 134 or None if no multiple channels are detected. 135 - cval (int): The number of channels found in the image, or 0 if no channels 136 are detected. 137 """ 138 self.reset() ## reload everything 139 self.epi_metadata["MovieFile"] = os.path.abspath(imgpath) 140 ## if the layer is scaled, should be the right scale 141 self.epi_metadata["ScaleXY"] = layer.scale[2] 142 self.img = layer.data 143 nchan = 0 144 if len(self.img.shape)>3: 145 ## Format TCYX in general 146 nchan = self.img.shape[1] 147 ## transform static image to movie (add temporal dimension) 148 if len(self.img.shape) == 2: 149 self.img = np.expand_dims(self.img, axis=0) 150 caxis = None 151 cval = 0 152 if nchan > 0 or len(self.img.shape) > 3: 153 if nchan > 0 and len(self.img.shape) > 3: 154 ## multiple chanels and multiple slices, order axis should be TCXY 155 caxis = 1 156 cval = nchan 157 else: 158 ## one image with multiple chanels 159 minshape = min(self.img.shape) 160 caxis = self.img.shape.index(minshape) 161 cval = minshape 162 self.mov = self.img 163 164 ## display the movie: rename the layer 165 ut.remove_layer(self.viewer, "Movie") 166 layer.name = "Movie" 167 168 self.imgshape = self.viewer.layers["Movie"].data.shape 169 self.imgshape2D = self.imgshape[1:3] 170 self.nframes = self.imgshape[0] 171 return caxis, cval 172 173 174 def load_movie(self, imgpath): 175 """ 176 Load the intensity movie, and get metadata 177 178 :param: imgpath: full path to where the movie file is 179 """ 180 self.reset() ## reload everything 181 self.epi_metadata["MovieFile"] = os.path.abspath(imgpath) 182 self.img, nchan, self.epi_metadata["ScaleXY"], self.epi_metadata["UnitXY"], self.epi_metadata["ScaleT"], self.epi_metadata["UnitT"] = ut.open_image( 183 self.epi_metadata["MovieFile"], get_metadata=True, verbose=self.verbose > 1 184 ) 185 ## transform static image to movie (add temporal dimension) 186 if len(self.img.shape) == 2: 187 self.img = np.expand_dims(self.img, axis=0) 188 caxis = None 189 cval = 0 190 if nchan > 0 or len(self.img.shape) > 3: 191 if nchan > 0 and len(self.img.shape) > 3: 192 ## multiple chanels and multiple slices, order axis should be TCXY 193 caxis = 1 194 cval = nchan 195 else: 196 ## one image with multiple chanels 197 minshape = min(self.img.shape) 198 caxis = self.img.shape.index(minshape) 199 cval = minshape 200 self.mov = self.img 201 202 ## display the movie 203 ut.remove_layer(self.viewer, "Movie") 204 mview = self.viewer.add_image(self.img, name="Movie", blending="additive", colormap="gray") 205 mview.contrast_limits = self.quantiles() 206 mview.gamma = 0.95 207 208 self.imgshape = self.viewer.layers["Movie"].data.shape 209 self.imgshape2D = self.imgshape[1:3] 210 self.nframes = self.imgshape[0] 211 return caxis, cval 212 213 214 def quantiles(self): 215 """ Returns the quantiles 1% and 99.999% of the raw image to set the display """ 216 return tuple(np.quantile(self.img, [0.01, 0.9999])) 217 218 def set_verbose(self, verbose): 219 """ 220 Set verbose level 221 222 :param: verbose: amount of message that will be displayed in the Terminal console, from 0 (none) to 4 (a lot, for debugging) 223 """ 224 self.verbose = verbose 225 self.epi_metadata["Verbose"] = verbose 226 227 def set_gaps_option(self, allow_gap): 228 """Set the mode for gap allowing/forbid in tracks 229 230 :param: allow_gap: boolean. Indicates if gap in tracks (missing cell in one or more frames) should be allowed or not. 231 """ 232 self.epi_metadata["Allow gaps"] = allow_gap 233 self.forbid_gaps = not allow_gap 234 235 def set_epithelia(self, epithelia): 236 """ 237 Set the mode for cell packing (touching or not especially) 238 239 :param: epithelia: boolean, True if cells are touching 240 """ 241 self.epi_metadata["EpithelialCells"] = epithelia 242 243 def set_scalebar(self, show_scalebar): 244 """ 245 Show or not the scale bar, and set its value 246 247 :param: show_scalebar: boolean, set the visibility of the scale bar 248 """ 249 self.epi_metadata["Scale bar"] = show_scalebar 250 if self.viewer is not None: 251 self.viewer.scale_bar.visible = show_scalebar 252 self.viewer.scale_bar.unit = self.epi_metadata["UnitXY"] 253 for lay in self.viewer.layers: 254 lay.scale = [1, self.epi_metadata["ScaleXY"], self.epi_metadata["ScaleXY"]] 255 self.viewer.reset_view() 256 257 def set_scales(self, scalexy, scalet, unitxy, unitt): 258 """ 259 Set the scaling units for outputs. Put the values in Epicure metadata object 260 261 :param: scalexy: size of one pixel in X,Y directions 262 :param: scalet: duration of one frame (acquisition frequency) 263 :param: unitxy: name of the unit in which the scale is given 264 :param: unitt: name of the temporal unit in which the scale is given 265 """ 266 self.epi_metadata["ScaleXY"] = scalexy 267 self.epi_metadata["ScaleT"] = scalet 268 self.epi_metadata["UnitXY"] = unitxy 269 self.epi_metadata["UnitT"] = unitt 270 if self.viewer is not None: 271 self.viewer 272 if self.verbose > 0: 273 ut.show_info("Movie scales set to " + str(self.epi_metadata["ScaleXY"]) + " " + self.epi_metadata["UnitXY"] + " and " + str(self.epi_metadata["ScaleT"]) + " " + self.epi_metadata["UnitT"]) 274 275 def set_chanel(self, chan, chanaxis): 276 """ 277 Update the movie to the correct chanel 278 279 :param: chan: channel in which the raw movie is 280 :param: chanaxis: in which axis is the color channels information (usually format is TCYX, so will be 1) 281 """ 282 self.img = np.rollaxis(np.copy(self.mov), chanaxis, 0)[chan] 283 if len(self.img.shape) == 2: 284 self.img = np.expand_dims(self.img, axis=0) 285 ## udpate the image shape informations 286 self.imgshape = self.img.shape 287 self.imgshape2D = self.imgshape[1:3] 288 self.nframes = self.imgshape[0] 289 self.main_channel = chan 290 if self.viewer is not None: 291 mview = self.viewer.layers["Movie"] 292 mview.data = self.img 293 mview.contrast_limits = self.quantiles() 294 mview.gamma = 0.95 295 mview.refresh() 296 297 def add_other_chanels(self, chan, chanaxis): 298 """ Open other channels if option selected """ 299 others_raw = np.delete(self.mov, chan, axis=chanaxis) 300 self.others = [] 301 self.others_chanlist = [] 302 if self.others is not None: 303 others_raw = np.rollaxis(others_raw, chanaxis, 0) 304 for ochan in range(others_raw.shape[0]): 305 purechan = ochan 306 if purechan >= chan: 307 purechan = purechan + 1 308 self.others_chanlist.append(purechan) 309 if len(others_raw[ochan].shape) == 2: 310 expanded = np.expand_dims(others_raw[ochan], axis=0) 311 self.others.append( expanded ) 312 else: 313 self.others.append( others_raw[ochan] ) 314 mview = self.viewer.add_image( self.others[ochan], name="MovieChannel_"+str(purechan), blending="additive", colormap="gray" ) 315 mview.contrast_limits=tuple(np.quantile(self.others[ochan],[0.01, 0.9999])) 316 mview.gamma=0.95 317 mview.visible = False 318 319 def import_trackmate(self, segpath, verbose=0): 320 """ Load segmentation and tracks from TrackMate XML file """ 321 if verbose > 1: 322 print("Importing segmentation and tracks from TrackMate XML file") 323 np.set_printoptions(suppress=True, floatmode="maxprec_equal") 324 325 img_data_tag = tm._get_ImageData_tag(segpath) 326 metadata = tm._get_metadata(img_data_tag) 327 seg_shape = (int(metadata["nframes"]), int(metadata["height"]), int(metadata["width"])) 328 segmentation = np.zeros(seg_shape, dtype=np.uint16)-1 329 positions, tracks = tm._parse_Model_tag(segpath, metadata, segmentation) 330 label_mapping = tm._build_label_mapping(positions, tracks) 331 positions = tm.relabel_positions(label_mapping, positions) 332 tracks = tm.relabel_tracks(label_mapping, tracks) 333 segmentation = tm.relabel_segmentation(label_mapping, segmentation) 334 return segmentation, tracks 335 336 337 def load_segmentation(self, seg_input): 338 """Load the segmentation file""" 339 start_time = ut.start_time() 340 self.graph = None ## no loaded graph 341 ## compatibility to string input, the path to the image or a dictionnary 342 if isinstance(seg_input, dict): 343 segpath = seg_input["File"] 344 else: 345 segpath = seg_input 346 self.epi_metadata["SegmentationFile"] = segpath 347 if isinstance(seg_input, dict) and "Layer" in seg_input: 348 ## take the segmentation data and close it 349 self.seg = seg_input["Layer"].data 350 ut.remove_layer(self.viewer, seg_input["Layer"]) 351 else: 352 if str(segpath).endswith(".xml"): 353 ## import a TrackMate file 354 self.seg, self.graph = self.import_trackmate(segpath, verbose=self.verbose>1) 355 else: 356 self.seg, _, _, _, _, _ = ut.open_image(segpath, get_metadata=False, verbose=self.verbose > 1) 357 self.seg = np.uint32(self.seg) 358 ## transform static image to movie (add temporal dimension) 359 if len(self.seg.shape) == 2: 360 self.seg = np.expand_dims(self.seg, axis=0) 361 ## ensure that the shapes are correctly set 362 self.imgshape = self.seg.shape 363 self.imgshape2D = self.seg.shape[1:3] 364 self.nframes = self.seg.shape[0] 365 ## if the segmentation is a junction file, transform it to a label image 366 if ut.is_binary(self.seg): 367 self.junctions_to_label() 368 self.tracked = 0 369 else: 370 self.has_been_tracked() 371 self.prepare_labels() 372 373 ## define a reference size of the movie to scale default parameters 374 self.reference_size = np.max(self.imgshape2D) 375 self.epi_metadata["Reloading"] = True ## has been formatted to EpiCure format 376 377 # display the segmentation file movie 378 if self.viewer is not None: 379 if "Movie" in self.viewer.layers: 380 scale = self.viewer.layers["Movie"].scale 381 else: 382 scale = (1,1,1) 383 self.seglayer = self.viewer.add_labels(self.seg, name="Segmentation", blending="additive", opacity=0.5, scale=scale) 384 self.viewer.dims.set_point(0, 0) 385 self.seglayer.brush_size = 4 ## default label pencil drawing size 386 if self.verbose > 0: 387 ut.show_duration(start_time, header="Segmentation loaded in ") 388 389 390 def load_tracks(self, progress_bar): 391 """From the segmentation, get all the metadata""" 392 tracked = "tracked" 393 self.tracking.init_tracks() 394 if self.tracked == 0: 395 tracked = "untracked" 396 else: 397 if self.graph is not None: 398 self.tracking.set_graph(self.graph) 399 if self.forbid_gaps: 400 progress_bar.set_description("check and fix track gaps") 401 self.handle_gaps(track_list=None, verbose=1) 402 ut.show_info("" + str(len(self.tracking.get_track_list())) + " " + tracked + " cells loaded") 403 404 def has_been_tracked(self): 405 """Look if has been tracked already (some labels are in several frames)""" 406 nb = 0 407 for frame in range(self.seg.shape[0]): 408 if frame > 0: 409 inter = np.intersect1d(np.unique(self.seg[frame - 1]), np.unique(self.seg[frame])) 410 if len(inter) > 1: 411 self.tracked = 1 412 return 413 self.tracked = 0 414 return 415 416 def suggest_segfile(self, outdir): 417 """Check if a segmentation file from EpiCure already exists""" 418 if (self.epi_metadata["SegmentationFile"] != "") and ut.found_segfile(self.epi_metadata["SegmentationFile"]): 419 return self.epi_metadata["SegmentationFile"] 420 imgname, imgdir, out = ut.extract_names(self.epi_metadata["MovieFile"], outdir, mkdir=False) 421 return ut.suggest_segfile(out, imgname) 422 423 def outname(self): 424 return os.path.join(self.outdir, self.imgname) 425 426 def set_names(self, outdir): 427 """Extract default names from imgpath""" 428 self.imgname, self.imgdir, self.outdir = ut.extract_names(self.epi_metadata["MovieFile"], outdir, mkdir=True) 429 430 def go_epicure(self, outdir="epics", segmentation_input=None): 431 """Initialize everything and start the main widget""" 432 self.set_names(outdir) 433 if segmentation_input is None: 434 segmentation_input = {} 435 segmentation_input["File"] = self.suggest_segfile(outdir) 436 self.viewer.window._status_bar._toggle_activity_dock(True) 437 progress_bar = progress(total=5) 438 progress_bar.set_description("Reading segmented image") 439 ## load the segmentation 440 self.load_segmentation(segmentation_input) 441 if isinstance(segmentation_input, dict): 442 self.epi_metadata["SegmentationFile"] = segmentation_input["File"] 443 else: 444 self.epi_metadata["SegmentationFile"] = segmentation_input 445 progress_bar.update(1) 446 ut.set_active_layer(self.viewer, "Segmentation") 447 448 ## setup the main interface and shortcuts 449 start_time = ut.start_time() 450 progress_bar.set_description("Active EpiCure shortcuts") 451 self.key_bindings() 452 progress_bar.update(2) 453 progress_bar.set_description("Prepare widget") 454 self.main_widget() 455 progress_bar.update(3) 456 progress_bar.set_description("Load tracks") 457 self.load_tracks(progress_bar) 458 progress_bar.update(4) 459 460 ## load graph if it exists 461 epiname = os.path.join(self.outdir, self.imgname + "_epidata.pkl") 462 if os.path.exists(epiname): 463 progress_bar.set_description("Load EpiCure informations") 464 self.load_epicure_data(epiname) 465 if self.verbose > 0: 466 ut.show_duration(start_time, header="Tracks and graph loaded in ") 467 progress_bar.update(5) 468 self.apply_settings() 469 progress_bar.close() 470 self.viewer.window._status_bar._toggle_activity_dock(False) 471 472 ###### Settings (preferences) save and load 473 def apply_settings(self): 474 """Apply all default or prefered settings""" 475 for sety, val in self.settings.items(): 476 if sety == "Display": 477 self.display.apply_settings(val) 478 if "Show help" in val: 479 index = int(val["Show help"]) 480 self.switchOverlayText(index) 481 if "Contour" in val: 482 contour = int(val["Contour"]) 483 self.seglayer.contour = contour 484 self.seglayer.refresh() 485 if "Colors" in val: 486 color = val["Colors"]["button"] 487 check_color = val["Colors"]["checkbox"] 488 line_edit_color = val["Colors"]["line edit"] 489 group_color = val["Colors"]["group"] 490 self.main_gui.setStyleSheet( 491 "QPushButton {background-color: " 492 + color 493 + "} QCheckBox::indicator {background-color: " 494 + check_color 495 + "} QLineEdit {background-color: " 496 + line_edit_color 497 + "} QGroupBox {color: grey; background-color: " 498 + group_color 499 + "} " 500 ) 501 self.display_colors = val["Colors"] 502 if sety == "events": 503 self.inspecting.apply_settings(val) 504 if sety == "Output": 505 self.outputing.apply_settings(val) 506 if sety == "Track": 507 self.tracking.apply_settings(val) 508 if sety == "Edit": 509 self.editing.apply_settings(val) 510 # case _: 511 # continue 512 ## match is not compatible with python 3.9 513 514 def update_settings(self): 515 """Returns all the prefered settings""" 516 disp = self.settings 517 ## load display current settings (layers visibility) 518 disp["Display"] = self.display.get_current_settings() 519 disp["Display"]["Show help"] = self.help_index 520 disp["Display"]["Contour"] = self.seglayer.contour 521 ## load suspect current settings 522 disp["events"] = self.inspecting.get_current_settings() 523 ## get outputs current settings 524 disp["Output"] = self.outputing.get_current_settings() 525 disp["Track"] = self.tracking.get_current_settings() 526 disp["Edit"] = self.editing.get_current_settings() 527 528 #### Main widget that contains the tabs of the sub widgets 529 530 def main_widget(self): 531 """Open the main widget interface""" 532 self.main_gui = QWidget() 533 534 layout = QVBoxLayout() 535 tabs = QTabWidget() 536 tabs.setObjectName("main") 537 layout.addWidget(tabs) 538 self.main_gui.setLayout(layout) 539 540 self.editing = Editing(self.viewer, self) 541 tabs.addTab(self.editing, "Edit") 542 self.inspecting = Inspecting(self.viewer, self) 543 tabs.addTab(self.inspecting, "Inspect") 544 self.tracking = Tracking(self.viewer, self) 545 tabs.addTab(self.tracking, "Track") 546 self.outputing = Outputing(self.viewer, self) 547 tabs.addTab(self.outputing, "Output") 548 self.display = Displaying(self.viewer, self) 549 tabs.addTab(self.display, "Display") 550 self.main_gui.setStyleSheet("QPushButton {background-color: rgb(40, 60, 75)} QCheckBox::indicator {background-color: rgb(40,52,65)}") 551 552 self.viewer.window.add_dock_widget(self.main_gui, name="Main") 553 554 def key_bindings(self): 555 """Activate shortcuts""" 556 self.text = "-------------- ShortCuts -------------- \n " 557 self.text += "!! Shortcuts work if Segmentation layer is active !! \n" 558 # for sctype, scvals in self.shortcuts.items(): 559 self.text += "\n---" + "General" + " options---\n" 560 sg = self.shortcuts["General"] 561 self.text += ut.print_shortcuts(sg) 562 self.text = self.text + "\n" 563 564 if self.verbose > 0: 565 print("Activating key shortcuts on segmentation layer") 566 print("Press <" + str(sg["show help"]["key"]) + "> to show/hide the main shortcuts") 567 print("Press <" + str(sg["show all"]["key"]) + "> to show ALL shortcuts") 568 ut.setOverlayText(self.viewer, self.text, size=12) 569 570 @self.seglayer.bind_key(sg["show help"]["key"], overwrite=True) 571 def switch_shortcuts(seglayer): 572 # index = (self.help_index+1)%(len(self.overtext.keys())+1) 573 # self.switchOverlayText(index) 574 index = (self.help_index + 1) % 2 575 self.switchOverlayText(index) 576 577 @self.seglayer.bind_key(sg["show all"]["key"], overwrite=True) 578 def list_all_shortcuts(seglayer): 579 self.switchOverlayText(0) ## hide display message in main window 580 text = "**************** EPICURE *********************** \n" 581 text += "\n" 582 text += self.text 583 text += "\n" 584 text += ut.napari_shortcuts() 585 for key, val in self.overtext.items(): 586 text += "\n" 587 text += val 588 self.update_text_window(text) 589 590 @self.seglayer.bind_key(sg["save segmentation"]["key"], overwrite=True) 591 def save_seglayer(seglayer): 592 self.save_epicures() 593 594 @self.viewer.bind_key(sg["save movie"]["key"], overwrite=True) 595 def save_movie(seglayer): 596 endname = "_frames.tif" 597 outname = os.path.join(self.outdir, self.imgname + endname) 598 self.save_movie(outname) 599 600 ########### Texts 601 602 def switchOverlayText(self, index): 603 """Switch overlay display text to index""" 604 self.help_index = index 605 if index == 0: 606 ut.showOverlayText(self.viewer, vis=False) 607 return 608 else: 609 ut.showOverlayText(self.viewer, vis=True) 610 # self.setCurrentOverlayText() 611 self.setGeneralOverlayText() 612 613 def init_text_window(self): 614 """Creates and opens a pop-up window with shortcut list""" 615 self.blabla = ut.create_text_window("EpiCure shortcuts") 616 617 def update_text_window(self, message): 618 """Update message in separate window""" 619 self.init_text_window() 620 self.blabla.value = message 621 622 def setGeneralOverlayText(self): 623 """set overlay help message to general message""" 624 text = self.text 625 ut.setOverlayText(self.viewer, text, size=12) 626 627 def setCurrentOverlayText(self): 628 """Set overlay help text message to current selected options list""" 629 text = self.text 630 dispkey = list(self.overtext.keys())[self.help_index - 1] 631 text += self.overtext[dispkey] 632 ut.setOverlayText(self.viewer, text, size=12) 633 634 def get_summary(self): 635 """Get a summary of the infos of the movie""" 636 summ = "----------- EpiCure summary ----------- \n" 637 summ += "--- Image infos \n" 638 summ += "Movie name: " + str(self.epi_metadata["MovieFile"]) + "\n" 639 summ += "Movie size (x,y): " + str(self.imgshape2D) + "\n" 640 if self.nframes is not None: 641 summ += "Nb frames: " + str(self.nframes) + "\n" 642 summ += "\n" 643 summ += "--- Segmentation infos \n" 644 summ += "Segmentation file: " + str(self.epi_metadata["SegmentationFile"]) + "\n" 645 summ += "Nb tracks: " + str(len(self.tracking.get_track_list())) + "\n" 646 tracked = "yes" 647 if self.tracked == 0: 648 tracked = "no" 649 summ += "Tracked: " + tracked + "\n" 650 nb_labels, mean_duration, mean_area = ut.summary_labels(self.seg) 651 summ += "Nb cells: " + str(nb_labels) + "\n" 652 summ += "Average track lengths: " + str(mean_duration) + " frames\n" 653 summ += "Average cell area: " + str(mean_area) + " pixels^2\n" 654 summ += "Nb suspect events: " + str(self.inspecting.nb_events(only_suspect=True)) + "\n" 655 summ += "Nb divisions: " + str(self.nb_divisions()) + "\n" 656 summ += "Nb extrusions: " + str(self.inspecting.nb_type("extrusion")) + "\n" 657 summ += "\n" 658 summ += "--- Parameter infos \n" 659 summ += "Junction thickness: " + str(self.thickness) + "\n" 660 return summ 661 662 def nb_divisions(self): 663 """ Return the number of divisions """ 664 return self.inspecting.nb_type("division") 665 666 def set_contour(self, width): 667 """ 668 Set the width of the contour of the cells to display the segmentation 669 670 :param: width: width of the contours of the segmentation (napari contour parameter). If 0 the cell will be filled by its label 671 """ 672 self.seglayer.contour = width 673 674 ############ Layers 675 676 def check_layers(self): 677 """Check that the necessary layers are present""" 678 if self.editing.shapelayer_name not in self.viewer.layers: 679 if self.verbose > 0: 680 print("Reput shape layer") 681 self.editing.create_shapelayer() 682 if self.inspecting.eventlayer_name not in self.viewer.layers: 683 if self.verbose > 0: 684 print("Reput event layer") 685 self.inspecting.create_eventlayer() 686 if "Movie" not in self.viewer.layers: 687 if self.verbose > 0: 688 print("Reput movie layer") 689 mview = self.viewer.add_image(self.img, name="Movie", blending="additive", colormap="gray", scale=[1, self.epi_metadata["ScaleXY"], self.epi_metadata["ScaleXY"]]) 690 # mview.reset_contrast_limits() 691 mview.contrast_limits = self.quantiles() 692 mview.gamma = 0.95 693 if "Segmentation" not in self.viewer.layers: 694 if self.verbose > 0: 695 print("Reput segmentation") 696 self.seglayer = self.viewer.add_labels(self.seg, name="Segmentation", blending="additive", opacity=0.5, scale=self.viewer.layers["Movie"].scale) 697 698 self.finish_update() 699 700 def finish_update(self, contour=None): 701 """ 702 After doing modifications on some layer(s), select back the main layer Segmentation as active (important for shortcut bindings) and refresh it 703 """ 704 if contour is not None: 705 self.seglayer.contour = contour 706 ut.set_active_layer(self.viewer, "Segmentation") 707 self.seglayer.refresh() 708 duplayers = ["PrevSegmentation"] 709 for dlay in duplayers: 710 if dlay in self.viewer.layers: 711 (self.viewer.layers[dlay]).refresh() 712 713 def read_epicure_metadata(self): 714 """Load saved infos from file""" 715 epiname = self.outname() + "_epidata.pkl" 716 if os.path.exists(epiname): 717 infile = open(epiname, "rb") 718 try: 719 epidata = pickle.load(infile) 720 if "EpiMetaData" in epidata.keys(): 721 for key, vals in epidata["EpiMetaData"].items(): 722 self.epi_metadata[key] = vals 723 infile.close() 724 except: 725 ut.show_warning("Could not read EpiCure metadata file " + epiname) 726 727 def save_epicures(self, imtype="float32"): 728 """ 729 Save all the current data: the segmentation, the metadata (metadata of the image, last parameters used), the events and some display settings. 730 """ 731 outname = os.path.join(self.outdir, self.imgname + "_labels.tif") 732 ut.writeTif(self.seg, outname, self.epi_metadata["ScaleXY"], imtype, what="Segmentation") 733 epiname = os.path.join(self.outdir, self.imgname + "_epidata.pkl") 734 outfile = open(epiname, "wb") 735 self.epi_metadata["MainChannel"] = self.main_channel 736 epidata = {} 737 epidata["EpiMetaData"] = self.epi_metadata 738 if self.groups is not None: 739 epidata["Group"] = self.groups 740 if self.tracking.graph is not None: 741 epidata["Graph"] = self.tracking.graph 742 if self.inspecting is not None and self.inspecting.events is not None: 743 epidata["Events"] = {} 744 if self.inspecting.events.data is not None: 745 epidata["Events"]["Points"] = self.inspecting.events.data 746 epidata["Events"]["Props"] = self.inspecting.events.properties 747 epidata["Events"]["Types"] = self.inspecting.event_types 748 # epidata["Events"]["Symbols"] = self.inspecting.events.symbol 749 # epidata["Events"]["Colors"] = self.inspecting.events.face_color 750 if "Movie" in self.viewer.layers: 751 ## to keep movie layer display settings for this file 752 epidata["Display"] = {} 753 epidata["Display"]["MovieContrast"] = self.viewer.layers["Movie"].contrast_limits 754 pickle.dump(epidata, outfile) 755 outfile.close() 756 757 def read_group_data(self, groups): 758 """Read the group EpiCure data from opened file""" 759 if self.verbose > 0: 760 print("Loaded cell groups info: " + str(list(groups.keys()))) 761 if self.verbose > 2: 762 print("Cell groups: " + str(groups)) 763 return groups 764 765 def read_graph_data(self, infile): 766 """ 767 Read the graph EpiCure data from opened pickle file 768 769 :param: infile: instance of pickle file being read. This will read the next part of the pickle file and load it in the track graph. 770 """ 771 try: 772 graph = pickle.load(infile) 773 if self.verbose > 0: 774 print("Graph (lineage) loaded") 775 return graph 776 except: 777 if self.verbose > 1: 778 print("No graph infos found") 779 return None 780 781 def read_events_data(self, infile): 782 """Read info of EpiCure events (suspects, divisions) from opened file""" 783 try: 784 events_pts = pickle.load(infile) 785 if events_pts is not None: 786 events_props = pickle.load(infile) 787 events_type = pickle.load(infile) 788 try: 789 symbols = pickle.load(infile) 790 colors = pickle.load(infile) 791 except: 792 if self.verbose > 1: 793 print("No events display info found") 794 symbols = None 795 colors = None 796 return events_pts, events_props, events_type 797 else: 798 return None, None, None 799 except: 800 if self.verbose > 1: 801 print("events info not complete") 802 return None, None, None 803 804 def load_epicure_data(self, epiname): 805 """Load saved infos from file""" 806 infile = open(epiname, "rb") 807 try: 808 if ut.is_windows(): 809 import pathlib 810 pathlib.PosixPath = pathlib.WindowsPath 811 #epidata = pickle.load( infile, encoding="utf8" ) 812 epidata = pickle.load( infile ) 813 #print(epidata) 814 if "EpiMetaData" in epidata.keys(): 815 # version of epicure file after Epicure 0.2.0 816 self.read_epidata(epidata) 817 infile.close() 818 else: 819 # version anterior of Epicure 0.2.0 820 self.load_epicure_data_old(epidata, infile) 821 except Exception as e: 822 if self.verbose > 1: 823 print(f" {type(e)} {e} - Could not read EpiCure data file {epiname}") 824 else: 825 ut.show_warning(f"Could not read EpiCure data file {epiname}") 826 print(f" {type(e)} {e} - Could not read EpiCure data file {epiname}") 827 828 def read_epidata(self, epidata): 829 """Read the dict of saved state and initialize all instances with it""" 830 for key, vals in epidata.items(): 831 if key == "EpiMetaData": 832 ## image data is read on the previous step 833 continue 834 if key == "Group": 835 ## Load groups information 836 self.groups = self.read_group_data(vals) 837 self.update_group_lists() 838 if key == "Graph": 839 ## Load graph (lineage) informations 840 self.tracking.graph = vals 841 if self.tracking.graph is not None: 842 self.tracking.tracklayer.refresh() 843 if self.verbose > 2: 844 print(f"Loaded track graph: {self.tracking.graph}") 845 if key == "Events": 846 ## Load events information 847 if "Points" in vals.keys(): 848 pts = vals["Points"] 849 if "Props" in vals.keys(): 850 props = vals["Props"] 851 if "Types" in vals.keys(): 852 event_types = vals["Types"] 853 # if "Symbols" in vals.keys(): 854 # symbols = vals["Symbols"] 855 # if "Colors" in vals.keys(): 856 # colors = vals["Colors"] 857 if pts is not None: 858 if len(pts) > 0: 859 self.inspecting.load_events(pts, props, event_types) 860 if len(pts) > 0 and self.verbose > 0: 861 print("events loaded") 862 ut.show_info("Loaded " + str(len(pts)) + " events") 863 if key == "Display": 864 if vals is not None: 865 ## load display setting 866 if "MovieContrast" in vals.keys(): 867 self.viewer.layers["Movie"].contrast_limits = vals["MovieContrast"] 868 869 def load_epicure_data_old(self, groups, infile): 870 """Load saved infos from file""" 871 ## Load groups information 872 self.groups = self.read_group_data(groups) 873 for group in self.groups.keys(): 874 self.editing.update_group_list(group) 875 self.outputing.update_selection_list() 876 ## Load graph (lineage) informations 877 self.tracking.graph = self.read_graph_data(infile) 878 if self.tracking.graph is not None: 879 self.tracking.tracklayer.refresh() 880 ## Load events information 881 pts, props, event_types = self.read_events_data(infile) 882 if pts is not None: 883 if len(pts) > 0: 884 self.inspecting.load_events(pts, props, event_types) 885 if len(pts) > 0 and self.verbose > 0: 886 print("events loaded") 887 ut.show_info("Loaded " + str(len(pts)) + " events") 888 infile.close() 889 890 def save_movie(self, outname): 891 """Save movie with current display parameters, except zoom""" 892 save_view = self.viewer.camera.copy() 893 save_frame = ut.current_frame(self.viewer) 894 ## place the view to see the whole image 895 self.viewer.reset_view() 896 # self.viewer.camera.zoom = 1 897 sizex = (self.imgshape2D[0] * self.viewer.camera.zoom) / 2 898 sizey = (self.imgshape2D[1] * self.viewer.camera.zoom) / 2 899 if os.path.exists(outname): 900 os.remove(outname) 901 902 ## take a screenshot of each frame 903 for frame in range(self.nframes): 904 self.viewer.dims.set_point(0, frame) 905 shot = self.viewer.window.screenshot(canvas_only=True, flash=False) 906 ## remove border: movie is at the center 907 centx = int(shot.shape[0] / 2) + 1 908 centy = int(shot.shape[1] / 2) + 1 909 shot = shot[ 910 int(centx - sizex) : int(centx + sizex), 911 int(centy - sizey) : int(centy + sizey), 912 ] 913 ut.appendToTif(shot, outname) 914 self.viewer.camera.update(save_view) 915 if save_frame is not None: 916 self.viewer.dims.set_point(0, save_frame) 917 ut.show_info("Movie " + outname + " saved") 918 919 def reset_data(self): 920 """Reset EpiCure data (group, suspect, graph)""" 921 self.inspecting.reset_all_events() 922 self.reset_groups() 923 self.tracking.graph = None 924 925 def junctions_to_label(self): 926 """convert epyseg/skeleton result (junctions) to labels map""" 927 ## ensure that skeleton is thin enough 928 for z in range(self.seg.shape[0]): 929 self.skel_one_frame(z) 930 self.seg = ut.reset_labels(self.seg, closing=True) 931 932 def skel_one_frame(self, z): 933 """From segmentation of junctions of one frame, get it as a correct skeleton""" 934 skel = skeletonize(self.seg[z] / np.max(self.seg[z])) 935 skel = ut.copy_border(skel, self.seg[z]) 936 self.seg[z] = np.invert(skel) 937 938 def reset_labels(self): 939 """Reset all labels, ensure unicity""" 940 if self.epi_metadata["EpithelialCells"]: 941 ### packed (contiguous cells), ensure that they are separated by one pixel only 942 skel = self.get_skeleton() 943 skel = np.uint32(skel) 944 self.seg = skel 945 self.seglayer.data = skel 946 self.junctions_to_label() 947 self.seglayer.data = self.seg 948 else: 949 self.get_cells() 950 951 def check_extrusions_sanity(self): 952 """Check that extrusions seem to be correct (last of tracks )""" 953 extrusions = self.inspecting.get_events_from_type("extrusion") 954 nrem = 0 955 if (extrusions is not None) and (extrusions != []): 956 for extr_id in extrusions: 957 pos, label = self.inspecting.get_event_infos(extr_id) 958 last_frame = self.tracking.get_last_frame(label) 959 if pos[0] != last_frame: 960 if self.verbose > 1: 961 print("Extrusion " + str(extr_id) + " at frame " + str(pos[0]) + " not at the end of track " + str(label)) 962 print("Removing it") 963 self.inspecting.remove_one_event(extr_id) 964 nrem = nrem + 1 965 print("Removed " + str(nrem) + " extrusions that dit not correspond to the end of tracks") 966 967 def prepare_labels(self): 968 """Process the labels to be in a correct Epicurable format""" 969 if self.epi_metadata["EpithelialCells"]: 970 if self.epi_metadata["Reloading"]: 971 ## if opening an already EpiCured movie, assume it's in correct format 972 return 973 ### packed (contiguous cells), ensure that they are separated by one pixel only 974 self.thin_boundaries() 975 else: 976 self.get_cells() 977 978 def get_cells(self): 979 """Non jointive cells: check label unicity""" 980 for frame in self.seg: 981 if ut.non_unique_labels(frame): 982 self.seg = ut.reset_labels(self.seg, closing=True) 983 return 984 985 def thin_boundaries(self): 986 """ " Assure that all boundaries are only 1 pixel thick""" 987 if self.process_parallel: 988 self.seg = Parallel(n_jobs=self.nparallel)(delayed(ut.thin_seg_one_frame)(zframe) for zframe in self.seg) 989 self.seg = np.array(self.seg) 990 else: 991 for z in range(self.seg.shape[0]): 992 self.seg[z] = ut.thin_seg_one_frame(self.seg[z]) 993 994 def add_skeleton(self): 995 """add a layer containing the skeleton movie of the segmentation""" 996 # display the segmentation file movie 997 if self.viewer is not None: 998 skel = np.zeros(self.seg.shape, dtype="uint8") 999 skel[self.seg == 0] = 1 1000 skel = self.get_skeleton(viewer=self.viewer) 1001 ut.remove_layer(self.viewer, "Skeleton") 1002 skellayer = self.viewer.add_image(skel, name="Skeleton", blending="additive", opacity=1, scale=self.viewer.layers["Movie"].scale) 1003 skellayer.reset_contrast_limits() 1004 skellayer.contrast_limits = (0, 1) 1005 1006 def get_skeleton(self, viewer=None): 1007 """convert labels movie to skeleton (thin boundaries)""" 1008 if self.seg is None: 1009 return None 1010 parallel = 0 1011 if self.process_parallel: 1012 parallel = self.nparallel 1013 return ut.get_skeleton(self.seg, viewer=viewer, verbose=self.verbose, parallel=parallel) 1014 1015 ############ Label functions 1016 1017 def get_free_labels(self, nlab): 1018 """Get the nlab smallest unused labels""" 1019 used = set(self.tracking.get_track_list()) 1020 return ut.get_free_labels(used, nlab) 1021 1022 def get_free_label(self): 1023 """Return the first free label""" 1024 return self.get_free_labels(1)[0] 1025 1026 def has_label(self, label): 1027 """Check if label is present in the tracks""" 1028 return self.tracking.has_track(label) 1029 1030 def has_labels(self, labels): 1031 """Check if labels are present in the tracks""" 1032 return self.tracking.has_tracks(labels) 1033 1034 def nlabels(self): 1035 """Number of unique tracks""" 1036 return self.tracking.nb_tracks() 1037 1038 def get_labels(self): 1039 """Return list of labels in tracks""" 1040 return list(self.tracking.get_track_list()) 1041 1042 ########## Edit tracks 1043 def delete_tracks(self, tracks): 1044 """Remove all the tracks from the Track layer""" 1045 self.tracking.remove_tracks(tracks) 1046 1047 def delete_track(self, label, frame=None): 1048 """Remove (part of) the track""" 1049 if frame is None: 1050 self.tracking.remove_track(label) 1051 else: 1052 self.tracking.remove_one_frame(label, frame, handle_gaps=self.forbid_gaps) 1053 1054 def update_centroid(self, label, frame): 1055 """Track label has been change at given frame""" 1056 if label not in self.tracking.has_track(label): 1057 if self.verbose > 1: 1058 print("Track " + str(label) + " not found") 1059 return 1060 self.tracking.update_centroid(label, frame) 1061 1062 ########## Edit label 1063 def get_label_indexes(self, label, start_frame=0): 1064 """Returns the indexes where label is present in segmentation, starting from start_frame""" 1065 indmodif = [] 1066 if self.verbose > 2: 1067 start_time = ut.start_time() 1068 pos = self.tracking.get_track_column(track_id=label, column="fullpos") 1069 pos = pos[pos[:, 0] >= start_frame] 1070 ## if nothing in pos, pb with track data 1071 if pos is None or len(pos) == 0: 1072 ut.show_warning("Something wrong in the track data. Resetting track data (can take time)") 1073 self.tracking.reset_tracks() 1074 self.get_label_indexes(label, start_frame) 1075 1076 indmodif = np.argwhere(self.seg[pos[:, 0]] == label) 1077 indmodif = ut.shiftFrames(indmodif, pos[:, 0]) 1078 if self.verbose > 2: 1079 ut.show_duration(start_time, header="Label indexes found in ") 1080 return indmodif 1081 1082 def replace_label(self, label, new_label, start_frame=0): 1083 """Replace label with new_label from start_frame - Relabelling only""" 1084 indmodif = self.get_label_indexes(label, start_frame) 1085 new_labels = [new_label] * len(indmodif) 1086 self.change_labels(indmodif, new_labels, replacing=True) 1087 1088 def change_labels_frommerge(self, indmodif, new_labels, remove_labels): 1089 """Change the value at pixels indmodif to new_labels and update tracks/graph. Full remove of the two merged labels""" 1090 if len(indmodif) > 0: 1091 ## get effectively changed labels 1092 indmodif, new_labels, _ = ut.setNewLabel(self.seglayer, indmodif, new_labels, add_frame=None, return_old=False) 1093 if len(new_labels) > 0: 1094 self.update_added_labels(indmodif, new_labels) 1095 self.update_removed_labels(indmodif, remove_labels) 1096 self.seglayer.refresh() 1097 1098 def change_labels(self, indmodif, new_labels, replacing=False): 1099 """Change the value at pixels indmodif to new_labels and update tracks/graph 1100 1101 Assume that only label at current frame can have its shape modified. Other changed label is only relabelling at frames > current frame (child propagation) 1102 """ 1103 if len(indmodif) > 0: 1104 ## get effectively changed labels 1105 indmodif, new_labels, old_labels = ut.setNewLabel(self.seglayer, indmodif, new_labels, add_frame=None) 1106 if len(new_labels) > 0: 1107 if replacing: 1108 self.update_replaced_labels(indmodif, new_labels, old_labels) 1109 else: 1110 ## the only label to change are the current frame (smaller one), the other are only relabelling (propagation) 1111 cur_frame = np.min(indmodif[0]) 1112 to_reshape = indmodif[0] == cur_frame 1113 self.update_changed_labels((indmodif[0][to_reshape], indmodif[1][to_reshape], indmodif[2][to_reshape]), new_labels[to_reshape], old_labels[to_reshape]) 1114 to_relab = np.invert(to_reshape) 1115 self.update_replaced_labels((indmodif[0][to_relab], indmodif[1][to_relab], indmodif[2][to_relab]), new_labels[to_relab], old_labels[to_relab]) 1116 self.seglayer.refresh() 1117 1118 def get_mask(self, label, start=None, end=None): 1119 """Get mask of label from frame start to frame end""" 1120 if (start is None) or (end is None): 1121 start, end = self.tracking.get_extreme_frames(label) 1122 crop = self.seg[start : (end + 1)] 1123 mask = np.isin(crop, [label]) * 1 1124 return mask 1125 1126 def get_label_movie(self, label, extend=1.25): 1127 """Get movie centered on label""" 1128 start, end = self.tracking.get_extreme_frames(label) 1129 mask = self.get_mask(label, start, end) 1130 boxes = [] 1131 centers = [] 1132 max_box = 0 1133 for frame in mask: 1134 props = regionprops(frame) 1135 bbox = props[0].bbox 1136 boxes.append(bbox) 1137 centers.append(props[0].centroid) 1138 for i in range(2): 1139 max_box = max(max_box, bbox[i + 2] - bbox[i]) 1140 1141 box_size = int(max_box * extend) 1142 movie = np.zeros((end - start + 1, box_size, box_size)) 1143 for i, frame in enumerate(range(start, end + 1)): 1144 xmin = int(centers[i][0] - box_size / 2) 1145 xminshift = 0 1146 if xmin < 0: 1147 xminshift = -xmin 1148 xmin = 0 1149 xmax = xmin + box_size - xminshift 1150 xmaxshift = box_size 1151 if xmax > self.imgshape2D[0]: 1152 xmaxshift = self.imgshape2D[0] - xmax 1153 xmax = self.imgshape2D[0] 1154 1155 ymin = int(centers[i][1] - max_box / 2) 1156 yminshift = 0 1157 if ymin < 0: 1158 yminshift = -ymin 1159 ymin = 0 1160 ymax = ymin + box_size - yminshift 1161 ymaxshift = box_size 1162 if ymax > self.imgshape2D[1]: 1163 ymaxshift = self.imgshape2D[1] - ymax 1164 ymax = self.imgshape2D[1] 1165 1166 movie[i, xminshift:xmaxshift, yminshift:ymaxshift] = self.img[frame, xmin:xmax, ymin:ymax] 1167 return movie 1168 1169 ### Check individual cell features 1170 def cell_radius(self, label, frame): 1171 """Approximate the cell radius at given frame""" 1172 area = np.sum(self.seg[frame] == label) 1173 radius = math.sqrt(area / math.pi) 1174 return radius 1175 1176 def cell_area(self, label, frame): 1177 """Approximate the cell radius at given frame""" 1178 area = np.sum(self.seg[frame] == label) 1179 return area 1180 1181 def cell_on_border(self, label, frame): 1182 """Check if a given cell is on border of the image""" 1183 bbox = ut.getBBox2D(self.seg[frame], label) 1184 out = ut.outerBBox2D(bbox, self.imgshape2D, margin=3) 1185 return out 1186 1187 ###### Synchronize tracks whith labels changed 1188 def add_label(self, labels, frame=None): 1189 """Add a label to the tracks""" 1190 if frame is not None: 1191 if np.isscalar(labels): 1192 labels = [labels] 1193 self.tracking.add_one_frame(labels, frame, refresh=True) 1194 else: 1195 if self.verbose > 1: 1196 print("TODO add label no frame") 1197 1198 def add_one_label_to_track(self, label): 1199 """Add the track data of a given label if missing""" 1200 iframe = 0 1201 while (iframe < self.nframes) and (label not in self.seg[iframe]): 1202 iframe = iframe + 1 1203 while (iframe < self.nframes) and (label in self.seg[iframe]): 1204 self.tracking.add_one_frame([label], iframe) 1205 iframe = iframe + 1 1206 1207 def update_label(self, label, frame): 1208 """Update the given label at given frame""" 1209 self.tracking.update_track_on_frame([label], frame) 1210 1211 def update_changed_labels(self, indmodif, new_labels, old_labels, full=False): 1212 """Check what had been modified, and update tracks from it, looking frame by frame""" 1213 ## check all the old_labels if still present or not 1214 if self.verbose > 1: 1215 start_time = time.time() 1216 frames = np.unique(indmodif[0]) 1217 all_deleted = [] 1218 debug_verb = self.verbose > 2 1219 if debug_verb: 1220 print("Updating labels in frames " + str(frames)) 1221 for frame in frames: 1222 keep = indmodif[0] == frame 1223 ## check old labels if totally removed or not 1224 deleted = np.setdiff1d(old_labels[keep], self.seg[frame]) 1225 left = np.setdiff1d(old_labels[keep], deleted) 1226 if deleted.shape[0] > 0: 1227 self.tracking.remove_one_frame(deleted, frame, handle_gaps=False, refresh=False) 1228 if self.forbid_gaps: 1229 all_deleted = all_deleted + list(set(deleted) - set(all_deleted)) 1230 if left.shape[0] > 0: 1231 self.tracking.update_track_on_frame(left, frame) 1232 ## now check new labels 1233 nlabels = np.unique(new_labels[keep]) 1234 if nlabels.shape[0] > 0: 1235 self.tracking.update_track_on_frame(nlabels, frame) 1236 if debug_verb: 1237 print("Labels deleted at frame " + str(frame) + " " + str(deleted) + " or added " + str(nlabels)) 1238 1239 def update_added_labels(self, indmodif, new_labels): 1240 """Update tracks of labels that have been fully added""" 1241 if self.verbose > 1: 1242 start_time = time.time() 1243 1244 ## Deleted labels 1245 frames = np.unique(indmodif[0]) 1246 self.tracking.add_tracks_fromindices(indmodif, new_labels) 1247 if self.forbid_gaps: 1248 ## Check if some gaps has been created in tracks (remove middle(s) frame(s)) 1249 added = list(set(new_labels)) 1250 if len(added) > 0: 1251 self.handle_gaps(added, verbose=0) 1252 1253 if self.verbose > 1: 1254 ut.show_duration(start_time, "updated added tracks in ") 1255 1256 def update_removed_labels(self, indmodif, old_labels): 1257 """Update tracks of labels that have been fully removed""" 1258 if self.verbose > 1: 1259 start_time = time.time() 1260 1261 ## Deleted labels 1262 frames = np.unique(indmodif[0]) 1263 self.tracking.remove_on_frames(np.unique(old_labels), frames) 1264 if self.forbid_gaps: 1265 ## Check if some gaps has been created in tracks (remove middle(s) frame(s)) 1266 deleted = list(set(old_labels)) 1267 if len(deleted) > 0: 1268 self.handle_gaps(deleted, verbose=0) 1269 1270 if self.verbose > 1: 1271 ut.show_duration(start_time, "updated removed tracks in ") 1272 1273 def update_replaced_labels(self, indmodif, new_labels, old_labels): 1274 """Old_labels were fully replaced by new_labels on some frames, update tracks from it""" 1275 if self.verbose > 1: 1276 start_time = time.time() 1277 1278 ## Deleted labels 1279 frames = np.unique(indmodif[0]) 1280 self.tracking.replace_on_frames(np.unique(old_labels), np.unique(new_labels), frames) 1281 if self.forbid_gaps: 1282 ## Check if some gaps has been created in tracks (remove middle(s) frame(s)) 1283 deleted = list(set(old_labels)) 1284 if len(deleted) > 0: 1285 self.handle_gaps(deleted, verbose=0) 1286 1287 if self.verbose > 1: 1288 ut.show_duration(start_time, "updated replaced tracks in ") 1289 1290 def handle_gaps(self, track_list, verbose=None): 1291 """Check and fix gaps in tracks""" 1292 if verbose is None: 1293 verbose = self.verbose 1294 gaped = self.tracking.check_gap(track_list, verbose=verbose) 1295 if len(gaped) > 0: 1296 if self.verbose > 0: 1297 print("Relabelling tracks with gaps") 1298 self.fix_gaps(gaped) 1299 1300 def fix_gaps(self, gaps): 1301 """Fix when some gaps has been created in tracks""" 1302 for gap in gaps: 1303 gap_frames = self.tracking.gap_frames(gap) 1304 cur_gap = gap 1305 for gapy in gap_frames: 1306 new_value = self.get_free_label() 1307 self.replace_label(cur_gap, new_value, gapy) 1308 cur_gap = new_value 1309 1310 def swap_labels(self, lab, olab, frame): 1311 """Exchange two labels""" 1312 self.tracking.swap_frame_id(lab, olab, frame) 1313 1314 def swap_tracks(self, lab, olab, start_frame): 1315 """Exchange two tracks""" 1316 ## split the two labels to unused value 1317 tmp_labels = self.get_free_labels(2) 1318 for i, laby in enumerate([lab, olab]): 1319 self.replace_label(laby, tmp_labels[i], start_frame) 1320 1321 ## replace the two initial labels, in inversed order 1322 self.replace_label(tmp_labels[0], olab, start_frame) 1323 self.replace_label(tmp_labels[1], lab, start_frame) 1324 1325 def split_track(self, label, frame): 1326 """Split a track at given frame""" 1327 new_label = self.get_free_label() 1328 self.replace_label(label, new_label, frame) 1329 if self.verbose > 0: 1330 ut.show_info("Split track " + str(label) + " from frame " + str(frame)) 1331 return new_label 1332 1333 def update_changed_labels_img(self, img_before, img_after, added=True, removed=True): 1334 """Update tracks from changes between the two labelled images""" 1335 if self.verbose > 1: 1336 print("Updating changed labels from images") 1337 indmodif = np.argwhere(img_before != img_after).tolist() 1338 if len(indmodif) <= 0: 1339 return 1340 indmodif = tuple(np.array(indmodif).T) 1341 new_labels = img_after[indmodif] 1342 old_labels = img_before[indmodif] 1343 self.update_changed_labels(indmodif, new_labels, old_labels) 1344 1345 def added_labels_oneframe(self, frame, img_before, img_after): 1346 """Update added tracks between the two labelled images at frame""" 1347 ## Look for added labels 1348 added_labels = np.setdiff1d(img_after, img_before) 1349 self.tracking.add_one_frame(added_labels, frame, refresh=True) 1350 1351 def removed_labels(self, img_before, img_after, frame=None): 1352 """Update removed tracks between the two labelled images""" 1353 ## Look for added labels 1354 deleted_labels = np.setdiff1d(img_before, img_after) 1355 if frame is None: 1356 self.tracking.remove_tracks(deleted_labels) 1357 else: 1358 self.tracking.remove_one_frame(track_id=deleted_labels.tolist(), frame=frame, handle_gaps=self.forbid_gaps) 1359 1360 def remove_label(self, label, force=False): 1361 """Remove a given label if allowed""" 1362 ut.changeLabel(self.seglayer, label, 0) 1363 self.tracking.remove_tracks(label) 1364 self.seglayer.refresh() 1365 1366 def remove_labels(self, labels, force=False): 1367 """Remove all allowed labels""" 1368 inds = [] 1369 for lab in labels: 1370 # if (force) or (not self.locked_label(label)): 1371 inds = inds + ut.getLabelIndexes(self.seglayer.data, lab, None) 1372 ut.setNewLabel(self.seglayer, inds, 0) 1373 self.tracking.remove_tracks(labels) 1374 1375 def keep_labels(self, labels, force=True): 1376 """Remove all other labels that are not in labels""" 1377 inds = [] 1378 toremove = list(set(self.tracking.get_track_list()) - set(labels)) 1379 # for lab in self.tracking.get_track_list(): 1380 # if lab not in labels: 1381 # if (force) or (not self.locked_label(label)): 1382 for lab in toremove: 1383 inds = inds + ut.getLabelIndexes(self.seglayer.data, lab, None) 1384 # toremove.append(lab) 1385 ut.setNewLabel(self.seglayer, inds, 0) 1386 self.tracking.remove_tracks(toremove) 1387 1388 def get_frame_features(self, frame): 1389 """Measure the label properties of given frame""" 1390 return regionprops(self.seg[frame]) 1391 1392 def updates_after_tracking(self): 1393 """When tracking has been done, update events, others""" 1394 self.inspecting.get_divisions() 1395 1396 ####################### 1397 ## Classified cells options 1398 def get_all_groups(self, numeric=False): 1399 """Add all groups info""" 1400 if numeric: 1401 groups = [0] * self.nlabels() 1402 else: 1403 groups = ["None"] * self.nlabels() 1404 for igroup, gr in self.groups.keys(): 1405 indexes = self.tracking.get_track_indexes(self.groups[gr]) 1406 if numeric: 1407 groups[indexes] = igroup + 1 1408 else: 1409 groups[indexes] = gr 1410 return groups 1411 1412 def get_groups(self, labels, numeric=False): 1413 """Add the group info of the given labels (repeated)""" 1414 if numeric: 1415 groups = [0] * len(labels) 1416 else: 1417 groups = ["Ungrouped"] * len(labels) 1418 for lab in np.unique(labels): 1419 gr = self.find_group(lab) 1420 if gr is None: 1421 continue 1422 if numeric: 1423 gr = self.groups.keys().index() + 1 1424 indexes = (np.argwhere(labels == lab)).flatten() 1425 for ind in indexes: 1426 groups[ind] = gr 1427 return groups 1428 1429 def cells_ingroup(self, labels, group): 1430 """Put the cell "label" in group group, add it if new group""" 1431 presents = self.has_labels(labels) 1432 labels = np.array(labels)[presents] 1433 if group not in self.groups.keys(): 1434 self.groups[group] = [] 1435 self.update_group_lists() 1436 ## add only non present label(s) 1437 grlabels = self.groups[group] 1438 self.groups[group] = list(set(grlabels + labels.tolist())) 1439 1440 def group_of_labels(self): 1441 """List the group of each label""" 1442 res = {} 1443 for group, labels in self.groups.items(): 1444 for label in labels: 1445 res[label] = group 1446 return res 1447 1448 def find_group(self, label): 1449 """Find in which group the label is""" 1450 for gr, labs in self.groups.items(): 1451 if label in labs: 1452 return gr 1453 return None 1454 1455 def cell_removegroup(self, label): 1456 """Detach the cell from its group""" 1457 if not self.has_label(label): 1458 if self.verbose > 1: 1459 print("Cell " + str(label) + " missing") 1460 group = self.find_group(label) 1461 if group is not None: 1462 self.groups[group].remove(label) 1463 if len(self.groups[group]) <= 0: 1464 del self.groups[group] 1465 self.update_group_lists() 1466 1467 def update_group_lists(self): 1468 """Update all the lists depending on the group names""" 1469 if self.outputing is not None: 1470 self.outputing.update_selection_list() 1471 if self.editing is not None: 1472 self.editing.update_group_lists() 1473 1474 def reset_group(self, group_name): 1475 """Reset/remove a given group""" 1476 if group_name == "All": 1477 self.reset_groups() 1478 return 1479 if group_name in self.groups.keys(): 1480 del self.groups[group_name] 1481 self.update_group_lists() 1482 1483 def reset_groups(self): 1484 """Remove all group information for all cells""" 1485 self.groups = {} 1486 self.update_group_lists() 1487 1488 def draw_groups(self): 1489 """Draw all the epicells colored by their group""" 1490 grouped = np.zeros(self.seg.shape, np.uint8) 1491 if (self.groups is None) or len(self.groups.keys()) == 0: 1492 return grouped 1493 for group, labels in self.groups.items(): 1494 igroup = self.get_group_index(group) + 1 1495 np.place(grouped, np.isin(self.seg, labels), igroup) 1496 return grouped 1497 1498 def get_group_index(self, group): 1499 """Get the index of group in the list of groups""" 1500 if group in list(self.groups.keys()): 1501 igroup = list(self.groups.keys()).index(group) 1502 return igroup 1503 return -1 1504 1505 ######### ROI 1506 def only_current_roi(self, frame): 1507 """Put 0 everywhere outside the current ROI""" 1508 roi_labels = self.editing.get_labels_inside() 1509 if roi_labels is None: 1510 return None 1511 # remove all other labels that are not in roi_labels 1512 roilab = np.copy(self.seg[frame]) 1513 np.place(roilab, np.isin(roilab, roi_labels, invert=True), 0) 1514 return roilab
31 def __init__(self, viewer=None): 32 """ 33 Initialize the EpiCure viewer instance. 34 35 :param: viewer (napari.Viewer, optional): An existing napari Viewer instance to use. 36 If None, a new Viewer instance will be created with show=False. 37 Defaults to None. 38 """ 39 self.viewer = viewer 40 """ Napari viewer that is used for this session """ 41 if self.viewer is None: 42 self.viewer = napari.Viewer(show=False) 43 self.viewer.title = "Napari - EpiCure" 44 self.reset()
Initialize the EpiCure viewer instance.
Parameters
- viewer (napari.Viewer, optional): An existing napari Viewer instance to use. If None, a new Viewer instance will be created with show=False. Defaults to None.
46 def reset(self): 47 """ Reset all the parameters to the default values """ 48 self.init_epicure_metadata() ## initialize metadata variables (scalings, channels) 49 self.img = None 50 """ data of the raw movie """ 51 self.inspecting = None 52 """ interface for inspection options """ 53 self.others = None 54 self.imgshape2D = None ## width, height of the image 55 self.nframes = None ## Number of time frames 56 self.thickness = 4 ## thickness of junctions, wider 57 self.minsize = 4 ## smallest number of pixels in a cell 58 self.verbose = 1 ## level of printing messages (None/few, normal, debug mode) 59 self.event_class = ["division", "extrusion", "suspect"] ## list of possible events 60 self.main_channel = 0 ## position of the main channel (raw movie) 61 62 self.overtext = dict() 63 self.help_index = 1 ## current display index of help overlay 64 self.blabla = None ## help window 65 self.groups = {} 66 self.tracked = 0 ## has done a tracking 67 self.process_parallel = False ## Do some operations in parallel (n frames in parallel) 68 self.nparallel = 4 ## number of parallel threads 69 self.dtype = np.uint32 ## label type, default 32 but if less labels, reduce it 70 self.outputing = None ## non initialized yet 71 72 self.forbid_gaps = False ## allow gaps in track or not 73 74 self.pref = Preferences() 75 self.shortcuts = self.pref.get_shortcuts() ## user specific shortcuts 76 self.settings = self.pref.get_settings() ## user specific preferences 77 ## display settings 78 self.display_colors = None ## settings for changing some display colors 79 if "Display" in self.settings: 80 if "Colors" in self.settings["Display"]: 81 self.display_colors = self.settings["Display"]["Colors"]
Reset all the parameters to the default values
84 def init_epicure_metadata(self): 85 """ Fills metadata with default values """ 86 ## scalings and unit names 87 self.epi_metadata = {} 88 self.epi_metadata["ScaleXY"] = 1 89 self.epi_metadata["UnitXY"] = "um" 90 self.epi_metadata["ScaleT"] = 1 91 self.epi_metadata["UnitT"] = "min" 92 self.epi_metadata["MainChannel"] = 0 93 self.epi_metadata["Allow gaps"] = True 94 self.epi_metadata["Verbose"] = 1 95 self.epi_metadata["Scale bar"] = True 96 self.epi_metadata["MovieFile"] = "" 97 self.epi_metadata["SegmentationFile"] = "" 98 self.epi_metadata["EpithelialCells"] = True ## epithelial (packed) cells 99 self.epi_metadata["Reloading"] = False ## Never been epiCured yet
Fills metadata with default values
101 def get_resetbtn_color(self): 102 """Returns the color of Reset buttons if defined""" 103 if "Display" in self.settings: 104 if "Colors" in self.settings["Display"]: 105 if "Reset button" in self.settings["Display"]["Colors"]: 106 return self.settings["Display"]["Colors"]["Reset button"] 107 return None
Returns the color of Reset buttons if defined
109 def set_thickness(self, thick): 110 """ 111 Thickness of junctions (half thickness) 112 113 :param: thick set thickness value to input value 114 """ 115 self.thickness = thick
Thickness of junctions (half thickness)
Parameters
- thick set thickness value to input value
117 def movie_from_layer(self, layer, imgpath): 118 """ 119 Prepare the intensity movie from opened layer, and get metadata. 120 121 Resets the internal state, loads image data from the provided layer, 122 handles temporal and channel dimensions, and prepares the movie for processing. 123 124 It extracts metadata including file path and pixel scale, and attempts to handle various 125 image formats (2D, 3D, 4D with different dimension orders). 126 127 :param: layer: A napari layer object containing the image data and scale information. 128 The layer's data attribute should contain the image array. 129 :param: imgpath (str): Absolute or relative file path to the image file being loaded. 130 131 :return: 132 A tuple containing: 133 - caxis (int or None): The axis index corresponding to the channel dimension, 134 or None if no multiple channels are detected. 135 - cval (int): The number of channels found in the image, or 0 if no channels 136 are detected. 137 """ 138 self.reset() ## reload everything 139 self.epi_metadata["MovieFile"] = os.path.abspath(imgpath) 140 ## if the layer is scaled, should be the right scale 141 self.epi_metadata["ScaleXY"] = layer.scale[2] 142 self.img = layer.data 143 nchan = 0 144 if len(self.img.shape)>3: 145 ## Format TCYX in general 146 nchan = self.img.shape[1] 147 ## transform static image to movie (add temporal dimension) 148 if len(self.img.shape) == 2: 149 self.img = np.expand_dims(self.img, axis=0) 150 caxis = None 151 cval = 0 152 if nchan > 0 or len(self.img.shape) > 3: 153 if nchan > 0 and len(self.img.shape) > 3: 154 ## multiple chanels and multiple slices, order axis should be TCXY 155 caxis = 1 156 cval = nchan 157 else: 158 ## one image with multiple chanels 159 minshape = min(self.img.shape) 160 caxis = self.img.shape.index(minshape) 161 cval = minshape 162 self.mov = self.img 163 164 ## display the movie: rename the layer 165 ut.remove_layer(self.viewer, "Movie") 166 layer.name = "Movie" 167 168 self.imgshape = self.viewer.layers["Movie"].data.shape 169 self.imgshape2D = self.imgshape[1:3] 170 self.nframes = self.imgshape[0] 171 return caxis, cval
Prepare the intensity movie from opened layer, and get metadata.
Resets the internal state, loads image data from the provided layer, handles temporal and channel dimensions, and prepares the movie for processing.
It extracts metadata including file path and pixel scale, and attempts to handle various image formats (2D, 3D, 4D with different dimension orders).
Parameters
- layer: A napari layer object containing the image data and scale information. The layer's data attribute should contain the image array.
- imgpath (str): Absolute or relative file path to the image file being loaded.
Returns
A tuple containing: - caxis (int or None): The axis index corresponding to the channel dimension, or None if no multiple channels are detected. - cval (int): The number of channels found in the image, or 0 if no channels are detected.
174 def load_movie(self, imgpath): 175 """ 176 Load the intensity movie, and get metadata 177 178 :param: imgpath: full path to where the movie file is 179 """ 180 self.reset() ## reload everything 181 self.epi_metadata["MovieFile"] = os.path.abspath(imgpath) 182 self.img, nchan, self.epi_metadata["ScaleXY"], self.epi_metadata["UnitXY"], self.epi_metadata["ScaleT"], self.epi_metadata["UnitT"] = ut.open_image( 183 self.epi_metadata["MovieFile"], get_metadata=True, verbose=self.verbose > 1 184 ) 185 ## transform static image to movie (add temporal dimension) 186 if len(self.img.shape) == 2: 187 self.img = np.expand_dims(self.img, axis=0) 188 caxis = None 189 cval = 0 190 if nchan > 0 or len(self.img.shape) > 3: 191 if nchan > 0 and len(self.img.shape) > 3: 192 ## multiple chanels and multiple slices, order axis should be TCXY 193 caxis = 1 194 cval = nchan 195 else: 196 ## one image with multiple chanels 197 minshape = min(self.img.shape) 198 caxis = self.img.shape.index(minshape) 199 cval = minshape 200 self.mov = self.img 201 202 ## display the movie 203 ut.remove_layer(self.viewer, "Movie") 204 mview = self.viewer.add_image(self.img, name="Movie", blending="additive", colormap="gray") 205 mview.contrast_limits = self.quantiles() 206 mview.gamma = 0.95 207 208 self.imgshape = self.viewer.layers["Movie"].data.shape 209 self.imgshape2D = self.imgshape[1:3] 210 self.nframes = self.imgshape[0] 211 return caxis, cval
Load the intensity movie, and get metadata
Parameters
- imgpath: full path to where the movie file is
214 def quantiles(self): 215 """ Returns the quantiles 1% and 99.999% of the raw image to set the display """ 216 return tuple(np.quantile(self.img, [0.01, 0.9999]))
Returns the quantiles 1% and 99.999% of the raw image to set the display
218 def set_verbose(self, verbose): 219 """ 220 Set verbose level 221 222 :param: verbose: amount of message that will be displayed in the Terminal console, from 0 (none) to 4 (a lot, for debugging) 223 """ 224 self.verbose = verbose 225 self.epi_metadata["Verbose"] = verbose
Set verbose level
Parameters
- verbose: amount of message that will be displayed in the Terminal console, from 0 (none) to 4 (a lot, for debugging)
227 def set_gaps_option(self, allow_gap): 228 """Set the mode for gap allowing/forbid in tracks 229 230 :param: allow_gap: boolean. Indicates if gap in tracks (missing cell in one or more frames) should be allowed or not. 231 """ 232 self.epi_metadata["Allow gaps"] = allow_gap 233 self.forbid_gaps = not allow_gap
Set the mode for gap allowing/forbid in tracks
Parameters
- allow_gap: boolean. Indicates if gap in tracks (missing cell in one or more frames) should be allowed or not.
235 def set_epithelia(self, epithelia): 236 """ 237 Set the mode for cell packing (touching or not especially) 238 239 :param: epithelia: boolean, True if cells are touching 240 """ 241 self.epi_metadata["EpithelialCells"] = epithelia
Set the mode for cell packing (touching or not especially)
Parameters
- epithelia: boolean, True if cells are touching
243 def set_scalebar(self, show_scalebar): 244 """ 245 Show or not the scale bar, and set its value 246 247 :param: show_scalebar: boolean, set the visibility of the scale bar 248 """ 249 self.epi_metadata["Scale bar"] = show_scalebar 250 if self.viewer is not None: 251 self.viewer.scale_bar.visible = show_scalebar 252 self.viewer.scale_bar.unit = self.epi_metadata["UnitXY"] 253 for lay in self.viewer.layers: 254 lay.scale = [1, self.epi_metadata["ScaleXY"], self.epi_metadata["ScaleXY"]] 255 self.viewer.reset_view()
Show or not the scale bar, and set its value
Parameters
- show_scalebar: boolean, set the visibility of the scale bar
257 def set_scales(self, scalexy, scalet, unitxy, unitt): 258 """ 259 Set the scaling units for outputs. Put the values in Epicure metadata object 260 261 :param: scalexy: size of one pixel in X,Y directions 262 :param: scalet: duration of one frame (acquisition frequency) 263 :param: unitxy: name of the unit in which the scale is given 264 :param: unitt: name of the temporal unit in which the scale is given 265 """ 266 self.epi_metadata["ScaleXY"] = scalexy 267 self.epi_metadata["ScaleT"] = scalet 268 self.epi_metadata["UnitXY"] = unitxy 269 self.epi_metadata["UnitT"] = unitt 270 if self.viewer is not None: 271 self.viewer 272 if self.verbose > 0: 273 ut.show_info("Movie scales set to " + str(self.epi_metadata["ScaleXY"]) + " " + self.epi_metadata["UnitXY"] + " and " + str(self.epi_metadata["ScaleT"]) + " " + self.epi_metadata["UnitT"])
Set the scaling units for outputs. Put the values in Epicure metadata object
Parameters
- scalexy: size of one pixel in X,Y directions
- scalet: duration of one frame (acquisition frequency)
- unitxy: name of the unit in which the scale is given
- unitt: name of the temporal unit in which the scale is given
275 def set_chanel(self, chan, chanaxis): 276 """ 277 Update the movie to the correct chanel 278 279 :param: chan: channel in which the raw movie is 280 :param: chanaxis: in which axis is the color channels information (usually format is TCYX, so will be 1) 281 """ 282 self.img = np.rollaxis(np.copy(self.mov), chanaxis, 0)[chan] 283 if len(self.img.shape) == 2: 284 self.img = np.expand_dims(self.img, axis=0) 285 ## udpate the image shape informations 286 self.imgshape = self.img.shape 287 self.imgshape2D = self.imgshape[1:3] 288 self.nframes = self.imgshape[0] 289 self.main_channel = chan 290 if self.viewer is not None: 291 mview = self.viewer.layers["Movie"] 292 mview.data = self.img 293 mview.contrast_limits = self.quantiles() 294 mview.gamma = 0.95 295 mview.refresh()
Update the movie to the correct chanel
Parameters
- chan: channel in which the raw movie is
- chanaxis: in which axis is the color channels information (usually format is TCYX, so will be 1)
297 def add_other_chanels(self, chan, chanaxis): 298 """ Open other channels if option selected """ 299 others_raw = np.delete(self.mov, chan, axis=chanaxis) 300 self.others = [] 301 self.others_chanlist = [] 302 if self.others is not None: 303 others_raw = np.rollaxis(others_raw, chanaxis, 0) 304 for ochan in range(others_raw.shape[0]): 305 purechan = ochan 306 if purechan >= chan: 307 purechan = purechan + 1 308 self.others_chanlist.append(purechan) 309 if len(others_raw[ochan].shape) == 2: 310 expanded = np.expand_dims(others_raw[ochan], axis=0) 311 self.others.append( expanded ) 312 else: 313 self.others.append( others_raw[ochan] ) 314 mview = self.viewer.add_image( self.others[ochan], name="MovieChannel_"+str(purechan), blending="additive", colormap="gray" ) 315 mview.contrast_limits=tuple(np.quantile(self.others[ochan],[0.01, 0.9999])) 316 mview.gamma=0.95 317 mview.visible = False
Open other channels if option selected
319 def import_trackmate(self, segpath, verbose=0): 320 """ Load segmentation and tracks from TrackMate XML file """ 321 if verbose > 1: 322 print("Importing segmentation and tracks from TrackMate XML file") 323 np.set_printoptions(suppress=True, floatmode="maxprec_equal") 324 325 img_data_tag = tm._get_ImageData_tag(segpath) 326 metadata = tm._get_metadata(img_data_tag) 327 seg_shape = (int(metadata["nframes"]), int(metadata["height"]), int(metadata["width"])) 328 segmentation = np.zeros(seg_shape, dtype=np.uint16)-1 329 positions, tracks = tm._parse_Model_tag(segpath, metadata, segmentation) 330 label_mapping = tm._build_label_mapping(positions, tracks) 331 positions = tm.relabel_positions(label_mapping, positions) 332 tracks = tm.relabel_tracks(label_mapping, tracks) 333 segmentation = tm.relabel_segmentation(label_mapping, segmentation) 334 return segmentation, tracks
Load segmentation and tracks from TrackMate XML file
337 def load_segmentation(self, seg_input): 338 """Load the segmentation file""" 339 start_time = ut.start_time() 340 self.graph = None ## no loaded graph 341 ## compatibility to string input, the path to the image or a dictionnary 342 if isinstance(seg_input, dict): 343 segpath = seg_input["File"] 344 else: 345 segpath = seg_input 346 self.epi_metadata["SegmentationFile"] = segpath 347 if isinstance(seg_input, dict) and "Layer" in seg_input: 348 ## take the segmentation data and close it 349 self.seg = seg_input["Layer"].data 350 ut.remove_layer(self.viewer, seg_input["Layer"]) 351 else: 352 if str(segpath).endswith(".xml"): 353 ## import a TrackMate file 354 self.seg, self.graph = self.import_trackmate(segpath, verbose=self.verbose>1) 355 else: 356 self.seg, _, _, _, _, _ = ut.open_image(segpath, get_metadata=False, verbose=self.verbose > 1) 357 self.seg = np.uint32(self.seg) 358 ## transform static image to movie (add temporal dimension) 359 if len(self.seg.shape) == 2: 360 self.seg = np.expand_dims(self.seg, axis=0) 361 ## ensure that the shapes are correctly set 362 self.imgshape = self.seg.shape 363 self.imgshape2D = self.seg.shape[1:3] 364 self.nframes = self.seg.shape[0] 365 ## if the segmentation is a junction file, transform it to a label image 366 if ut.is_binary(self.seg): 367 self.junctions_to_label() 368 self.tracked = 0 369 else: 370 self.has_been_tracked() 371 self.prepare_labels() 372 373 ## define a reference size of the movie to scale default parameters 374 self.reference_size = np.max(self.imgshape2D) 375 self.epi_metadata["Reloading"] = True ## has been formatted to EpiCure format 376 377 # display the segmentation file movie 378 if self.viewer is not None: 379 if "Movie" in self.viewer.layers: 380 scale = self.viewer.layers["Movie"].scale 381 else: 382 scale = (1,1,1) 383 self.seglayer = self.viewer.add_labels(self.seg, name="Segmentation", blending="additive", opacity=0.5, scale=scale) 384 self.viewer.dims.set_point(0, 0) 385 self.seglayer.brush_size = 4 ## default label pencil drawing size 386 if self.verbose > 0: 387 ut.show_duration(start_time, header="Segmentation loaded in ")
Load the segmentation file
390 def load_tracks(self, progress_bar): 391 """From the segmentation, get all the metadata""" 392 tracked = "tracked" 393 self.tracking.init_tracks() 394 if self.tracked == 0: 395 tracked = "untracked" 396 else: 397 if self.graph is not None: 398 self.tracking.set_graph(self.graph) 399 if self.forbid_gaps: 400 progress_bar.set_description("check and fix track gaps") 401 self.handle_gaps(track_list=None, verbose=1) 402 ut.show_info("" + str(len(self.tracking.get_track_list())) + " " + tracked + " cells loaded")
From the segmentation, get all the metadata
404 def has_been_tracked(self): 405 """Look if has been tracked already (some labels are in several frames)""" 406 nb = 0 407 for frame in range(self.seg.shape[0]): 408 if frame > 0: 409 inter = np.intersect1d(np.unique(self.seg[frame - 1]), np.unique(self.seg[frame])) 410 if len(inter) > 1: 411 self.tracked = 1 412 return 413 self.tracked = 0 414 return
Look if has been tracked already (some labels are in several frames)
416 def suggest_segfile(self, outdir): 417 """Check if a segmentation file from EpiCure already exists""" 418 if (self.epi_metadata["SegmentationFile"] != "") and ut.found_segfile(self.epi_metadata["SegmentationFile"]): 419 return self.epi_metadata["SegmentationFile"] 420 imgname, imgdir, out = ut.extract_names(self.epi_metadata["MovieFile"], outdir, mkdir=False) 421 return ut.suggest_segfile(out, imgname)
Check if a segmentation file from EpiCure already exists
426 def set_names(self, outdir): 427 """Extract default names from imgpath""" 428 self.imgname, self.imgdir, self.outdir = ut.extract_names(self.epi_metadata["MovieFile"], outdir, mkdir=True)
Extract default names from imgpath
430 def go_epicure(self, outdir="epics", segmentation_input=None): 431 """Initialize everything and start the main widget""" 432 self.set_names(outdir) 433 if segmentation_input is None: 434 segmentation_input = {} 435 segmentation_input["File"] = self.suggest_segfile(outdir) 436 self.viewer.window._status_bar._toggle_activity_dock(True) 437 progress_bar = progress(total=5) 438 progress_bar.set_description("Reading segmented image") 439 ## load the segmentation 440 self.load_segmentation(segmentation_input) 441 if isinstance(segmentation_input, dict): 442 self.epi_metadata["SegmentationFile"] = segmentation_input["File"] 443 else: 444 self.epi_metadata["SegmentationFile"] = segmentation_input 445 progress_bar.update(1) 446 ut.set_active_layer(self.viewer, "Segmentation") 447 448 ## setup the main interface and shortcuts 449 start_time = ut.start_time() 450 progress_bar.set_description("Active EpiCure shortcuts") 451 self.key_bindings() 452 progress_bar.update(2) 453 progress_bar.set_description("Prepare widget") 454 self.main_widget() 455 progress_bar.update(3) 456 progress_bar.set_description("Load tracks") 457 self.load_tracks(progress_bar) 458 progress_bar.update(4) 459 460 ## load graph if it exists 461 epiname = os.path.join(self.outdir, self.imgname + "_epidata.pkl") 462 if os.path.exists(epiname): 463 progress_bar.set_description("Load EpiCure informations") 464 self.load_epicure_data(epiname) 465 if self.verbose > 0: 466 ut.show_duration(start_time, header="Tracks and graph loaded in ") 467 progress_bar.update(5) 468 self.apply_settings() 469 progress_bar.close() 470 self.viewer.window._status_bar._toggle_activity_dock(False)
Initialize everything and start the main widget
473 def apply_settings(self): 474 """Apply all default or prefered settings""" 475 for sety, val in self.settings.items(): 476 if sety == "Display": 477 self.display.apply_settings(val) 478 if "Show help" in val: 479 index = int(val["Show help"]) 480 self.switchOverlayText(index) 481 if "Contour" in val: 482 contour = int(val["Contour"]) 483 self.seglayer.contour = contour 484 self.seglayer.refresh() 485 if "Colors" in val: 486 color = val["Colors"]["button"] 487 check_color = val["Colors"]["checkbox"] 488 line_edit_color = val["Colors"]["line edit"] 489 group_color = val["Colors"]["group"] 490 self.main_gui.setStyleSheet( 491 "QPushButton {background-color: " 492 + color 493 + "} QCheckBox::indicator {background-color: " 494 + check_color 495 + "} QLineEdit {background-color: " 496 + line_edit_color 497 + "} QGroupBox {color: grey; background-color: " 498 + group_color 499 + "} " 500 ) 501 self.display_colors = val["Colors"] 502 if sety == "events": 503 self.inspecting.apply_settings(val) 504 if sety == "Output": 505 self.outputing.apply_settings(val) 506 if sety == "Track": 507 self.tracking.apply_settings(val) 508 if sety == "Edit": 509 self.editing.apply_settings(val) 510 # case _: 511 # continue 512 ## match is not compatible with python 3.9
Apply all default or prefered settings
514 def update_settings(self): 515 """Returns all the prefered settings""" 516 disp = self.settings 517 ## load display current settings (layers visibility) 518 disp["Display"] = self.display.get_current_settings() 519 disp["Display"]["Show help"] = self.help_index 520 disp["Display"]["Contour"] = self.seglayer.contour 521 ## load suspect current settings 522 disp["events"] = self.inspecting.get_current_settings() 523 ## get outputs current settings 524 disp["Output"] = self.outputing.get_current_settings() 525 disp["Track"] = self.tracking.get_current_settings() 526 disp["Edit"] = self.editing.get_current_settings()
Returns all the prefered settings
530 def main_widget(self): 531 """Open the main widget interface""" 532 self.main_gui = QWidget() 533 534 layout = QVBoxLayout() 535 tabs = QTabWidget() 536 tabs.setObjectName("main") 537 layout.addWidget(tabs) 538 self.main_gui.setLayout(layout) 539 540 self.editing = Editing(self.viewer, self) 541 tabs.addTab(self.editing, "Edit") 542 self.inspecting = Inspecting(self.viewer, self) 543 tabs.addTab(self.inspecting, "Inspect") 544 self.tracking = Tracking(self.viewer, self) 545 tabs.addTab(self.tracking, "Track") 546 self.outputing = Outputing(self.viewer, self) 547 tabs.addTab(self.outputing, "Output") 548 self.display = Displaying(self.viewer, self) 549 tabs.addTab(self.display, "Display") 550 self.main_gui.setStyleSheet("QPushButton {background-color: rgb(40, 60, 75)} QCheckBox::indicator {background-color: rgb(40,52,65)}") 551 552 self.viewer.window.add_dock_widget(self.main_gui, name="Main")
Open the main widget interface
554 def key_bindings(self): 555 """Activate shortcuts""" 556 self.text = "-------------- ShortCuts -------------- \n " 557 self.text += "!! Shortcuts work if Segmentation layer is active !! \n" 558 # for sctype, scvals in self.shortcuts.items(): 559 self.text += "\n---" + "General" + " options---\n" 560 sg = self.shortcuts["General"] 561 self.text += ut.print_shortcuts(sg) 562 self.text = self.text + "\n" 563 564 if self.verbose > 0: 565 print("Activating key shortcuts on segmentation layer") 566 print("Press <" + str(sg["show help"]["key"]) + "> to show/hide the main shortcuts") 567 print("Press <" + str(sg["show all"]["key"]) + "> to show ALL shortcuts") 568 ut.setOverlayText(self.viewer, self.text, size=12) 569 570 @self.seglayer.bind_key(sg["show help"]["key"], overwrite=True) 571 def switch_shortcuts(seglayer): 572 # index = (self.help_index+1)%(len(self.overtext.keys())+1) 573 # self.switchOverlayText(index) 574 index = (self.help_index + 1) % 2 575 self.switchOverlayText(index) 576 577 @self.seglayer.bind_key(sg["show all"]["key"], overwrite=True) 578 def list_all_shortcuts(seglayer): 579 self.switchOverlayText(0) ## hide display message in main window 580 text = "**************** EPICURE *********************** \n" 581 text += "\n" 582 text += self.text 583 text += "\n" 584 text += ut.napari_shortcuts() 585 for key, val in self.overtext.items(): 586 text += "\n" 587 text += val 588 self.update_text_window(text) 589 590 @self.seglayer.bind_key(sg["save segmentation"]["key"], overwrite=True) 591 def save_seglayer(seglayer): 592 self.save_epicures() 593 594 @self.viewer.bind_key(sg["save movie"]["key"], overwrite=True) 595 def save_movie(seglayer): 596 endname = "_frames.tif" 597 outname = os.path.join(self.outdir, self.imgname + endname) 598 self.save_movie(outname)
Activate shortcuts
602 def switchOverlayText(self, index): 603 """Switch overlay display text to index""" 604 self.help_index = index 605 if index == 0: 606 ut.showOverlayText(self.viewer, vis=False) 607 return 608 else: 609 ut.showOverlayText(self.viewer, vis=True) 610 # self.setCurrentOverlayText() 611 self.setGeneralOverlayText()
Switch overlay display text to index
613 def init_text_window(self): 614 """Creates and opens a pop-up window with shortcut list""" 615 self.blabla = ut.create_text_window("EpiCure shortcuts")
Creates and opens a pop-up window with shortcut list
617 def update_text_window(self, message): 618 """Update message in separate window""" 619 self.init_text_window() 620 self.blabla.value = message
Update message in separate window
622 def setGeneralOverlayText(self): 623 """set overlay help message to general message""" 624 text = self.text 625 ut.setOverlayText(self.viewer, text, size=12)
set overlay help message to general message
627 def setCurrentOverlayText(self): 628 """Set overlay help text message to current selected options list""" 629 text = self.text 630 dispkey = list(self.overtext.keys())[self.help_index - 1] 631 text += self.overtext[dispkey] 632 ut.setOverlayText(self.viewer, text, size=12)
Set overlay help text message to current selected options list
634 def get_summary(self): 635 """Get a summary of the infos of the movie""" 636 summ = "----------- EpiCure summary ----------- \n" 637 summ += "--- Image infos \n" 638 summ += "Movie name: " + str(self.epi_metadata["MovieFile"]) + "\n" 639 summ += "Movie size (x,y): " + str(self.imgshape2D) + "\n" 640 if self.nframes is not None: 641 summ += "Nb frames: " + str(self.nframes) + "\n" 642 summ += "\n" 643 summ += "--- Segmentation infos \n" 644 summ += "Segmentation file: " + str(self.epi_metadata["SegmentationFile"]) + "\n" 645 summ += "Nb tracks: " + str(len(self.tracking.get_track_list())) + "\n" 646 tracked = "yes" 647 if self.tracked == 0: 648 tracked = "no" 649 summ += "Tracked: " + tracked + "\n" 650 nb_labels, mean_duration, mean_area = ut.summary_labels(self.seg) 651 summ += "Nb cells: " + str(nb_labels) + "\n" 652 summ += "Average track lengths: " + str(mean_duration) + " frames\n" 653 summ += "Average cell area: " + str(mean_area) + " pixels^2\n" 654 summ += "Nb suspect events: " + str(self.inspecting.nb_events(only_suspect=True)) + "\n" 655 summ += "Nb divisions: " + str(self.nb_divisions()) + "\n" 656 summ += "Nb extrusions: " + str(self.inspecting.nb_type("extrusion")) + "\n" 657 summ += "\n" 658 summ += "--- Parameter infos \n" 659 summ += "Junction thickness: " + str(self.thickness) + "\n" 660 return summ
Get a summary of the infos of the movie
662 def nb_divisions(self): 663 """ Return the number of divisions """ 664 return self.inspecting.nb_type("division")
Return the number of divisions
666 def set_contour(self, width): 667 """ 668 Set the width of the contour of the cells to display the segmentation 669 670 :param: width: width of the contours of the segmentation (napari contour parameter). If 0 the cell will be filled by its label 671 """ 672 self.seglayer.contour = width
Set the width of the contour of the cells to display the segmentation
Parameters
- width: width of the contours of the segmentation (napari contour parameter). If 0 the cell will be filled by its label
676 def check_layers(self): 677 """Check that the necessary layers are present""" 678 if self.editing.shapelayer_name not in self.viewer.layers: 679 if self.verbose > 0: 680 print("Reput shape layer") 681 self.editing.create_shapelayer() 682 if self.inspecting.eventlayer_name not in self.viewer.layers: 683 if self.verbose > 0: 684 print("Reput event layer") 685 self.inspecting.create_eventlayer() 686 if "Movie" not in self.viewer.layers: 687 if self.verbose > 0: 688 print("Reput movie layer") 689 mview = self.viewer.add_image(self.img, name="Movie", blending="additive", colormap="gray", scale=[1, self.epi_metadata["ScaleXY"], self.epi_metadata["ScaleXY"]]) 690 # mview.reset_contrast_limits() 691 mview.contrast_limits = self.quantiles() 692 mview.gamma = 0.95 693 if "Segmentation" not in self.viewer.layers: 694 if self.verbose > 0: 695 print("Reput segmentation") 696 self.seglayer = self.viewer.add_labels(self.seg, name="Segmentation", blending="additive", opacity=0.5, scale=self.viewer.layers["Movie"].scale) 697 698 self.finish_update()
Check that the necessary layers are present
700 def finish_update(self, contour=None): 701 """ 702 After doing modifications on some layer(s), select back the main layer Segmentation as active (important for shortcut bindings) and refresh it 703 """ 704 if contour is not None: 705 self.seglayer.contour = contour 706 ut.set_active_layer(self.viewer, "Segmentation") 707 self.seglayer.refresh() 708 duplayers = ["PrevSegmentation"] 709 for dlay in duplayers: 710 if dlay in self.viewer.layers: 711 (self.viewer.layers[dlay]).refresh()
After doing modifications on some layer(s), select back the main layer Segmentation as active (important for shortcut bindings) and refresh it
713 def read_epicure_metadata(self): 714 """Load saved infos from file""" 715 epiname = self.outname() + "_epidata.pkl" 716 if os.path.exists(epiname): 717 infile = open(epiname, "rb") 718 try: 719 epidata = pickle.load(infile) 720 if "EpiMetaData" in epidata.keys(): 721 for key, vals in epidata["EpiMetaData"].items(): 722 self.epi_metadata[key] = vals 723 infile.close() 724 except: 725 ut.show_warning("Could not read EpiCure metadata file " + epiname)
Load saved infos from file
727 def save_epicures(self, imtype="float32"): 728 """ 729 Save all the current data: the segmentation, the metadata (metadata of the image, last parameters used), the events and some display settings. 730 """ 731 outname = os.path.join(self.outdir, self.imgname + "_labels.tif") 732 ut.writeTif(self.seg, outname, self.epi_metadata["ScaleXY"], imtype, what="Segmentation") 733 epiname = os.path.join(self.outdir, self.imgname + "_epidata.pkl") 734 outfile = open(epiname, "wb") 735 self.epi_metadata["MainChannel"] = self.main_channel 736 epidata = {} 737 epidata["EpiMetaData"] = self.epi_metadata 738 if self.groups is not None: 739 epidata["Group"] = self.groups 740 if self.tracking.graph is not None: 741 epidata["Graph"] = self.tracking.graph 742 if self.inspecting is not None and self.inspecting.events is not None: 743 epidata["Events"] = {} 744 if self.inspecting.events.data is not None: 745 epidata["Events"]["Points"] = self.inspecting.events.data 746 epidata["Events"]["Props"] = self.inspecting.events.properties 747 epidata["Events"]["Types"] = self.inspecting.event_types 748 # epidata["Events"]["Symbols"] = self.inspecting.events.symbol 749 # epidata["Events"]["Colors"] = self.inspecting.events.face_color 750 if "Movie" in self.viewer.layers: 751 ## to keep movie layer display settings for this file 752 epidata["Display"] = {} 753 epidata["Display"]["MovieContrast"] = self.viewer.layers["Movie"].contrast_limits 754 pickle.dump(epidata, outfile) 755 outfile.close()
Save all the current data: the segmentation, the metadata (metadata of the image, last parameters used), the events and some display settings.
757 def read_group_data(self, groups): 758 """Read the group EpiCure data from opened file""" 759 if self.verbose > 0: 760 print("Loaded cell groups info: " + str(list(groups.keys()))) 761 if self.verbose > 2: 762 print("Cell groups: " + str(groups)) 763 return groups
Read the group EpiCure data from opened file
765 def read_graph_data(self, infile): 766 """ 767 Read the graph EpiCure data from opened pickle file 768 769 :param: infile: instance of pickle file being read. This will read the next part of the pickle file and load it in the track graph. 770 """ 771 try: 772 graph = pickle.load(infile) 773 if self.verbose > 0: 774 print("Graph (lineage) loaded") 775 return graph 776 except: 777 if self.verbose > 1: 778 print("No graph infos found") 779 return None
Read the graph EpiCure data from opened pickle file
Parameters
- infile: instance of pickle file being read. This will read the next part of the pickle file and load it in the track graph.
781 def read_events_data(self, infile): 782 """Read info of EpiCure events (suspects, divisions) from opened file""" 783 try: 784 events_pts = pickle.load(infile) 785 if events_pts is not None: 786 events_props = pickle.load(infile) 787 events_type = pickle.load(infile) 788 try: 789 symbols = pickle.load(infile) 790 colors = pickle.load(infile) 791 except: 792 if self.verbose > 1: 793 print("No events display info found") 794 symbols = None 795 colors = None 796 return events_pts, events_props, events_type 797 else: 798 return None, None, None 799 except: 800 if self.verbose > 1: 801 print("events info not complete") 802 return None, None, None
Read info of EpiCure events (suspects, divisions) from opened file
804 def load_epicure_data(self, epiname): 805 """Load saved infos from file""" 806 infile = open(epiname, "rb") 807 try: 808 if ut.is_windows(): 809 import pathlib 810 pathlib.PosixPath = pathlib.WindowsPath 811 #epidata = pickle.load( infile, encoding="utf8" ) 812 epidata = pickle.load( infile ) 813 #print(epidata) 814 if "EpiMetaData" in epidata.keys(): 815 # version of epicure file after Epicure 0.2.0 816 self.read_epidata(epidata) 817 infile.close() 818 else: 819 # version anterior of Epicure 0.2.0 820 self.load_epicure_data_old(epidata, infile) 821 except Exception as e: 822 if self.verbose > 1: 823 print(f" {type(e)} {e} - Could not read EpiCure data file {epiname}") 824 else: 825 ut.show_warning(f"Could not read EpiCure data file {epiname}") 826 print(f" {type(e)} {e} - Could not read EpiCure data file {epiname}")
Load saved infos from file
828 def read_epidata(self, epidata): 829 """Read the dict of saved state and initialize all instances with it""" 830 for key, vals in epidata.items(): 831 if key == "EpiMetaData": 832 ## image data is read on the previous step 833 continue 834 if key == "Group": 835 ## Load groups information 836 self.groups = self.read_group_data(vals) 837 self.update_group_lists() 838 if key == "Graph": 839 ## Load graph (lineage) informations 840 self.tracking.graph = vals 841 if self.tracking.graph is not None: 842 self.tracking.tracklayer.refresh() 843 if self.verbose > 2: 844 print(f"Loaded track graph: {self.tracking.graph}") 845 if key == "Events": 846 ## Load events information 847 if "Points" in vals.keys(): 848 pts = vals["Points"] 849 if "Props" in vals.keys(): 850 props = vals["Props"] 851 if "Types" in vals.keys(): 852 event_types = vals["Types"] 853 # if "Symbols" in vals.keys(): 854 # symbols = vals["Symbols"] 855 # if "Colors" in vals.keys(): 856 # colors = vals["Colors"] 857 if pts is not None: 858 if len(pts) > 0: 859 self.inspecting.load_events(pts, props, event_types) 860 if len(pts) > 0 and self.verbose > 0: 861 print("events loaded") 862 ut.show_info("Loaded " + str(len(pts)) + " events") 863 if key == "Display": 864 if vals is not None: 865 ## load display setting 866 if "MovieContrast" in vals.keys(): 867 self.viewer.layers["Movie"].contrast_limits = vals["MovieContrast"]
Read the dict of saved state and initialize all instances with it
869 def load_epicure_data_old(self, groups, infile): 870 """Load saved infos from file""" 871 ## Load groups information 872 self.groups = self.read_group_data(groups) 873 for group in self.groups.keys(): 874 self.editing.update_group_list(group) 875 self.outputing.update_selection_list() 876 ## Load graph (lineage) informations 877 self.tracking.graph = self.read_graph_data(infile) 878 if self.tracking.graph is not None: 879 self.tracking.tracklayer.refresh() 880 ## Load events information 881 pts, props, event_types = self.read_events_data(infile) 882 if pts is not None: 883 if len(pts) > 0: 884 self.inspecting.load_events(pts, props, event_types) 885 if len(pts) > 0 and self.verbose > 0: 886 print("events loaded") 887 ut.show_info("Loaded " + str(len(pts)) + " events") 888 infile.close()
Load saved infos from file
890 def save_movie(self, outname): 891 """Save movie with current display parameters, except zoom""" 892 save_view = self.viewer.camera.copy() 893 save_frame = ut.current_frame(self.viewer) 894 ## place the view to see the whole image 895 self.viewer.reset_view() 896 # self.viewer.camera.zoom = 1 897 sizex = (self.imgshape2D[0] * self.viewer.camera.zoom) / 2 898 sizey = (self.imgshape2D[1] * self.viewer.camera.zoom) / 2 899 if os.path.exists(outname): 900 os.remove(outname) 901 902 ## take a screenshot of each frame 903 for frame in range(self.nframes): 904 self.viewer.dims.set_point(0, frame) 905 shot = self.viewer.window.screenshot(canvas_only=True, flash=False) 906 ## remove border: movie is at the center 907 centx = int(shot.shape[0] / 2) + 1 908 centy = int(shot.shape[1] / 2) + 1 909 shot = shot[ 910 int(centx - sizex) : int(centx + sizex), 911 int(centy - sizey) : int(centy + sizey), 912 ] 913 ut.appendToTif(shot, outname) 914 self.viewer.camera.update(save_view) 915 if save_frame is not None: 916 self.viewer.dims.set_point(0, save_frame) 917 ut.show_info("Movie " + outname + " saved")
Save movie with current display parameters, except zoom
919 def reset_data(self): 920 """Reset EpiCure data (group, suspect, graph)""" 921 self.inspecting.reset_all_events() 922 self.reset_groups() 923 self.tracking.graph = None
Reset EpiCure data (group, suspect, graph)
925 def junctions_to_label(self): 926 """convert epyseg/skeleton result (junctions) to labels map""" 927 ## ensure that skeleton is thin enough 928 for z in range(self.seg.shape[0]): 929 self.skel_one_frame(z) 930 self.seg = ut.reset_labels(self.seg, closing=True)
convert epyseg/skeleton result (junctions) to labels map
932 def skel_one_frame(self, z): 933 """From segmentation of junctions of one frame, get it as a correct skeleton""" 934 skel = skeletonize(self.seg[z] / np.max(self.seg[z])) 935 skel = ut.copy_border(skel, self.seg[z]) 936 self.seg[z] = np.invert(skel)
From segmentation of junctions of one frame, get it as a correct skeleton
938 def reset_labels(self): 939 """Reset all labels, ensure unicity""" 940 if self.epi_metadata["EpithelialCells"]: 941 ### packed (contiguous cells), ensure that they are separated by one pixel only 942 skel = self.get_skeleton() 943 skel = np.uint32(skel) 944 self.seg = skel 945 self.seglayer.data = skel 946 self.junctions_to_label() 947 self.seglayer.data = self.seg 948 else: 949 self.get_cells()
Reset all labels, ensure unicity
951 def check_extrusions_sanity(self): 952 """Check that extrusions seem to be correct (last of tracks )""" 953 extrusions = self.inspecting.get_events_from_type("extrusion") 954 nrem = 0 955 if (extrusions is not None) and (extrusions != []): 956 for extr_id in extrusions: 957 pos, label = self.inspecting.get_event_infos(extr_id) 958 last_frame = self.tracking.get_last_frame(label) 959 if pos[0] != last_frame: 960 if self.verbose > 1: 961 print("Extrusion " + str(extr_id) + " at frame " + str(pos[0]) + " not at the end of track " + str(label)) 962 print("Removing it") 963 self.inspecting.remove_one_event(extr_id) 964 nrem = nrem + 1 965 print("Removed " + str(nrem) + " extrusions that dit not correspond to the end of tracks")
Check that extrusions seem to be correct (last of tracks )
967 def prepare_labels(self): 968 """Process the labels to be in a correct Epicurable format""" 969 if self.epi_metadata["EpithelialCells"]: 970 if self.epi_metadata["Reloading"]: 971 ## if opening an already EpiCured movie, assume it's in correct format 972 return 973 ### packed (contiguous cells), ensure that they are separated by one pixel only 974 self.thin_boundaries() 975 else: 976 self.get_cells()
Process the labels to be in a correct Epicurable format
978 def get_cells(self): 979 """Non jointive cells: check label unicity""" 980 for frame in self.seg: 981 if ut.non_unique_labels(frame): 982 self.seg = ut.reset_labels(self.seg, closing=True) 983 return
Non jointive cells: check label unicity
985 def thin_boundaries(self): 986 """ " Assure that all boundaries are only 1 pixel thick""" 987 if self.process_parallel: 988 self.seg = Parallel(n_jobs=self.nparallel)(delayed(ut.thin_seg_one_frame)(zframe) for zframe in self.seg) 989 self.seg = np.array(self.seg) 990 else: 991 for z in range(self.seg.shape[0]): 992 self.seg[z] = ut.thin_seg_one_frame(self.seg[z])
" Assure that all boundaries are only 1 pixel thick
994 def add_skeleton(self): 995 """add a layer containing the skeleton movie of the segmentation""" 996 # display the segmentation file movie 997 if self.viewer is not None: 998 skel = np.zeros(self.seg.shape, dtype="uint8") 999 skel[self.seg == 0] = 1 1000 skel = self.get_skeleton(viewer=self.viewer) 1001 ut.remove_layer(self.viewer, "Skeleton") 1002 skellayer = self.viewer.add_image(skel, name="Skeleton", blending="additive", opacity=1, scale=self.viewer.layers["Movie"].scale) 1003 skellayer.reset_contrast_limits() 1004 skellayer.contrast_limits = (0, 1)
add a layer containing the skeleton movie of the segmentation
1006 def get_skeleton(self, viewer=None): 1007 """convert labels movie to skeleton (thin boundaries)""" 1008 if self.seg is None: 1009 return None 1010 parallel = 0 1011 if self.process_parallel: 1012 parallel = self.nparallel 1013 return ut.get_skeleton(self.seg, viewer=viewer, verbose=self.verbose, parallel=parallel)
convert labels movie to skeleton (thin boundaries)
1017 def get_free_labels(self, nlab): 1018 """Get the nlab smallest unused labels""" 1019 used = set(self.tracking.get_track_list()) 1020 return ut.get_free_labels(used, nlab)
Get the nlab smallest unused labels
1022 def get_free_label(self): 1023 """Return the first free label""" 1024 return self.get_free_labels(1)[0]
Return the first free label
1026 def has_label(self, label): 1027 """Check if label is present in the tracks""" 1028 return self.tracking.has_track(label)
Check if label is present in the tracks
1030 def has_labels(self, labels): 1031 """Check if labels are present in the tracks""" 1032 return self.tracking.has_tracks(labels)
Check if labels are present in the tracks
1038 def get_labels(self): 1039 """Return list of labels in tracks""" 1040 return list(self.tracking.get_track_list())
Return list of labels in tracks
1043 def delete_tracks(self, tracks): 1044 """Remove all the tracks from the Track layer""" 1045 self.tracking.remove_tracks(tracks)
Remove all the tracks from the Track layer
1047 def delete_track(self, label, frame=None): 1048 """Remove (part of) the track""" 1049 if frame is None: 1050 self.tracking.remove_track(label) 1051 else: 1052 self.tracking.remove_one_frame(label, frame, handle_gaps=self.forbid_gaps)
Remove (part of) the track
1054 def update_centroid(self, label, frame): 1055 """Track label has been change at given frame""" 1056 if label not in self.tracking.has_track(label): 1057 if self.verbose > 1: 1058 print("Track " + str(label) + " not found") 1059 return 1060 self.tracking.update_centroid(label, frame)
Track label has been change at given frame
1063 def get_label_indexes(self, label, start_frame=0): 1064 """Returns the indexes where label is present in segmentation, starting from start_frame""" 1065 indmodif = [] 1066 if self.verbose > 2: 1067 start_time = ut.start_time() 1068 pos = self.tracking.get_track_column(track_id=label, column="fullpos") 1069 pos = pos[pos[:, 0] >= start_frame] 1070 ## if nothing in pos, pb with track data 1071 if pos is None or len(pos) == 0: 1072 ut.show_warning("Something wrong in the track data. Resetting track data (can take time)") 1073 self.tracking.reset_tracks() 1074 self.get_label_indexes(label, start_frame) 1075 1076 indmodif = np.argwhere(self.seg[pos[:, 0]] == label) 1077 indmodif = ut.shiftFrames(indmodif, pos[:, 0]) 1078 if self.verbose > 2: 1079 ut.show_duration(start_time, header="Label indexes found in ") 1080 return indmodif
Returns the indexes where label is present in segmentation, starting from start_frame
1082 def replace_label(self, label, new_label, start_frame=0): 1083 """Replace label with new_label from start_frame - Relabelling only""" 1084 indmodif = self.get_label_indexes(label, start_frame) 1085 new_labels = [new_label] * len(indmodif) 1086 self.change_labels(indmodif, new_labels, replacing=True)
Replace label with new_label from start_frame - Relabelling only
1088 def change_labels_frommerge(self, indmodif, new_labels, remove_labels): 1089 """Change the value at pixels indmodif to new_labels and update tracks/graph. Full remove of the two merged labels""" 1090 if len(indmodif) > 0: 1091 ## get effectively changed labels 1092 indmodif, new_labels, _ = ut.setNewLabel(self.seglayer, indmodif, new_labels, add_frame=None, return_old=False) 1093 if len(new_labels) > 0: 1094 self.update_added_labels(indmodif, new_labels) 1095 self.update_removed_labels(indmodif, remove_labels) 1096 self.seglayer.refresh()
Change the value at pixels indmodif to new_labels and update tracks/graph. Full remove of the two merged labels
1098 def change_labels(self, indmodif, new_labels, replacing=False): 1099 """Change the value at pixels indmodif to new_labels and update tracks/graph 1100 1101 Assume that only label at current frame can have its shape modified. Other changed label is only relabelling at frames > current frame (child propagation) 1102 """ 1103 if len(indmodif) > 0: 1104 ## get effectively changed labels 1105 indmodif, new_labels, old_labels = ut.setNewLabel(self.seglayer, indmodif, new_labels, add_frame=None) 1106 if len(new_labels) > 0: 1107 if replacing: 1108 self.update_replaced_labels(indmodif, new_labels, old_labels) 1109 else: 1110 ## the only label to change are the current frame (smaller one), the other are only relabelling (propagation) 1111 cur_frame = np.min(indmodif[0]) 1112 to_reshape = indmodif[0] == cur_frame 1113 self.update_changed_labels((indmodif[0][to_reshape], indmodif[1][to_reshape], indmodif[2][to_reshape]), new_labels[to_reshape], old_labels[to_reshape]) 1114 to_relab = np.invert(to_reshape) 1115 self.update_replaced_labels((indmodif[0][to_relab], indmodif[1][to_relab], indmodif[2][to_relab]), new_labels[to_relab], old_labels[to_relab]) 1116 self.seglayer.refresh()
Change the value at pixels indmodif to new_labels and update tracks/graph
Assume that only label at current frame can have its shape modified. Other changed label is only relabelling at frames > current frame (child propagation)
1118 def get_mask(self, label, start=None, end=None): 1119 """Get mask of label from frame start to frame end""" 1120 if (start is None) or (end is None): 1121 start, end = self.tracking.get_extreme_frames(label) 1122 crop = self.seg[start : (end + 1)] 1123 mask = np.isin(crop, [label]) * 1 1124 return mask
Get mask of label from frame start to frame end
1126 def get_label_movie(self, label, extend=1.25): 1127 """Get movie centered on label""" 1128 start, end = self.tracking.get_extreme_frames(label) 1129 mask = self.get_mask(label, start, end) 1130 boxes = [] 1131 centers = [] 1132 max_box = 0 1133 for frame in mask: 1134 props = regionprops(frame) 1135 bbox = props[0].bbox 1136 boxes.append(bbox) 1137 centers.append(props[0].centroid) 1138 for i in range(2): 1139 max_box = max(max_box, bbox[i + 2] - bbox[i]) 1140 1141 box_size = int(max_box * extend) 1142 movie = np.zeros((end - start + 1, box_size, box_size)) 1143 for i, frame in enumerate(range(start, end + 1)): 1144 xmin = int(centers[i][0] - box_size / 2) 1145 xminshift = 0 1146 if xmin < 0: 1147 xminshift = -xmin 1148 xmin = 0 1149 xmax = xmin + box_size - xminshift 1150 xmaxshift = box_size 1151 if xmax > self.imgshape2D[0]: 1152 xmaxshift = self.imgshape2D[0] - xmax 1153 xmax = self.imgshape2D[0] 1154 1155 ymin = int(centers[i][1] - max_box / 2) 1156 yminshift = 0 1157 if ymin < 0: 1158 yminshift = -ymin 1159 ymin = 0 1160 ymax = ymin + box_size - yminshift 1161 ymaxshift = box_size 1162 if ymax > self.imgshape2D[1]: 1163 ymaxshift = self.imgshape2D[1] - ymax 1164 ymax = self.imgshape2D[1] 1165 1166 movie[i, xminshift:xmaxshift, yminshift:ymaxshift] = self.img[frame, xmin:xmax, ymin:ymax] 1167 return movie
Get movie centered on label
1170 def cell_radius(self, label, frame): 1171 """Approximate the cell radius at given frame""" 1172 area = np.sum(self.seg[frame] == label) 1173 radius = math.sqrt(area / math.pi) 1174 return radius
Approximate the cell radius at given frame
1176 def cell_area(self, label, frame): 1177 """Approximate the cell radius at given frame""" 1178 area = np.sum(self.seg[frame] == label) 1179 return area
Approximate the cell radius at given frame
1181 def cell_on_border(self, label, frame): 1182 """Check if a given cell is on border of the image""" 1183 bbox = ut.getBBox2D(self.seg[frame], label) 1184 out = ut.outerBBox2D(bbox, self.imgshape2D, margin=3) 1185 return out
Check if a given cell is on border of the image
1188 def add_label(self, labels, frame=None): 1189 """Add a label to the tracks""" 1190 if frame is not None: 1191 if np.isscalar(labels): 1192 labels = [labels] 1193 self.tracking.add_one_frame(labels, frame, refresh=True) 1194 else: 1195 if self.verbose > 1: 1196 print("TODO add label no frame")
Add a label to the tracks
1198 def add_one_label_to_track(self, label): 1199 """Add the track data of a given label if missing""" 1200 iframe = 0 1201 while (iframe < self.nframes) and (label not in self.seg[iframe]): 1202 iframe = iframe + 1 1203 while (iframe < self.nframes) and (label in self.seg[iframe]): 1204 self.tracking.add_one_frame([label], iframe) 1205 iframe = iframe + 1
Add the track data of a given label if missing
1207 def update_label(self, label, frame): 1208 """Update the given label at given frame""" 1209 self.tracking.update_track_on_frame([label], frame)
Update the given label at given frame
1211 def update_changed_labels(self, indmodif, new_labels, old_labels, full=False): 1212 """Check what had been modified, and update tracks from it, looking frame by frame""" 1213 ## check all the old_labels if still present or not 1214 if self.verbose > 1: 1215 start_time = time.time() 1216 frames = np.unique(indmodif[0]) 1217 all_deleted = [] 1218 debug_verb = self.verbose > 2 1219 if debug_verb: 1220 print("Updating labels in frames " + str(frames)) 1221 for frame in frames: 1222 keep = indmodif[0] == frame 1223 ## check old labels if totally removed or not 1224 deleted = np.setdiff1d(old_labels[keep], self.seg[frame]) 1225 left = np.setdiff1d(old_labels[keep], deleted) 1226 if deleted.shape[0] > 0: 1227 self.tracking.remove_one_frame(deleted, frame, handle_gaps=False, refresh=False) 1228 if self.forbid_gaps: 1229 all_deleted = all_deleted + list(set(deleted) - set(all_deleted)) 1230 if left.shape[0] > 0: 1231 self.tracking.update_track_on_frame(left, frame) 1232 ## now check new labels 1233 nlabels = np.unique(new_labels[keep]) 1234 if nlabels.shape[0] > 0: 1235 self.tracking.update_track_on_frame(nlabels, frame) 1236 if debug_verb: 1237 print("Labels deleted at frame " + str(frame) + " " + str(deleted) + " or added " + str(nlabels))
Check what had been modified, and update tracks from it, looking frame by frame
1239 def update_added_labels(self, indmodif, new_labels): 1240 """Update tracks of labels that have been fully added""" 1241 if self.verbose > 1: 1242 start_time = time.time() 1243 1244 ## Deleted labels 1245 frames = np.unique(indmodif[0]) 1246 self.tracking.add_tracks_fromindices(indmodif, new_labels) 1247 if self.forbid_gaps: 1248 ## Check if some gaps has been created in tracks (remove middle(s) frame(s)) 1249 added = list(set(new_labels)) 1250 if len(added) > 0: 1251 self.handle_gaps(added, verbose=0) 1252 1253 if self.verbose > 1: 1254 ut.show_duration(start_time, "updated added tracks in ")
Update tracks of labels that have been fully added
1256 def update_removed_labels(self, indmodif, old_labels): 1257 """Update tracks of labels that have been fully removed""" 1258 if self.verbose > 1: 1259 start_time = time.time() 1260 1261 ## Deleted labels 1262 frames = np.unique(indmodif[0]) 1263 self.tracking.remove_on_frames(np.unique(old_labels), frames) 1264 if self.forbid_gaps: 1265 ## Check if some gaps has been created in tracks (remove middle(s) frame(s)) 1266 deleted = list(set(old_labels)) 1267 if len(deleted) > 0: 1268 self.handle_gaps(deleted, verbose=0) 1269 1270 if self.verbose > 1: 1271 ut.show_duration(start_time, "updated removed tracks in ")
Update tracks of labels that have been fully removed
1273 def update_replaced_labels(self, indmodif, new_labels, old_labels): 1274 """Old_labels were fully replaced by new_labels on some frames, update tracks from it""" 1275 if self.verbose > 1: 1276 start_time = time.time() 1277 1278 ## Deleted labels 1279 frames = np.unique(indmodif[0]) 1280 self.tracking.replace_on_frames(np.unique(old_labels), np.unique(new_labels), frames) 1281 if self.forbid_gaps: 1282 ## Check if some gaps has been created in tracks (remove middle(s) frame(s)) 1283 deleted = list(set(old_labels)) 1284 if len(deleted) > 0: 1285 self.handle_gaps(deleted, verbose=0) 1286 1287 if self.verbose > 1: 1288 ut.show_duration(start_time, "updated replaced tracks in ")
Old_labels were fully replaced by new_labels on some frames, update tracks from it
1290 def handle_gaps(self, track_list, verbose=None): 1291 """Check and fix gaps in tracks""" 1292 if verbose is None: 1293 verbose = self.verbose 1294 gaped = self.tracking.check_gap(track_list, verbose=verbose) 1295 if len(gaped) > 0: 1296 if self.verbose > 0: 1297 print("Relabelling tracks with gaps") 1298 self.fix_gaps(gaped)
Check and fix gaps in tracks
1300 def fix_gaps(self, gaps): 1301 """Fix when some gaps has been created in tracks""" 1302 for gap in gaps: 1303 gap_frames = self.tracking.gap_frames(gap) 1304 cur_gap = gap 1305 for gapy in gap_frames: 1306 new_value = self.get_free_label() 1307 self.replace_label(cur_gap, new_value, gapy) 1308 cur_gap = new_value
Fix when some gaps has been created in tracks
1310 def swap_labels(self, lab, olab, frame): 1311 """Exchange two labels""" 1312 self.tracking.swap_frame_id(lab, olab, frame)
Exchange two labels
1314 def swap_tracks(self, lab, olab, start_frame): 1315 """Exchange two tracks""" 1316 ## split the two labels to unused value 1317 tmp_labels = self.get_free_labels(2) 1318 for i, laby in enumerate([lab, olab]): 1319 self.replace_label(laby, tmp_labels[i], start_frame) 1320 1321 ## replace the two initial labels, in inversed order 1322 self.replace_label(tmp_labels[0], olab, start_frame) 1323 self.replace_label(tmp_labels[1], lab, start_frame)
Exchange two tracks
1325 def split_track(self, label, frame): 1326 """Split a track at given frame""" 1327 new_label = self.get_free_label() 1328 self.replace_label(label, new_label, frame) 1329 if self.verbose > 0: 1330 ut.show_info("Split track " + str(label) + " from frame " + str(frame)) 1331 return new_label
Split a track at given frame
1333 def update_changed_labels_img(self, img_before, img_after, added=True, removed=True): 1334 """Update tracks from changes between the two labelled images""" 1335 if self.verbose > 1: 1336 print("Updating changed labels from images") 1337 indmodif = np.argwhere(img_before != img_after).tolist() 1338 if len(indmodif) <= 0: 1339 return 1340 indmodif = tuple(np.array(indmodif).T) 1341 new_labels = img_after[indmodif] 1342 old_labels = img_before[indmodif] 1343 self.update_changed_labels(indmodif, new_labels, old_labels)
Update tracks from changes between the two labelled images
1345 def added_labels_oneframe(self, frame, img_before, img_after): 1346 """Update added tracks between the two labelled images at frame""" 1347 ## Look for added labels 1348 added_labels = np.setdiff1d(img_after, img_before) 1349 self.tracking.add_one_frame(added_labels, frame, refresh=True)
Update added tracks between the two labelled images at frame
1351 def removed_labels(self, img_before, img_after, frame=None): 1352 """Update removed tracks between the two labelled images""" 1353 ## Look for added labels 1354 deleted_labels = np.setdiff1d(img_before, img_after) 1355 if frame is None: 1356 self.tracking.remove_tracks(deleted_labels) 1357 else: 1358 self.tracking.remove_one_frame(track_id=deleted_labels.tolist(), frame=frame, handle_gaps=self.forbid_gaps)
Update removed tracks between the two labelled images
1360 def remove_label(self, label, force=False): 1361 """Remove a given label if allowed""" 1362 ut.changeLabel(self.seglayer, label, 0) 1363 self.tracking.remove_tracks(label) 1364 self.seglayer.refresh()
Remove a given label if allowed
1366 def remove_labels(self, labels, force=False): 1367 """Remove all allowed labels""" 1368 inds = [] 1369 for lab in labels: 1370 # if (force) or (not self.locked_label(label)): 1371 inds = inds + ut.getLabelIndexes(self.seglayer.data, lab, None) 1372 ut.setNewLabel(self.seglayer, inds, 0) 1373 self.tracking.remove_tracks(labels)
Remove all allowed labels
1375 def keep_labels(self, labels, force=True): 1376 """Remove all other labels that are not in labels""" 1377 inds = [] 1378 toremove = list(set(self.tracking.get_track_list()) - set(labels)) 1379 # for lab in self.tracking.get_track_list(): 1380 # if lab not in labels: 1381 # if (force) or (not self.locked_label(label)): 1382 for lab in toremove: 1383 inds = inds + ut.getLabelIndexes(self.seglayer.data, lab, None) 1384 # toremove.append(lab) 1385 ut.setNewLabel(self.seglayer, inds, 0) 1386 self.tracking.remove_tracks(toremove)
Remove all other labels that are not in labels
1388 def get_frame_features(self, frame): 1389 """Measure the label properties of given frame""" 1390 return regionprops(self.seg[frame])
Measure the label properties of given frame
1392 def updates_after_tracking(self): 1393 """When tracking has been done, update events, others""" 1394 self.inspecting.get_divisions()
When tracking has been done, update events, others
1398 def get_all_groups(self, numeric=False): 1399 """Add all groups info""" 1400 if numeric: 1401 groups = [0] * self.nlabels() 1402 else: 1403 groups = ["None"] * self.nlabels() 1404 for igroup, gr in self.groups.keys(): 1405 indexes = self.tracking.get_track_indexes(self.groups[gr]) 1406 if numeric: 1407 groups[indexes] = igroup + 1 1408 else: 1409 groups[indexes] = gr 1410 return groups
Add all groups info
1412 def get_groups(self, labels, numeric=False): 1413 """Add the group info of the given labels (repeated)""" 1414 if numeric: 1415 groups = [0] * len(labels) 1416 else: 1417 groups = ["Ungrouped"] * len(labels) 1418 for lab in np.unique(labels): 1419 gr = self.find_group(lab) 1420 if gr is None: 1421 continue 1422 if numeric: 1423 gr = self.groups.keys().index() + 1 1424 indexes = (np.argwhere(labels == lab)).flatten() 1425 for ind in indexes: 1426 groups[ind] = gr 1427 return groups
Add the group info of the given labels (repeated)
1429 def cells_ingroup(self, labels, group): 1430 """Put the cell "label" in group group, add it if new group""" 1431 presents = self.has_labels(labels) 1432 labels = np.array(labels)[presents] 1433 if group not in self.groups.keys(): 1434 self.groups[group] = [] 1435 self.update_group_lists() 1436 ## add only non present label(s) 1437 grlabels = self.groups[group] 1438 self.groups[group] = list(set(grlabels + labels.tolist()))
Put the cell "label" in group group, add it if new group
1440 def group_of_labels(self): 1441 """List the group of each label""" 1442 res = {} 1443 for group, labels in self.groups.items(): 1444 for label in labels: 1445 res[label] = group 1446 return res
List the group of each label
1448 def find_group(self, label): 1449 """Find in which group the label is""" 1450 for gr, labs in self.groups.items(): 1451 if label in labs: 1452 return gr 1453 return None
Find in which group the label is
1455 def cell_removegroup(self, label): 1456 """Detach the cell from its group""" 1457 if not self.has_label(label): 1458 if self.verbose > 1: 1459 print("Cell " + str(label) + " missing") 1460 group = self.find_group(label) 1461 if group is not None: 1462 self.groups[group].remove(label) 1463 if len(self.groups[group]) <= 0: 1464 del self.groups[group] 1465 self.update_group_lists()
Detach the cell from its group
1467 def update_group_lists(self): 1468 """Update all the lists depending on the group names""" 1469 if self.outputing is not None: 1470 self.outputing.update_selection_list() 1471 if self.editing is not None: 1472 self.editing.update_group_lists()
Update all the lists depending on the group names
1474 def reset_group(self, group_name): 1475 """Reset/remove a given group""" 1476 if group_name == "All": 1477 self.reset_groups() 1478 return 1479 if group_name in self.groups.keys(): 1480 del self.groups[group_name] 1481 self.update_group_lists()
Reset/remove a given group
1483 def reset_groups(self): 1484 """Remove all group information for all cells""" 1485 self.groups = {} 1486 self.update_group_lists()
Remove all group information for all cells
1488 def draw_groups(self): 1489 """Draw all the epicells colored by their group""" 1490 grouped = np.zeros(self.seg.shape, np.uint8) 1491 if (self.groups is None) or len(self.groups.keys()) == 0: 1492 return grouped 1493 for group, labels in self.groups.items(): 1494 igroup = self.get_group_index(group) + 1 1495 np.place(grouped, np.isin(self.seg, labels), igroup) 1496 return grouped
Draw all the epicells colored by their group
1498 def get_group_index(self, group): 1499 """Get the index of group in the list of groups""" 1500 if group in list(self.groups.keys()): 1501 igroup = list(self.groups.keys()).index(group) 1502 return igroup 1503 return -1
Get the index of group in the list of groups
1506 def only_current_roi(self, frame): 1507 """Put 0 everywhere outside the current ROI""" 1508 roi_labels = self.editing.get_labels_inside() 1509 if roi_labels is None: 1510 return None 1511 # remove all other labels that are not in roi_labels 1512 roilab = np.copy(self.seg[frame]) 1513 np.place(roilab, np.isin(roilab, roi_labels, invert=True), 0) 1514 return roilab
Put 0 everywhere outside the current ROI