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