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
class EpiCure:
  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
EpiCure(viewer=None)
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.
viewer

Napari viewer that is used for this session

def reset(self):
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

def init_epicure_metadata(self):
 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

def get_resetbtn_color(self):
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

def set_thickness(self, thick):
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
def movie_from_layer(self, layer, imgpath):
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.
def load_movie(self, imgpath):
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
def quantiles(self):
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

def set_verbose(self, verbose):
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)
def set_gaps_option(self, allow_gap):
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.
def set_epithelia(self, epithelia):
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
def set_scalebar(self, show_scalebar):
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
def set_scales(self, scalexy, scalet, unitxy, unitt):
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
def set_chanel(self, chan, chanaxis):
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)
def add_other_chanels(self, chan, chanaxis):
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

def import_geff(self, segpath, verbose=0):
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

def import_trackmate(self, segpath, verbose=0):
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

def load_segmentation(self, seg_input):
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

def load_tracks(self, track_table, progress_bar):
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

def has_been_tracked(self):
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)

def suggest_segfile(self, outdir):
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

def outname(self):
455    def outname(self):
456        return os.path.join(self.outdir, self.imgname)
def set_names(self, outdir):
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

def go_epicure(self, outdir='epics', segmentation_input=None):
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

def apply_settings(self):
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

def update_settings(self):
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

def main_widget(self):
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

def key_bindings(self):
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

def switchOverlayText(self, index):
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

def init_text_window(self):
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

def update_text_window(self, message):
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

def setGeneralOverlayText(self):
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

def setCurrentOverlayText(self):
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

def get_summary(self):
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

def nb_divisions(self):
694    def nb_divisions(self):
695        """ Return the number of divisions """
696        return self.inspecting.nb_type("division")

Return the number of divisions

def set_contour(self, width):
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
def check_layers(self):
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

def finish_update(self, contour=None):
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

def read_epicure_metadata(self):
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

def save_epicures(self, imtype='float32'):
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.

def read_group_data(self, groups):
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

def read_graph_data(self, infile):
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.
def read_events_data(self, infile):
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

def load_epicure_data(self, epiname):
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

def read_epidata(self, epidata):
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

def load_epicure_data_old(self, groups, infile):
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

def save_movie(self, outname):
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

def reset_data(self):
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)

def junctions_to_label(self):
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

def skel_one_frame(self, z):
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

def reset_labels(self):
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

def check_extrusions_sanity(self):
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 )

def prepare_labels(self):
 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

def get_cells(self):
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

def thin_boundaries(self):
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

def add_skeleton(self):
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

def get_skeleton(self, viewer=None):
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)

def get_free_labels(self, nlab):
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

def get_free_label(self):
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

def has_label(self, 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

def has_labels(self, labels):
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

def nlabels(self):
1066    def nlabels(self):
1067        """Number of unique tracks"""
1068        return self.tracking.nb_tracks()

Number of unique tracks

def get_labels(self):
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

def delete_tracks(self, 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

def delete_track(self, label, frame=None):
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

def update_centroid(self, label, frame):
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

def get_label_indexes(self, label, start_frame=0):
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

def replace_label(self, label, new_label, start_frame=0):
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

def change_labels_frommerge(self, indmodif, new_labels, remove_labels):
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

def change_labels(self, indmodif, new_labels, replacing=False):
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)

def get_mask(self, label, start=None, end=None):
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

def get_label_movie(self, label, extend=1.25):
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

def cell_radius(self, label, frame):
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

def cell_area(self, label, 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

def cell_on_border(self, label, 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

def add_label(self, labels, frame=None):
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

def add_one_label_to_track(self, label):
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

def update_label(self, label, frame):
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

def update_changed_labels(self, indmodif, new_labels, old_labels, full=False):
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

def update_added_labels(self, indmodif, new_labels):
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

def update_removed_labels(self, indmodif, old_labels):
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

def update_replaced_labels(self, indmodif, new_labels, old_labels):
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

def handle_gaps(self, track_list, verbose=None):
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

def fix_gaps(self, gaps):
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

def swap_labels(self, lab, olab, frame):
1342    def swap_labels(self, lab, olab, frame):
1343        """Exchange two labels"""
1344        self.tracking.swap_frame_id(lab, olab, frame)

Exchange two labels

def swap_tracks(self, lab, olab, start_frame):
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

def split_track(self, label, frame):
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

def update_changed_labels_img(self, img_before, img_after, added=True, removed=True):
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

def added_labels_oneframe(self, frame, img_before, img_after):
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

def removed_labels(self, img_before, img_after, frame=None):
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

def remove_label(self, label, force=False):
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

def remove_labels(self, labels, force=False):
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

def keep_labels(self, labels, force=True):
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

def get_frame_features(self, frame):
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

def updates_after_tracking(self):
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

def get_all_groups(self, numeric=False):
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

def get_groups(self, labels, numeric=False):
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)

def cells_ingroup(self, labels, group):
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

def group_of_labels(self):
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

def find_group(self, 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

def cell_removegroup(self, label):
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

def update_group_lists(self):
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

def reset_group(self, group_name):
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

def reset_groups(self):
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

def draw_groups(self):
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

def get_group_index(self, 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

def only_current_roi(self, frame):
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