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