epicure.epicuring

EpiCure main class.

Organizes all the different parts, handles the raw movie and segmentation.

Open and initialize the files. Launch the main widget composed of the segmentation and tracking editing features. This class handles the metadata and do the hub between the different objects/classes. All other classes are linked to this one.

The metadata are stored in the epi_metadata object. The raw movie is linked in the self.movie attribute. The napari viewer referenced by self.viewer handles all the layers that are displayed.

EpiCure's main parts are organized similarly to the Tab widgets in the interface, with main objects: edits to handle segmentation editions, tracking for tracking options and handling of the tracking graph and informations, outputs to perform analyses or export to other format, display for displaying options, inspecting to look for potential segmentation errors and handles cellular events (division, extruxion, suspect segmentation).

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

Initialize the EpiCure viewer instance.

Parameters
  • viewer (napari.Viewer, optional): An existing napari Viewer instance to use. If None, a new Viewer instance will be created with show=False. Defaults to None.
viewer

Napari viewer that is used for this session

def reset(self):
55    def reset(self):
56        """ Reset all the parameters to the default values """
57        self.init_epicure_metadata()  ## initialize metadata variables (scalings, channels)
58        self.img = None
59        """ data of the raw movie """
60        self.inspecting = None
61        """ interface for inspection options """
62        self.others = None
63        self.imgshape2D = None  ## width, height of the image
64        self.nframes = None  ## Number of time frames
65        self.thickness = 4  ## thickness of junctions, wider
66        self.minsize = 4  ## smallest number of pixels in a cell
67        self.verbose = 1  ## level of printing messages (None/few, normal, debug mode)
68        self.event_class = ["division", "extrusion", "suspect"]  ## list of possible events
69        self.main_channel = 0  ## position of the main channel (raw movie) 
70        
71        self.overtext = dict()
72        self.help_index = 1  ## current display index of help overlay
73        self.blabla = None  ## help window
74        self.groups = {}
75        self.tracked = 0  ## has done a tracking
76        self.process_parallel = False  ## Do some operations in parallel (n frames in parallel)
77        self.nparallel = 4  ## number of parallel threads
78        self.dtype = np.uint32  ## label type, default 32 but if less labels, reduce it
79        self.outputing = None  ## non initialized yet
80
81        self.forbid_gaps = False  ## allow gaps in track or not
82
83        self.pref = Preferences()
84        self.shortcuts = self.pref.get_shortcuts()  ## user specific shortcuts
85        self.settings = self.pref.get_settings()  ## user specific preferences
86        ## display settings
87        self.display_colors = None  ## settings for changing some display colors
88        if "Display" in self.settings:
89            if "Colors" in self.settings["Display"]:
90                self.display_colors = self.settings["Display"]["Colors"]

Reset all the parameters to the default values

def init_epicure_metadata(self):
 93    def init_epicure_metadata(self):
 94        """ Fills metadata with default values """
 95        ## scalings and unit names
 96        self.epi_metadata = {}
 97        self.epi_metadata["ScaleXY"] = 1
 98        self.epi_metadata["UnitXY"] = "um"
 99        self.epi_metadata["ScaleT"] = 1
100        self.epi_metadata["UnitT"] = "min"
101        self.epi_metadata["MainChannel"] = 0
102        self.epi_metadata["Allow gaps"] = True
103        self.epi_metadata["Verbose"] = 1
104        self.epi_metadata["Scale bar"] = True
105        self.epi_metadata["MovieFile"] = ""
106        self.epi_metadata["SegmentationFile"] = ""
107        self.epi_metadata["EpithelialCells"] = True  ## epithelial (packed) cells
108        self.epi_metadata["Reloading"] = False  ## Never been epiCured yet

Fills metadata with default values

def get_resetbtn_color(self):
110    def get_resetbtn_color(self):
111        """Returns the color of Reset buttons if defined"""
112        if "Display" in self.settings:
113            if "Colors" in self.settings["Display"]:
114                if "Reset button" in self.settings["Display"]["Colors"]:
115                    return self.settings["Display"]["Colors"]["Reset button"]
116        return None

Returns the color of Reset buttons if defined

def set_thickness(self, thick):
118    def set_thickness(self, thick):
119        """
120        Thickness of junctions (half thickness)
121        
122        :param: thick set thickness value to input value
123        """
124        self.thickness = thick

Thickness of junctions (half thickness)

Parameters
  • thick set thickness value to input value
def movie_from_layer(self, layer, imgpath):
126    def movie_from_layer(self, layer, imgpath):
127        """
128        Prepare the intensity movie from opened layer, and get metadata.
129        
130        Resets the internal state, loads image data from the provided layer,
131        handles temporal and channel dimensions, and prepares the movie for processing.
132        
133        It extracts metadata including file path and pixel scale, and attempts to handle various
134        image formats (2D, 3D, 4D with different dimension orders).
135        
136        :param: layer: A napari layer object containing the image data and scale information.
137                The layer's data attribute should contain the image array.
138        :param: imgpath (str): Absolute or relative file path to the image file being loaded.
139        
140        :return:
141            A tuple containing:
142                - caxis (int or None): The axis index corresponding to the channel dimension,
143                  or None if no multiple channels are detected.
144                - cval (int): The number of channels found in the image, or 0 if no channels
145                  are detected.
146        """
147        self.reset() ## reload everything 
148        self.epi_metadata["MovieFile"] = os.path.abspath(imgpath)
149        ## if the layer is scaled, should be the right scale
150        self.epi_metadata["ScaleXY"] = layer.scale[2]
151        self.img = layer.data
152        nchan = 0
153        if len(self.img.shape)>3:
154            ## Format TCYX in general
155            nchan = self.img.shape[1]
156        ## transform static image to movie (add temporal dimension)
157        if len(self.img.shape) == 2:
158            self.img = np.expand_dims(self.img, axis=0)
159        caxis = None
160        cval = 0
161        if nchan > 0 or len(self.img.shape) > 3:
162            if nchan > 0 and len(self.img.shape) > 3:
163                ## multiple chanels and multiple slices, order axis should be TCXY
164                caxis = 1
165                cval = nchan
166            else:
167                ## one image with multiple chanels
168                minshape = min(self.img.shape)
169                caxis = self.img.shape.index(minshape)
170                cval = minshape
171            self.mov = self.img
172
173        ## display the movie: rename the layer
174        ut.remove_layer(self.viewer, "Movie")
175        layer.name = "Movie"
176
177        self.imgshape = self.viewer.layers["Movie"].data.shape
178        self.imgshape2D = self.imgshape[1:3]
179        self.nframes = self.imgshape[0]
180        return caxis, cval

Prepare the intensity movie from opened layer, and get metadata.

Resets the internal state, loads image data from the provided layer, handles temporal and channel dimensions, and prepares the movie for processing.

It extracts metadata including file path and pixel scale, and attempts to handle various image formats (2D, 3D, 4D with different dimension orders).

Parameters
  • layer: A napari layer object containing the image data and scale information. The layer's data attribute should contain the image array.
  • imgpath (str): Absolute or relative file path to the image file being loaded.
Returns
A tuple containing:
    - caxis (int or None): The axis index corresponding to the channel dimension,
      or None if no multiple channels are detected.
    - cval (int): The number of channels found in the image, or 0 if no channels
      are detected.
def load_movie(self, imgpath):
183    def load_movie(self, imgpath):
184        """ 
185            Load the intensity movie, and get metadata
186
187            :param: imgpath: full path to where the movie file is    
188        """
189        self.reset() ## reload everything 
190        self.epi_metadata["MovieFile"] = os.path.abspath(imgpath)
191        self.img, nchan, self.epi_metadata["ScaleXY"], self.epi_metadata["UnitXY"], self.epi_metadata["ScaleT"], self.epi_metadata["UnitT"] = ut.open_image(
192            self.epi_metadata["MovieFile"], get_metadata=True, verbose=self.verbose > 1
193        )
194        ## transform static image to movie (add temporal dimension)
195        if len(self.img.shape) == 2:
196            self.img = np.expand_dims(self.img, axis=0)
197        caxis = None
198        cval = 0
199        if nchan > 0 or len(self.img.shape) > 3:
200            if nchan > 0 and len(self.img.shape) > 3:
201                ## multiple chanels and multiple slices, order axis should be TCXY
202                caxis = 1
203                cval = nchan
204            else:
205                ## one image with multiple chanels
206                minshape = min(self.img.shape)
207                caxis = self.img.shape.index(minshape)
208                cval = minshape
209            self.mov = self.img
210
211        ## display the movie
212        ut.remove_layer(self.viewer, "Movie")
213        mview = self.viewer.add_image(self.img, name="Movie", blending="additive", colormap="gray")
214        mview.contrast_limits = self.quantiles()
215        mview.gamma = 0.95
216
217        self.imgshape = self.viewer.layers["Movie"].data.shape
218        self.imgshape2D = self.imgshape[1:3]
219        self.nframes = self.imgshape[0]
220        return caxis, cval

Load the intensity movie, and get metadata

Parameters
  • imgpath: full path to where the movie file is
def quantiles(self):
223    def quantiles(self):
224        """ Returns the quantiles 1% and 99.999% of the raw image to set the display """
225        return tuple(np.quantile(self.img, [0.01, 0.9999]))

Returns the quantiles 1% and 99.999% of the raw image to set the display

def set_verbose(self, verbose):
227    def set_verbose(self, verbose):
228        """
229        Set verbose level
230        
231        :param: verbose: amount of message that will be displayed in the Terminal console, from 0 (none) to 4 (a lot, for debugging)
232        """
233        self.verbose = verbose
234        self.epi_metadata["Verbose"] = verbose

Set verbose level

Parameters
  • verbose: amount of message that will be displayed in the Terminal console, from 0 (none) to 4 (a lot, for debugging)
def set_gaps_option(self, allow_gap):
236    def set_gaps_option(self, allow_gap):
237        """Set the mode for gap allowing/forbid in tracks
238        
239        :param: allow_gap: boolean. Indicates if gap in tracks (missing cell in one or more frames) should be allowed or not.
240        """
241        self.epi_metadata["Allow gaps"] = allow_gap
242        self.forbid_gaps = not allow_gap

Set the mode for gap allowing/forbid in tracks

Parameters
  • allow_gap: boolean. Indicates if gap in tracks (missing cell in one or more frames) should be allowed or not.
def set_epithelia(self, epithelia):
244    def set_epithelia(self, epithelia):
245        """
246        Set the mode for cell packing (touching or not especially)
247        
248        :param: epithelia: boolean, True if cells are touching
249        """
250        self.epi_metadata["EpithelialCells"] = epithelia

Set the mode for cell packing (touching or not especially)

Parameters
  • epithelia: boolean, True if cells are touching
def set_scalebar(self, show_scalebar):
252    def set_scalebar(self, show_scalebar):
253        """
254        Show or not the scale bar, and set its value
255        
256        :param: show_scalebar: boolean, set the visibility of the scale bar
257        """
258        self.epi_metadata["Scale bar"] = show_scalebar
259        if self.viewer is not None:
260            self.viewer.scale_bar.visible = show_scalebar
261            self.viewer.scale_bar.unit = self.epi_metadata["UnitXY"]
262            for lay in self.viewer.layers:
263                lay.scale = [1, self.epi_metadata["ScaleXY"], self.epi_metadata["ScaleXY"]]
264            self.viewer.reset_view()

Show or not the scale bar, and set its value

Parameters
  • show_scalebar: boolean, set the visibility of the scale bar
def set_scales(self, scalexy, scalet, unitxy, unitt):
266    def set_scales(self, scalexy, scalet, unitxy, unitt):
267        """
268        Set the scaling units for outputs. Put the values in Epicure metadata object
269        
270        :param: scalexy: size of one pixel in X,Y directions
271        :param: scalet: duration of one frame (acquisition frequency)
272        :param: unitxy: name of the unit in which the scale is given
273        :param: unitt: name of the temporal unit in which the scale is given
274        """
275        self.epi_metadata["ScaleXY"] = scalexy
276        self.epi_metadata["ScaleT"] = scalet
277        self.epi_metadata["UnitXY"] = unitxy
278        self.epi_metadata["UnitT"] = unitt
279        if self.viewer is not None:
280            self.viewer
281        if self.verbose > 0:
282            ut.show_info("Movie scales set to " + str(self.epi_metadata["ScaleXY"]) + " " + self.epi_metadata["UnitXY"] + " and " + str(self.epi_metadata["ScaleT"]) + " " + self.epi_metadata["UnitT"])

Set the scaling units for outputs. Put the values in Epicure metadata object

Parameters
  • scalexy: size of one pixel in X,Y directions
  • scalet: duration of one frame (acquisition frequency)
  • unitxy: name of the unit in which the scale is given
  • unitt: name of the temporal unit in which the scale is given
def set_chanel(self, chan, chanaxis):
284    def set_chanel(self, chan, chanaxis):
285        """
286        Update the movie to the correct chanel
287        
288        :param: chan: channel in which the raw movie is 
289        :param: chanaxis: in which axis is the color channels information (usually format is TCYX, so will be 1)
290        """
291        self.img = np.rollaxis(np.copy(self.mov), chanaxis, 0)[chan]
292        if len(self.img.shape) == 2:
293            self.img = np.expand_dims(self.img, axis=0)
294            ## udpate the image shape informations
295            self.imgshape = self.img.shape
296            self.imgshape2D = self.imgshape[1:3]
297            self.nframes = self.imgshape[0]
298        self.main_channel = chan
299        if self.viewer is not None:
300            mview = self.viewer.layers["Movie"]
301            mview.data = self.img
302            mview.contrast_limits = self.quantiles()
303            mview.gamma = 0.95
304            mview.refresh()

Update the movie to the correct chanel

Parameters
  • chan: channel in which the raw movie is
  • chanaxis: in which axis is the color channels information (usually format is TCYX, so will be 1)
def add_other_chanels(self, chan, chanaxis):
306    def add_other_chanels(self, chan, chanaxis): 
307        """ Open other channels if option selected """
308        others_raw = np.delete(self.mov, chan, axis=chanaxis)
309        self.others = []
310        self.others_chanlist = []
311        if self.others is not None:
312            others_raw = np.rollaxis(others_raw, chanaxis, 0)
313            for ochan in range(others_raw.shape[0]):
314                purechan = ochan
315                if purechan >= chan:
316                    purechan = purechan + 1
317                self.others_chanlist.append(purechan)
318                if len(others_raw[ochan].shape) == 2:
319                    expanded = np.expand_dims(others_raw[ochan], axis=0)
320                    self.others.append( expanded )
321                else:
322                    self.others.append( others_raw[ochan] )
323                mview = self.viewer.add_image( self.others[ochan], name="MovieChannel_"+str(purechan), blending="additive", colormap="gray" )
324                mview.contrast_limits=tuple(np.quantile(self.others[ochan],[0.01, 0.9999]))
325                mview.gamma=0.95
326                mview.visible = False

Open other channels if option selected

def import_geff(self, segpath, verbose=0):
328    def import_geff(self, segpath, verbose=0):
329        """ Load segmentation and tracks from GEFF file """
330        if verbose > 1:
331            print("Importing segmentation and tracks from GEFF file")
332        import epicure.geff_import as geffy
333        tracks, graph, metadata, labels_path = geffy.import_geff( segpath )
334        self.epi_metadata["Import"] = "GEFF"  ## initially came from a GEFF file
335        ## copy the metadata loaded from the GEFF file to the Epicure metadata
336        if metadata is not {}:
337            for key, val in metadata.items():
338                self.epi_metadata[key] = val
339        return labels_path, graph, tracks

Load segmentation and tracks from GEFF file

def import_trackmate(self, segpath, verbose=0):
341    def import_trackmate(self, segpath, verbose=0):
342        """ Load segmentation and tracks from TrackMate XML file """
343        if verbose > 1:
344            print("Importing segmentation and tracks from TrackMate XML file")
345        np.set_printoptions(suppress=True, floatmode="maxprec_equal")
346
347        img_data_tag = tm._get_ImageData_tag(segpath)
348        metadata = tm._get_metadata(img_data_tag)
349        seg_shape = (int(metadata["nframes"]), int(metadata["height"]), int(metadata["width"]))
350        segmentation = np.zeros(seg_shape, dtype=np.uint16)-1
351        positions, tracks = tm._parse_Model_tag(segpath, metadata, segmentation)
352        label_mapping = tm._build_label_mapping(positions, tracks)
353        positions = tm.relabel_positions(label_mapping, positions)
354        tracks = tm.relabel_tracks(label_mapping, tracks)
355        segmentation = tm.relabel_segmentation(label_mapping, segmentation)
356        self.epi_metadata["Import"] = "TrackMate"  ## initially came from a TrackMate file
357        return segmentation, tracks

Load segmentation and tracks from TrackMate XML file

def load_segmentation(self, seg_input):
360    def load_segmentation(self, seg_input):
361        """Load the segmentation file"""
362        start_time = ut.start_time()
363        self.graph = None ## no loaded graph
364        track_table = None ## no loaded track data
365        ## compatibility to string input, the path to the image or a dictionnary
366        if isinstance(seg_input, dict):
367            segpath = seg_input["File"]
368        else:
369            segpath = seg_input
370        self.epi_metadata["SegmentationFile"] = segpath
371        if isinstance(seg_input, dict) and "Layer" in seg_input:
372            ## take the segmentation data and close it
373            self.seg = seg_input["Layer"].data
374            ut.remove_layer(self.viewer, seg_input["Layer"])
375        else:
376            if str(segpath).endswith(".xml"):
377                ## import a TrackMate file
378                self.seg, self.graph = self.import_trackmate(segpath, verbose=self.verbose>1)
379            elif str(segpath).endswith(".geff"):
380                ## import a GEFF file
381                label_path, self.graph, track_table = self.import_geff(segpath, verbose=self.verbose>1)
382                if label_path is not None:
383                    self.seg, _, _, _, _, _ = ut.open_image( label_path, get_metadata=False, verbose=self.verbose > 1)
384                else:
385                    ut.show_error( "No labelled movie found in the GEFF file. This case is not yet handled by EpiCure. Please raise an issue in the github so that we add it." )
386                    return
387            else:
388                self.seg, _, _, _, _, _ = ut.open_image(segpath, get_metadata=False, verbose=self.verbose > 1)
389        self.seg = np.uint32(self.seg)
390        ## transform static image to movie (add temporal dimension)
391        if len(self.seg.shape) == 2:
392            self.seg = np.expand_dims(self.seg, axis=0)
393        ## ensure that the shapes are correctly set
394        self.imgshape = self.seg.shape
395        self.imgshape2D = self.seg.shape[1:3]
396        self.nframes = self.seg.shape[0]
397        ## if the segmentation is a junction file, transform it to a label image
398        if ut.is_binary(self.seg):
399            self.junctions_to_label()
400            self.tracked = 0
401        else:
402            self.has_been_tracked()
403            self.prepare_labels()
404
405        ## define a reference size of the movie to scale default parameters
406        self.reference_size = np.max(self.imgshape2D)
407        self.epi_metadata["Reloading"] = True  ## has been formatted to EpiCure format
408
409        # display the segmentation file movie
410        if self.viewer is not None:
411            if "Movie" in self.viewer.layers:
412                scale = self.viewer.layers["Movie"].scale
413            else:
414                scale = (1,1,1)
415            self.seglayer = self.viewer.add_labels(self.seg, name="Segmentation", blending="additive", opacity=0.5, scale=scale)
416            self.viewer.dims.set_point(0, 0)
417            self.seglayer.brush_size = 4  ## default label pencil drawing size
418        
419        if self.verbose > 0:
420            ut.show_duration(start_time, header="Segmentation loaded in ")
421        
422        return track_table

Load the segmentation file

def load_tracks(self, track_table, progress_bar):
425    def load_tracks(self, track_table, progress_bar):
426        """From the segmentation, get all the metadata"""
427        tracked = "tracked"
428        self.tracking.init_tracks( track_table )
429        if self.tracked == 0:
430            tracked = "untracked"
431        else:
432            if self.graph is not None:
433                self.tracking.set_graph(self.graph)
434            if self.forbid_gaps:
435                progress_bar.set_description("check and fix track gaps")
436                self.handle_gaps(track_list=None, verbose=1)
437        ut.show_info("" + str(len(self.tracking.get_track_list())) + " " + tracked + " cells loaded")

From the segmentation, get all the metadata

def has_been_tracked(self):
439    def has_been_tracked(self):
440        """Look if has been tracked already (some labels are in several frames)"""
441        nb = 0
442        for frame in range(self.seg.shape[0]):
443            if frame > 0:
444                inter = np.intersect1d(np.unique(self.seg[frame - 1]), np.unique(self.seg[frame]))
445                if len(inter) > 1:
446                    self.tracked = 1
447                    return
448        self.tracked = 0
449        return

Look if has been tracked already (some labels are in several frames)

def suggest_segfile(self, outdir):
451    def suggest_segfile(self, outdir):
452        """Check if a segmentation file from EpiCure already exists"""
453        if (self.epi_metadata["SegmentationFile"] != "") and ut.found_segfile(self.epi_metadata["SegmentationFile"]):
454            return self.epi_metadata["SegmentationFile"]
455        imgname, imgdir, out = ut.extract_names(self.epi_metadata["MovieFile"], outdir, mkdir=False)
456        return ut.suggest_segfile(out, imgname)

Check if a segmentation file from EpiCure already exists

def outname(self):
458    def outname(self):
459        return os.path.join(self.outdir, self.imgname)
def set_names(self, outdir):
461    def set_names(self, outdir):
462        """Extract default names from imgpath"""
463        self.imgname, self.imgdir, self.outdir = ut.extract_names(self.epi_metadata["MovieFile"], outdir, mkdir=True)

Extract default names from imgpath

def go_epicure(self, outdir='epics', segmentation_input=None):
465    def go_epicure(self, outdir="epics", segmentation_input=None):
466        """Initialize everything and start the main widget"""
467        self.set_names(outdir)
468        if segmentation_input is None:
469            segmentation_input = {}
470            segmentation_input["File"] = self.suggest_segfile(outdir)
471        self.viewer.window._status_bar._toggle_activity_dock(True)
472        progress_bar = progress(total=5)
473        progress_bar.set_description("Reading segmented image")
474        ## load the segmentation
475        track_table = self.load_segmentation( segmentation_input )
476        if isinstance(segmentation_input, dict):
477            self.epi_metadata["SegmentationFile"] = segmentation_input["File"]
478        else:
479            self.epi_metadata["SegmentationFile"] = segmentation_input
480        progress_bar.update(1)
481        ut.set_active_layer(self.viewer, "Segmentation")
482
483        ## setup the main interface and shortcuts
484        start_time = ut.start_time()
485        progress_bar.set_description("Active EpiCure shortcuts")
486        self.key_bindings()
487        progress_bar.update(2)
488        progress_bar.set_description("Prepare widget")
489        self.main_widget()
490        progress_bar.update(3)
491        progress_bar.set_description("Load tracks")
492        self.load_tracks( track_table, progress_bar)
493        progress_bar.update(4)
494
495        ## load graph if it exists
496        epiname = os.path.join(self.outdir, self.imgname + "_epidata.pkl")
497        if os.path.exists(epiname):
498            progress_bar.set_description("Load EpiCure informations")
499            self.load_epicure_data(epiname)
500        if self.verbose > 0:
501            ut.show_duration(start_time, header="Tracks and graph loaded in ")
502        progress_bar.update(5)
503        self.apply_settings()
504        progress_bar.close()
505        self.viewer.window._status_bar._toggle_activity_dock(False)

Initialize everything and start the main widget

def apply_settings(self):
508    def apply_settings(self):
509        """Apply all default or prefered settings"""
510        for sety, val in self.settings.items():
511            if sety == "Display":
512                self.display.apply_settings(val)
513                if "Show help" in val:
514                    index = int(val["Show help"])
515                    self.switchOverlayText(index)
516                if "Contour" in val:
517                    contour = int(val["Contour"])
518                    self.seglayer.contour = contour
519                    self.seglayer.refresh()
520                if "Colors" in val:
521                    color = val["Colors"]["button"]
522                    check_color = val["Colors"]["checkbox"]
523                    line_edit_color = val["Colors"]["line edit"]
524                    group_color = val["Colors"]["group"]
525                    self.main_gui.setStyleSheet(
526                        "QPushButton {background-color: "
527                        + color
528                        + "} QCheckBox::indicator {background-color: "
529                        + check_color
530                        + "} QLineEdit {background-color: "
531                        + line_edit_color
532                        + "} QGroupBox {color: grey; background-color: "
533                        + group_color
534                        + "} "
535                    )
536                    self.display_colors = val["Colors"]
537            if sety == "events":
538                self.inspecting.apply_settings(val)
539            if sety == "Output":
540                self.outputing.apply_settings(val)
541            if sety == "Track":
542                self.tracking.apply_settings(val)
543            if sety == "Edit":
544                self.editing.apply_settings(val)
545            # case _:
546            #       continue
547            ## match is not compatible with python 3.9

Apply all default or prefered settings

def update_settings(self):
549    def update_settings(self):
550        """Returns all the prefered settings"""
551        disp = self.settings
552        ## load display current settings (layers visibility)
553        disp["Display"] = self.display.get_current_settings()
554        disp["Display"]["Show help"] = self.help_index
555        disp["Display"]["Contour"] = self.seglayer.contour
556        ## load suspect current settings
557        disp["events"] = self.inspecting.get_current_settings()
558        ## get outputs current settings
559        disp["Output"] = self.outputing.get_current_settings()
560        disp["Track"] = self.tracking.get_current_settings()
561        disp["Edit"] = self.editing.get_current_settings()

Returns all the prefered settings

def main_widget(self):
565    def main_widget(self):
566        """Open the main widget interface"""
567        self.main_gui = QWidget()
568
569        layout = QVBoxLayout()
570        tabs = QTabWidget()
571        tabs.setObjectName("main")
572        layout.addWidget(tabs)
573        self.main_gui.setLayout(layout)
574
575        self.editing = Editing(self.viewer, self)
576        tabs.addTab(self.editing, "Edit")
577        self.inspecting = Inspecting(self.viewer, self)
578        tabs.addTab(self.inspecting, "Inspect")
579        self.tracking = Tracking(self.viewer, self)
580        tabs.addTab(self.tracking, "Track")
581        self.outputing = Outputing(self.viewer, self)
582        tabs.addTab(self.outputing, "Output")
583        self.display = Displaying(self.viewer, self)
584        tabs.addTab(self.display, "Display")
585        self.main_gui.setStyleSheet("QPushButton {background-color: rgb(40, 60, 75)} QCheckBox::indicator {background-color: rgb(40,52,65)}")
586
587        self.viewer.window.add_dock_widget(self.main_gui, name="Main")

Open the main widget interface

def key_bindings(self):
589    def key_bindings(self):
590        """Activate shortcuts"""
591        self.text = "-------------- ShortCuts -------------- \n "
592        self.text += "!! Shortcuts work if Segmentation layer is active !! \n"
593        # for sctype, scvals in self.shortcuts.items():
594        self.text += "\n---" + "General" + " options---\n"
595        sg = self.shortcuts["General"]
596        self.text += ut.print_shortcuts(sg)
597        self.text = self.text + "\n"
598
599        if self.verbose > 0:
600            print("Activating key shortcuts on segmentation layer")
601            print("Press <" + str(sg["show help"]["key"]) + "> to show/hide the main shortcuts")
602            print("Press <" + str(sg["show all"]["key"]) + "> to show ALL shortcuts")
603        ut.setOverlayText(self.viewer, self.text, size=12)
604
605        @self.seglayer.bind_key(sg["show help"]["key"], overwrite=True)
606        def switch_shortcuts(seglayer):
607            # index = (self.help_index+1)%(len(self.overtext.keys())+1)
608            # self.switchOverlayText(index)
609            index = (self.help_index + 1) % 2
610            self.switchOverlayText(index)
611
612        @self.seglayer.bind_key(sg["show all"]["key"], overwrite=True)
613        def list_all_shortcuts(seglayer):
614            self.switchOverlayText(0)  ## hide display message in main window
615            text = "**************** EPICURE *********************** \n"
616            text += "\n"
617            text += self.text
618            text += "\n"
619            text += ut.napari_shortcuts()
620            for key, val in self.overtext.items():
621                text += "\n"
622                text += val
623            self.update_text_window(text)
624
625        @self.seglayer.bind_key(sg["save segmentation"]["key"], overwrite=True)
626        def save_seglayer(seglayer):
627            self.save_epicures()
628
629        @self.viewer.bind_key(sg["save movie"]["key"], overwrite=True)
630        def save_movie(seglayer):
631            endname = "_frames.tif"
632            outname = os.path.join(self.outdir, self.imgname + endname)
633            self.save_movie(outname)

Activate shortcuts

def switchOverlayText(self, index):
637    def switchOverlayText(self, index):
638        """Switch overlay display text to index"""
639        self.help_index = index
640        if index == 0:
641            ut.showOverlayText(self.viewer, vis=False)
642            return
643        else:
644            ut.showOverlayText(self.viewer, vis=True)
645        # self.setCurrentOverlayText()
646        self.setGeneralOverlayText()

Switch overlay display text to index

def init_text_window(self):
648    def init_text_window(self):
649        """Creates and opens a pop-up window with shortcut list"""
650        self.blabla = ut.create_text_window("EpiCure shortcuts")

Creates and opens a pop-up window with shortcut list

def update_text_window(self, message):
652    def update_text_window(self, message):
653        """Update message in separate window"""
654        self.init_text_window()
655        self.blabla.value = message

Update message in separate window

def setGeneralOverlayText(self):
657    def setGeneralOverlayText(self):
658        """set overlay help message to general message"""
659        text = self.text
660        ut.setOverlayText(self.viewer, text, size=12)

set overlay help message to general message

def setCurrentOverlayText(self):
662    def setCurrentOverlayText(self):
663        """Set overlay help text message to current selected options list"""
664        text = self.text
665        dispkey = list(self.overtext.keys())[self.help_index - 1]
666        text += self.overtext[dispkey]
667        ut.setOverlayText(self.viewer, text, size=12)

Set overlay help text message to current selected options list

def get_summary(self):
669    def get_summary(self):
670        """Get a summary of the infos of the movie"""
671        summ = "----------- EpiCure summary ----------- \n"
672        summ += "--- Image infos \n"
673        summ += "Movie name: " + str(self.epi_metadata["MovieFile"]) + "\n"
674        summ += "Movie size (x,y): " + str(self.imgshape2D) + "\n"
675        if self.nframes is not None:
676            summ += "Nb frames: " + str(self.nframes) + "\n"
677        summ += "\n"
678        summ += "--- Segmentation infos \n"
679        summ += "Segmentation file: " + str(self.epi_metadata["SegmentationFile"]) + "\n"
680        summ += "Nb tracks: " + str(len(self.tracking.get_track_list())) + "\n"
681        tracked = "yes"
682        if self.tracked == 0:
683            tracked = "no"
684        summ += "Tracked: " + tracked + "\n"
685        nb_labels, mean_duration, mean_area = ut.summary_labels(self.seg)
686        summ += "Nb cells: " + str(nb_labels) + "\n"
687        summ += "Average track lengths: " + str(mean_duration) + " frames\n"
688        summ += "Average cell area: " + str(mean_area) + " pixels^2\n"
689        summ += "Nb suspect events: " + str(self.inspecting.nb_events(only_suspect=True)) + "\n"
690        summ += "Nb divisions: " + str(self.nb_divisions()) + "\n"
691        summ += "Nb extrusions: " + str(self.inspecting.nb_type("extrusion")) + "\n"
692        summ += "\n"
693        summ += "--- Parameter infos \n"
694        summ += "Junction thickness: " + str(self.thickness) + "\n"
695        return summ

Get a summary of the infos of the movie

def nb_divisions(self):
697    def nb_divisions(self):
698        """ Return the number of divisions """
699        return self.inspecting.nb_type("division")

Return the number of divisions

def set_contour(self, width):
701    def set_contour(self, width):
702        """ 
703        Set the width of the contour of the cells to display the segmentation
704
705        :param: width: width of the contours of the segmentation (napari contour parameter). If 0 the cell will be filled by its label 
706        """
707        self.seglayer.contour = width

Set the width of the contour of the cells to display the segmentation

Parameters
  • width: width of the contours of the segmentation (napari contour parameter). If 0 the cell will be filled by its label
def check_layers(self):
711    def check_layers(self):
712        """Check that the necessary layers are present"""
713        if self.editing.shapelayer_name not in self.viewer.layers:
714            if self.verbose > 0:
715                print("Reput shape layer")
716            self.editing.create_shapelayer()
717        if self.inspecting.eventlayer_name not in self.viewer.layers:
718            if self.verbose > 0:
719                print("Reput event layer")
720            self.inspecting.create_eventlayer()
721        if "Movie" not in self.viewer.layers:
722            if self.verbose > 0:
723                print("Reput movie layer")
724            mview = self.viewer.add_image(self.img, name="Movie", blending="additive", colormap="gray", scale=[1, self.epi_metadata["ScaleXY"], self.epi_metadata["ScaleXY"]])
725            # mview.reset_contrast_limits()
726            mview.contrast_limits = self.quantiles()
727            mview.gamma = 0.95
728        if "Segmentation" not in self.viewer.layers:
729            if self.verbose > 0:
730                print("Reput segmentation")
731            self.seglayer = self.viewer.add_labels(self.seg, name="Segmentation", blending="additive", opacity=0.5, scale=self.viewer.layers["Movie"].scale)
732
733        self.finish_update()

Check that the necessary layers are present

def finish_update(self, contour=None):
735    def finish_update(self, contour=None):
736        """
737        After doing modifications on some layer(s), select back the main layer Segmentation as active (important for shortcut bindings) and refresh it
738        """
739        if contour is not None:
740            self.seglayer.contour = contour
741        ut.set_active_layer(self.viewer, "Segmentation")
742        self.seglayer.refresh()
743        duplayers = ["PrevSegmentation"]
744        for dlay in duplayers:
745            if dlay in self.viewer.layers:
746                (self.viewer.layers[dlay]).refresh()

After doing modifications on some layer(s), select back the main layer Segmentation as active (important for shortcut bindings) and refresh it

def read_epicure_metadata(self):
748    def read_epicure_metadata(self):
749        """Load saved infos from file"""
750        epiname = self.outname() + "_epidata.pkl"
751        if os.path.exists(epiname):
752            infile = open(epiname, "rb")
753            try:
754                epidata = pickle.load(infile)
755                if "EpiMetaData" in epidata.keys():
756                    for key, vals in epidata["EpiMetaData"].items():
757                        self.epi_metadata[key] = vals
758                infile.close()
759            except:
760                ut.show_warning("Could not read EpiCure metadata file " + epiname)

Load saved infos from file

def save_epicures(self, imtype='float32'):
762    def save_epicures(self, imtype="float32"):
763        """
764        Save all the current data: the segmentation, the metadata (metadata of the image, last parameters used), the events and some display settings.
765        """
766        outname = os.path.join(self.outdir, self.imgname + "_labels.tif")
767        ut.writeTif(self.seg, outname, self.epi_metadata["ScaleXY"], imtype, what="Segmentation")
768        epiname = os.path.join(self.outdir, self.imgname + "_epidata.pkl")
769        outfile = open(epiname, "wb")
770        self.epi_metadata["MainChannel"] = self.main_channel 
771        epidata = {}
772        epidata["EpiMetaData"] = self.epi_metadata
773        if self.groups is not None:
774            epidata["Group"] = self.groups
775        if self.tracking.graph is not None:
776            epidata["Graph"] = self.tracking.graph
777        if self.inspecting is not None and self.inspecting.events is not None:
778            epidata["Events"] = {}
779            if self.inspecting.events.data is not None:
780                epidata["Events"]["Points"] = self.inspecting.events.data
781                epidata["Events"]["Props"] = self.inspecting.events.properties
782                epidata["Events"]["Types"] = self.inspecting.event_types
783                # epidata["Events"]["Symbols"] = self.inspecting.events.symbol
784                # epidata["Events"]["Colors"] = self.inspecting.events.face_color
785        if "Movie" in self.viewer.layers:
786            ## to keep movie layer display settings for this file
787            epidata["Display"] = {}
788            epidata["Display"]["MovieContrast"] = self.viewer.layers["Movie"].contrast_limits
789        pickle.dump(epidata, outfile)
790        outfile.close()

Save all the current data: the segmentation, the metadata (metadata of the image, last parameters used), the events and some display settings.

def read_group_data(self, groups):
792    def read_group_data(self, groups):
793        """Read the group EpiCure data from opened file"""
794        if self.verbose > 0:
795            print("Loaded cell groups info: " + str(list(groups.keys())))
796            if self.verbose > 2:
797                print("Cell groups: " + str(groups))
798        return groups

Read the group EpiCure data from opened file

def read_graph_data(self, infile):
800    def read_graph_data(self, infile):
801        """
802        Read the graph EpiCure data from opened pickle file
803
804        :param: infile: instance of pickle file being read. This will read the next part of the pickle file and load it in the track graph.
805        """
806        try:
807            graph = pickle.load(infile)
808            if self.verbose > 0:
809                print("Graph (lineage) loaded")
810            return graph
811        except:
812            if self.verbose > 1:
813                print("No graph infos found")
814            return None

Read the graph EpiCure data from opened pickle file

Parameters
  • infile: instance of pickle file being read. This will read the next part of the pickle file and load it in the track graph.
def read_events_data(self, infile):
816    def read_events_data(self, infile):
817        """Read info of EpiCure events (suspects, divisions) from opened file"""
818        try:
819            events_pts = pickle.load(infile)
820            if events_pts is not None:
821                events_props = pickle.load(infile)
822                events_type = pickle.load(infile)
823                try:
824                    symbols = pickle.load(infile)
825                    colors = pickle.load(infile)
826                except:
827                    if self.verbose > 1:
828                        print("No events display info found")
829                    symbols = None
830                    colors = None
831                return events_pts, events_props, events_type
832            else:
833                return None, None, None
834        except:
835            if self.verbose > 1:
836                print("events info not complete")
837            return None, None, None

Read info of EpiCure events (suspects, divisions) from opened file

def load_epicure_data(self, epiname):
839    def load_epicure_data(self, epiname):
840        """Load saved infos from file"""
841        infile = open(epiname, "rb")
842        try:
843            if ut.is_windows():
844               import pathlib
845               pathlib.PosixPath = pathlib.WindowsPath
846               #epidata = pickle.load( infile, encoding="utf8" )
847            epidata = pickle.load( infile )
848            #print(epidata)
849            if "EpiMetaData" in epidata.keys():
850                # version of epicure file after Epicure 0.2.0
851                self.read_epidata(epidata)
852                infile.close()
853            else:
854                # version anterior of Epicure 0.2.0
855                self.load_epicure_data_old(epidata, infile)
856        except Exception as e:
857            if self.verbose > 1:
858                print(f" {type(e)} {e} - Could not read EpiCure data file {epiname}")
859            else:
860                ut.show_warning(f"Could not read EpiCure data file {epiname}")
861                print(f" {type(e)} {e} - Could not read EpiCure data file {epiname}")

Load saved infos from file

def read_epidata(self, epidata):
863    def read_epidata(self, epidata):
864        """Read the dict of saved state and initialize all instances with it"""
865        for key, vals in epidata.items():
866            if key == "EpiMetaData":
867                ## image data is read on the previous step
868                continue
869            if key == "Group":
870                ## Load groups information
871                self.groups = self.read_group_data(vals)
872                self.update_group_lists()
873            if key == "Graph":
874                ## Load graph (lineage) informations
875                self.tracking.graph = vals
876                if self.tracking.graph is not None:
877                    self.tracking.tracklayer.refresh()
878                if self.verbose > 2:
879                    print(f"Loaded track graph: {self.tracking.graph}")
880            if key == "Events":
881                ## Load events information
882                if "Points" in vals.keys():
883                    pts = vals["Points"]
884                if "Props" in vals.keys():
885                    props = vals["Props"]
886                if "Types" in vals.keys():
887                    event_types = vals["Types"]
888                # if "Symbols" in vals.keys():
889                #    symbols = vals["Symbols"]
890                # if "Colors" in vals.keys():
891                #    colors = vals["Colors"]
892                if pts is not None:
893                    if len(pts) > 0:
894                        self.inspecting.load_events(pts, props, event_types)
895                    if len(pts) > 0 and self.verbose > 0:
896                        print("events loaded")
897                    ut.show_info("Loaded " + str(len(pts)) + " events")
898            if key == "Display":
899                if vals is not None:
900                    ## load display setting
901                    if "MovieContrast" in vals.keys():
902                        self.viewer.layers["Movie"].contrast_limits = vals["MovieContrast"]

Read the dict of saved state and initialize all instances with it

def load_epicure_data_old(self, groups, infile):
904    def load_epicure_data_old(self, groups, infile):
905        """Load saved infos from file"""
906        ## Load groups information
907        self.groups = self.read_group_data(groups)
908        for group in self.groups.keys():
909            self.editing.update_group_list(group)
910        self.outputing.update_selection_list()
911        ## Load graph (lineage) informations
912        self.tracking.graph = self.read_graph_data(infile)
913        if self.tracking.graph is not None:
914            self.tracking.tracklayer.refresh()
915        ## Load events information
916        pts, props, event_types = self.read_events_data(infile)
917        if pts is not None:
918            if len(pts) > 0:
919                self.inspecting.load_events(pts, props, event_types)
920                if len(pts) > 0 and self.verbose > 0:
921                    print("events loaded")
922                    ut.show_info("Loaded " + str(len(pts)) + " events")
923        infile.close()

Load saved infos from file

def save_movie(self, outname):
925    def save_movie(self, outname):
926        """Save movie with current display parameters, except zoom"""
927        save_view = self.viewer.camera.copy()
928        save_frame = ut.current_frame(self.viewer)
929        ## place the view to see the whole image
930        self.viewer.reset_view()
931        # self.viewer.camera.zoom = 1
932        sizex = (self.imgshape2D[0] * self.viewer.camera.zoom) / 2
933        sizey = (self.imgshape2D[1] * self.viewer.camera.zoom) / 2
934        if os.path.exists(outname):
935            os.remove(outname)
936
937        ## take a screenshot of each frame
938        for frame in range(self.nframes):
939            self.viewer.dims.set_point(0, frame)
940            shot = self.viewer.window.screenshot(canvas_only=True, flash=False)
941            ## remove border: movie is at the center
942            centx = int(shot.shape[0] / 2) + 1
943            centy = int(shot.shape[1] / 2) + 1
944            shot = shot[
945                int(centx - sizex) : int(centx + sizex),
946                int(centy - sizey) : int(centy + sizey),
947            ]
948            ut.appendToTif(shot, outname)
949        self.viewer.camera.update(save_view)
950        if save_frame is not None:
951            self.viewer.dims.set_point(0, save_frame)
952        ut.show_info("Movie " + outname + " saved")

Save movie with current display parameters, except zoom

def reset_data(self):
954    def reset_data(self):
955        """Reset EpiCure data (group, suspect, graph)"""
956        self.inspecting.reset_all_events()
957        self.reset_groups()
958        self.tracking.graph = None

Reset EpiCure data (group, suspect, graph)

def junctions_to_label(self):
960    def junctions_to_label(self):
961        """convert epyseg/skeleton result (junctions) to labels map"""
962        ## ensure that skeleton is thin enough
963        for z in range(self.seg.shape[0]):
964            self.skel_one_frame(z)
965        self.seg = ut.reset_labels(self.seg, closing=True)

convert epyseg/skeleton result (junctions) to labels map

def skel_one_frame(self, z):
967    def skel_one_frame(self, z):
968        """From segmentation of junctions of one frame, get it as a correct skeleton"""
969        skel = skeletonize(self.seg[z] / np.max(self.seg[z]))
970        skel = ut.copy_border(skel, self.seg[z])
971        self.seg[z] = np.invert(skel)

From segmentation of junctions of one frame, get it as a correct skeleton

def reset_labels(self):
973    def reset_labels(self):
974        """Reset all labels, ensure unicity"""
975        if self.epi_metadata["EpithelialCells"]:
976            ### packed (contiguous cells), ensure that they are separated by one pixel only
977            skel = self.get_skeleton()
978            skel = np.uint32(skel)
979            self.seg = skel
980            self.seglayer.data = skel
981            self.junctions_to_label()
982            self.seglayer.data = self.seg
983        else:
984            self.get_cells()

Reset all labels, ensure unicity

def check_extrusions_sanity(self):
 986    def check_extrusions_sanity(self):
 987        """Check that extrusions seem to be correct (last of tracks )"""
 988        extrusions = self.inspecting.get_events_from_type("extrusion")
 989        nrem = 0
 990        if (extrusions is not None) and (extrusions != []):
 991            for extr_id in extrusions:
 992                pos, label = self.inspecting.get_event_infos(extr_id)
 993                last_frame = self.tracking.get_last_frame(label)
 994                if pos[0] != last_frame:
 995                    if self.verbose > 1:
 996                        print("Extrusion " + str(extr_id) + " at frame " + str(pos[0]) + " not at the end of track " + str(label))
 997                        print("Removing it")
 998                    self.inspecting.remove_one_event(extr_id)
 999                    nrem = nrem + 1
1000            print("Removed " + str(nrem) + " extrusions that dit not correspond to the end of tracks")

Check that extrusions seem to be correct (last of tracks )

def prepare_labels(self):
1002    def prepare_labels(self):
1003        """Process the labels to be in a correct Epicurable format"""
1004        if self.epi_metadata["EpithelialCells"]:
1005            if self.epi_metadata["Reloading"]:
1006                ## if opening an already EpiCured movie, assume it's in correct format
1007                return
1008            ### packed (contiguous cells), ensure that they are separated by one pixel only
1009            self.thin_boundaries()
1010        else:
1011            self.get_cells()

Process the labels to be in a correct Epicurable format

def get_cells(self):
1013    def get_cells(self):
1014        """Non jointive cells: check label unicity"""
1015        for frame in self.seg:
1016            if ut.non_unique_labels(frame):
1017                self.seg = ut.reset_labels(self.seg, closing=True)
1018                return

Non jointive cells: check label unicity

def thin_boundaries(self):
1020    def thin_boundaries(self):
1021        """ " Assure that all boundaries are only 1 pixel thick"""
1022        if self.process_parallel:
1023            self.seg = Parallel(n_jobs=self.nparallel)(delayed(ut.thin_seg_one_frame)(zframe) for zframe in self.seg)
1024            self.seg = np.array(self.seg)
1025        else:
1026            for z in range(self.seg.shape[0]):
1027                self.seg[z] = ut.thin_seg_one_frame(self.seg[z])

" Assure that all boundaries are only 1 pixel thick

def add_skeleton(self):
1029    def add_skeleton(self):
1030        """add a layer containing the skeleton movie of the segmentation"""
1031        # display the segmentation file movie
1032        if self.viewer is not None:
1033            skel = np.zeros(self.seg.shape, dtype="uint8")
1034            skel[self.seg == 0] = 1
1035            skel = self.get_skeleton(viewer=self.viewer)
1036            ut.remove_layer(self.viewer, "Skeleton")
1037            skellayer = self.viewer.add_image(skel, name="Skeleton", blending="additive", opacity=1, scale=self.viewer.layers["Movie"].scale)
1038            skellayer.reset_contrast_limits()
1039            skellayer.contrast_limits = (0, 1)

add a layer containing the skeleton movie of the segmentation

def get_skeleton(self, viewer=None):
1041    def get_skeleton(self, viewer=None):
1042        """convert labels movie to skeleton (thin boundaries)"""
1043        if self.seg is None:
1044            return None
1045        parallel = 0
1046        if self.process_parallel:
1047            parallel = self.nparallel
1048        return ut.get_skeleton(self.seg, viewer=viewer, verbose=self.verbose, parallel=parallel)

convert labels movie to skeleton (thin boundaries)

def get_free_labels(self, nlab):
1052    def get_free_labels(self, nlab):
1053        """Get the nlab smallest unused labels"""
1054        used = set(self.tracking.get_track_list())
1055        return ut.get_free_labels(used, nlab)

Get the nlab smallest unused labels

def get_free_label(self):
1057    def get_free_label(self):
1058        """Return the first free label"""
1059        return self.get_free_labels(1)[0]

Return the first free label

def has_label(self, label):
1061    def has_label(self, label):
1062        """Check if label is present in the tracks"""
1063        return self.tracking.has_track(label)

Check if label is present in the tracks

def has_labels(self, labels):
1065    def has_labels(self, labels):
1066        """Check if labels are present in the tracks"""
1067        return self.tracking.has_tracks(labels)

Check if labels are present in the tracks

def nlabels(self):
1069    def nlabels(self):
1070        """Number of unique tracks"""
1071        return self.tracking.nb_tracks()

Number of unique tracks

def get_labels(self):
1073    def get_labels(self):
1074        """Return list of labels in tracks"""
1075        return list(self.tracking.get_track_list())

Return list of labels in tracks

def delete_tracks(self, tracks):
1078    def delete_tracks(self, tracks):
1079        """Remove all the tracks from the Track layer"""
1080        self.tracking.remove_tracks(tracks)

Remove all the tracks from the Track layer

def delete_track(self, label, frame=None):
1082    def delete_track(self, label, frame=None):
1083        """Remove (part of) the track"""
1084        if frame is None:
1085            self.tracking.remove_track(label)
1086        else:
1087            self.tracking.remove_one_frame(label, frame, handle_gaps=self.forbid_gaps)

Remove (part of) the track

def update_centroid(self, label, frame):
1089    def update_centroid(self, label, frame):
1090        """Track label has been change at given frame"""
1091        if label not in self.tracking.has_track(label):
1092            if self.verbose > 1:
1093                print("Track " + str(label) + " not found")
1094            return
1095        self.tracking.update_centroid(label, frame)

Track label has been change at given frame

def get_label_indexes(self, label, start_frame=0):
1098    def get_label_indexes(self, label, start_frame=0):
1099        """Returns the indexes where label is present in segmentation, starting from start_frame"""
1100        indmodif = []
1101        if self.verbose > 2:
1102            start_time = ut.start_time()
1103        pos = self.tracking.get_track_column(track_id=label, column="fullpos")
1104        pos = pos[pos[:, 0] >= start_frame]
1105        ## if nothing in pos, pb with track data
1106        if pos is None or len(pos) == 0:
1107            ut.show_warning("Something wrong in the track data. Resetting track data (can take time)")
1108            self.tracking.reset_tracks()
1109            self.get_label_indexes(label, start_frame)
1110
1111        indmodif = np.argwhere(self.seg[pos[:, 0]] == label)
1112        indmodif = ut.shiftFrames(indmodif, pos[:, 0])
1113        if self.verbose > 2:
1114            ut.show_duration(start_time, header="Label indexes found in ")
1115        return indmodif

Returns the indexes where label is present in segmentation, starting from start_frame

def replace_label(self, label, new_label, start_frame=0):
1117    def replace_label(self, label, new_label, start_frame=0):
1118        """Replace label with new_label from start_frame - Relabelling only"""
1119        indmodif = self.get_label_indexes(label, start_frame)
1120        new_labels = [new_label] * len(indmodif)
1121        self.change_labels(indmodif, new_labels, replacing=True)

Replace label with new_label from start_frame - Relabelling only

def change_labels_frommerge(self, indmodif, new_labels, remove_labels):
1123    def change_labels_frommerge(self, indmodif, new_labels, remove_labels):
1124        """Change the value at pixels indmodif to new_labels and update tracks/graph. Full remove of the two merged labels"""
1125        if len(indmodif) > 0:
1126            ## get effectively changed labels
1127            indmodif, new_labels, _ = ut.setNewLabel(self.seglayer, indmodif, new_labels, add_frame=None, return_old=False)
1128            if len(new_labels) > 0:
1129                self.update_added_labels(indmodif, new_labels)
1130                self.update_removed_labels(indmodif, remove_labels)
1131        self.seglayer.refresh()

Change the value at pixels indmodif to new_labels and update tracks/graph. Full remove of the two merged labels

def change_labels(self, indmodif, new_labels, replacing=False):
1133    def change_labels(self, indmodif, new_labels, replacing=False):
1134        """Change the value at pixels indmodif to new_labels and update tracks/graph
1135
1136        Assume that only label at current frame can have its shape modified. Other changed label is only relabelling at frames > current frame (child propagation)
1137        """
1138        if len(indmodif) > 0:
1139            ## get effectively changed labels
1140            indmodif, new_labels, old_labels = ut.setNewLabel(self.seglayer, indmodif, new_labels, add_frame=None)
1141            if len(new_labels) > 0:
1142                if replacing:
1143                    self.update_replaced_labels(indmodif, new_labels, old_labels)
1144                else:
1145                    ## the only label to change are the current frame (smaller one), the other are only relabelling (propagation)
1146                    cur_frame = np.min(indmodif[0])
1147                    to_reshape = indmodif[0] == cur_frame
1148                    self.update_changed_labels((indmodif[0][to_reshape], indmodif[1][to_reshape], indmodif[2][to_reshape]), new_labels[to_reshape], old_labels[to_reshape])
1149                    to_relab = np.invert(to_reshape)
1150                    self.update_replaced_labels((indmodif[0][to_relab], indmodif[1][to_relab], indmodif[2][to_relab]), new_labels[to_relab], old_labels[to_relab])
1151        self.seglayer.refresh()

Change the value at pixels indmodif to new_labels and update tracks/graph

Assume that only label at current frame can have its shape modified. Other changed label is only relabelling at frames > current frame (child propagation)

def get_mask(self, label, start=None, end=None):
1153    def get_mask(self, label, start=None, end=None):
1154        """Get mask of label from frame start to frame end"""
1155        if (start is None) or (end is None):
1156            start, end = self.tracking.get_extreme_frames(label)
1157        crop = self.seg[start : (end + 1)]
1158        mask = np.isin(crop, [label]) * 1
1159        return mask

Get mask of label from frame start to frame end

def get_label_movie(self, label, extend=1.25):
1161    def get_label_movie(self, label, extend=1.25):
1162        """Get movie centered on label"""
1163        start, end = self.tracking.get_extreme_frames(label)
1164        mask = self.get_mask(label, start, end)
1165        boxes = []
1166        centers = []
1167        max_box = 0
1168        for frame in mask:
1169            props = regionprops(frame)
1170            bbox = props[0].bbox
1171            boxes.append(bbox)
1172            centers.append(props[0].centroid)
1173            for i in range(2):
1174                max_box = max(max_box, bbox[i + 2] - bbox[i])
1175
1176        box_size = int(max_box * extend)
1177        movie = np.zeros((end - start + 1, box_size, box_size))
1178        for i, frame in enumerate(range(start, end + 1)):
1179            xmin = int(centers[i][0] - box_size / 2)
1180            xminshift = 0
1181            if xmin < 0:
1182                xminshift = -xmin
1183                xmin = 0
1184            xmax = xmin + box_size - xminshift
1185            xmaxshift = box_size
1186            if xmax > self.imgshape2D[0]:
1187                xmaxshift = self.imgshape2D[0] - xmax
1188                xmax = self.imgshape2D[0]
1189
1190            ymin = int(centers[i][1] - max_box / 2)
1191            yminshift = 0
1192            if ymin < 0:
1193                yminshift = -ymin
1194                ymin = 0
1195            ymax = ymin + box_size - yminshift
1196            ymaxshift = box_size
1197            if ymax > self.imgshape2D[1]:
1198                ymaxshift = self.imgshape2D[1] - ymax
1199                ymax = self.imgshape2D[1]
1200
1201            movie[i, xminshift:xmaxshift, yminshift:ymaxshift] = self.img[frame, xmin:xmax, ymin:ymax]
1202        return movie

Get movie centered on label

def cell_radius(self, label, frame):
1205    def cell_radius(self, label, frame):
1206        """Approximate the cell radius at given frame"""
1207        area = np.sum(self.seg[frame] == label)
1208        radius = math.sqrt(area / math.pi)
1209        return radius

Approximate the cell radius at given frame

def cell_area(self, label, frame):
1211    def cell_area(self, label, frame):
1212        """Approximate the cell radius at given frame"""
1213        area = np.sum(self.seg[frame] == label)
1214        return area

Approximate the cell radius at given frame

def cell_on_border(self, label, frame):
1216    def cell_on_border(self, label, frame):
1217        """Check if a given cell is on border of the image"""
1218        bbox = ut.getBBox2D(self.seg[frame], label)
1219        out = ut.outerBBox2D(bbox, self.imgshape2D, margin=3)
1220        return out

Check if a given cell is on border of the image

def add_label(self, labels, frame=None):
1223    def add_label(self, labels, frame=None):
1224        """Add a label to the tracks"""
1225        if frame is not None:
1226            if np.isscalar(labels):
1227                labels = [labels]
1228            self.tracking.add_one_frame(labels, frame, refresh=True)
1229        else:
1230            if self.verbose > 1:
1231                print("TODO add label no frame")

Add a label to the tracks

def add_one_label_to_track(self, label):
1233    def add_one_label_to_track(self, label):
1234        """Add the track data of a given label if missing"""
1235        iframe = 0
1236        while (iframe < self.nframes) and (label not in self.seg[iframe]):
1237            iframe = iframe + 1
1238        while (iframe < self.nframes) and (label in self.seg[iframe]):
1239            self.tracking.add_one_frame([label], iframe)
1240            iframe = iframe + 1

Add the track data of a given label if missing

def update_label(self, label, frame):
1242    def update_label(self, label, frame):
1243        """Update the given label at given frame"""
1244        self.tracking.update_track_on_frame([label], frame)

Update the given label at given frame

def update_changed_labels(self, indmodif, new_labels, old_labels, full=False):
1246    def update_changed_labels(self, indmodif, new_labels, old_labels, full=False):
1247        """Check what had been modified, and update tracks from it, looking frame by frame"""
1248        ## check all the old_labels if still present or not
1249        if self.verbose > 1:
1250            start_time = time.time()
1251        frames = np.unique(indmodif[0])
1252        all_deleted = []
1253        debug_verb = self.verbose > 2
1254        if debug_verb:
1255            print("Updating labels in frames " + str(frames))
1256        for frame in frames:
1257            keep = indmodif[0] == frame
1258            ## check old labels if totally removed or not
1259            deleted = np.setdiff1d(old_labels[keep], self.seg[frame])
1260            left = np.setdiff1d(old_labels[keep], deleted)
1261            if deleted.shape[0] > 0:
1262                self.tracking.remove_one_frame(deleted, frame, handle_gaps=False, refresh=False)
1263                if self.forbid_gaps:
1264                    all_deleted = all_deleted + list(set(deleted) - set(all_deleted))
1265            if left.shape[0] > 0:
1266                self.tracking.update_track_on_frame(left, frame)
1267            ## now check new labels
1268            nlabels = np.unique(new_labels[keep])
1269            if nlabels.shape[0] > 0:
1270                self.tracking.update_track_on_frame(nlabels, frame)
1271            if debug_verb:
1272                print("Labels deleted at frame " + str(frame) + " " + str(deleted) + " or added " + str(nlabels))

Check what had been modified, and update tracks from it, looking frame by frame

def update_added_labels(self, indmodif, new_labels):
1274    def update_added_labels(self, indmodif, new_labels):
1275        """Update tracks of labels that have been fully added"""
1276        if self.verbose > 1:
1277            start_time = time.time()
1278
1279        ## Deleted labels
1280        frames = np.unique(indmodif[0])
1281        self.tracking.add_tracks_fromindices(indmodif, new_labels)
1282        if self.forbid_gaps:
1283            ## Check if some gaps has been created in tracks (remove middle(s) frame(s))
1284            added = list(set(new_labels))
1285            if len(added) > 0:
1286                self.handle_gaps(added, verbose=0)
1287
1288        if self.verbose > 1:
1289            ut.show_duration(start_time, "updated added tracks in ")

Update tracks of labels that have been fully added

def update_removed_labels(self, indmodif, old_labels):
1291    def update_removed_labels(self, indmodif, old_labels):
1292        """Update tracks of labels that have been fully removed"""
1293        if self.verbose > 1:
1294            start_time = time.time()
1295
1296        ## Deleted labels
1297        frames = np.unique(indmodif[0])
1298        self.tracking.remove_on_frames(np.unique(old_labels), frames)
1299        if self.forbid_gaps:
1300            ## Check if some gaps has been created in tracks (remove middle(s) frame(s))
1301            deleted = list(set(old_labels))
1302            if len(deleted) > 0:
1303                self.handle_gaps(deleted, verbose=0)
1304
1305        if self.verbose > 1:
1306            ut.show_duration(start_time, "updated removed tracks in ")

Update tracks of labels that have been fully removed

def update_replaced_labels(self, indmodif, new_labels, old_labels):
1308    def update_replaced_labels(self, indmodif, new_labels, old_labels):
1309        """Old_labels were fully replaced by new_labels on some frames, update tracks from it"""
1310        if self.verbose > 1:
1311            start_time = time.time()
1312
1313        ## Deleted labels
1314        frames = np.unique(indmodif[0])
1315        self.tracking.replace_on_frames(np.unique(old_labels), np.unique(new_labels), frames)
1316        if self.forbid_gaps:
1317            ## Check if some gaps has been created in tracks (remove middle(s) frame(s))
1318            deleted = list(set(old_labels))
1319            if len(deleted) > 0:
1320                self.handle_gaps(deleted, verbose=0)
1321
1322        if self.verbose > 1:
1323            ut.show_duration(start_time, "updated replaced tracks in ")

Old_labels were fully replaced by new_labels on some frames, update tracks from it

def handle_gaps(self, track_list, verbose=None):
1325    def handle_gaps(self, track_list, verbose=None):
1326        """Check and fix gaps in tracks"""
1327        if verbose is None:
1328            verbose = self.verbose
1329        gaped = self.tracking.check_gap(track_list, verbose=verbose)
1330        if len(gaped) > 0:
1331            if self.verbose > 0:
1332                print("Relabelling tracks with gaps")
1333            self.fix_gaps(gaped)

Check and fix gaps in tracks

def fix_gaps(self, gaps):
1335    def fix_gaps(self, gaps):
1336        """Fix when some gaps has been created in tracks"""
1337        for gap in gaps:
1338            gap_frames = self.tracking.gap_frames(gap)
1339            cur_gap = gap
1340            for gapy in gap_frames:
1341                new_value = self.get_free_label()
1342                self.replace_label(cur_gap, new_value, gapy)
1343                cur_gap = new_value

Fix when some gaps has been created in tracks

def swap_labels(self, lab, olab, frame):
1345    def swap_labels(self, lab, olab, frame):
1346        """Exchange two labels"""
1347        self.tracking.swap_frame_id(lab, olab, frame)

Exchange two labels

def swap_tracks(self, lab, olab, start_frame):
1349    def swap_tracks(self, lab, olab, start_frame):
1350        """Exchange two tracks"""
1351        ## split the two labels to unused value
1352        tmp_labels = self.get_free_labels(2)
1353        for i, laby in enumerate([lab, olab]):
1354            self.replace_label(laby, tmp_labels[i], start_frame)
1355
1356        ## replace the two initial labels, in inversed order
1357        self.replace_label(tmp_labels[0], olab, start_frame)
1358        self.replace_label(tmp_labels[1], lab, start_frame)

Exchange two tracks

def split_track(self, label, frame):
1360    def split_track(self, label, frame):
1361        """Split a track at given frame"""
1362        new_label = self.get_free_label()
1363        self.replace_label(label, new_label, frame)
1364        if self.verbose > 0:
1365            ut.show_info("Split track " + str(label) + " from frame " + str(frame))
1366        return new_label

Split a track at given frame

def update_changed_labels_img(self, img_before, img_after, added=True, removed=True):
1368    def update_changed_labels_img(self, img_before, img_after, added=True, removed=True):
1369        """Update tracks from changes between the two labelled images"""
1370        if self.verbose > 1:
1371            print("Updating changed labels from images")
1372        indmodif = np.argwhere(img_before != img_after).tolist()
1373        if len(indmodif) <= 0:
1374            return
1375        indmodif = tuple(np.array(indmodif).T)
1376        new_labels = img_after[indmodif]
1377        old_labels = img_before[indmodif]
1378        self.update_changed_labels(indmodif, new_labels, old_labels)

Update tracks from changes between the two labelled images

def added_labels_oneframe(self, frame, img_before, img_after):
1380    def added_labels_oneframe(self, frame, img_before, img_after):
1381        """Update added tracks between the two labelled images at frame"""
1382        ## Look for added labels
1383        added_labels = np.setdiff1d(img_after, img_before)
1384        self.tracking.add_one_frame(added_labels, frame, refresh=True)

Update added tracks between the two labelled images at frame

def removed_labels(self, img_before, img_after, frame=None):
1386    def removed_labels(self, img_before, img_after, frame=None):
1387        """Update removed tracks between the two labelled images"""
1388        ## Look for added labels
1389        deleted_labels = np.setdiff1d(img_before, img_after)
1390        if frame is None:
1391            self.tracking.remove_tracks(deleted_labels)
1392        else:
1393            self.tracking.remove_one_frame(track_id=deleted_labels.tolist(), frame=frame, handle_gaps=self.forbid_gaps)

Update removed tracks between the two labelled images

def remove_label(self, label, force=False):
1395    def remove_label(self, label, force=False):
1396        """Remove a given label if allowed"""
1397        ut.changeLabel(self.seglayer, label, 0)
1398        self.tracking.remove_tracks(label)
1399        self.seglayer.refresh()

Remove a given label if allowed

def remove_labels(self, labels, force=False):
1401    def remove_labels(self, labels, force=False):
1402        """Remove all allowed labels"""
1403        inds = []
1404        for lab in labels:
1405            # if (force) or (not self.locked_label(label)):
1406            inds = inds + ut.getLabelIndexes(self.seglayer.data, lab, None)
1407        ut.setNewLabel(self.seglayer, inds, 0)
1408        self.tracking.remove_tracks(labels)

Remove all allowed labels

def keep_labels(self, labels, force=True):
1410    def keep_labels(self, labels, force=True):
1411        """Remove all other labels that are not in labels"""
1412        inds = []
1413        toremove = list(set(self.tracking.get_track_list()) - set(labels))
1414        # for lab in self.tracking.get_track_list():
1415        #    if lab not in labels:
1416        # if (force) or (not self.locked_label(label)):
1417        for lab in toremove:
1418            inds = inds + ut.getLabelIndexes(self.seglayer.data, lab, None)
1419        #        toremove.append(lab)
1420        ut.setNewLabel(self.seglayer, inds, 0)
1421        self.tracking.remove_tracks(toremove)

Remove all other labels that are not in labels

def get_frame_features(self, frame):
1423    def get_frame_features(self, frame):
1424        """Measure the label properties of given frame"""
1425        return regionprops(self.seg[frame])

Measure the label properties of given frame

def updates_after_tracking(self):
1427    def updates_after_tracking(self):
1428        """When tracking has been done, update events, others"""
1429        self.inspecting.get_divisions()

When tracking has been done, update events, others

def get_all_groups(self, numeric=False):
1433    def get_all_groups(self, numeric=False):
1434        """Add all groups info"""
1435        if numeric:
1436            groups = [0] * self.nlabels()
1437        else:
1438            groups = ["None"] * self.nlabels()
1439        for igroup, gr in self.groups.keys():
1440            indexes = self.tracking.get_track_indexes(self.groups[gr])
1441            if numeric:
1442                groups[indexes] = igroup + 1
1443            else:
1444                groups[indexes] = gr
1445        return groups

Add all groups info

def get_groups(self, labels, numeric=False):
1447    def get_groups(self, labels, numeric=False):
1448        """Add the group info of the given labels (repeated)"""
1449        if numeric:
1450            groups = [0] * len(labels)
1451        else:
1452            groups = ["Ungrouped"] * len(labels)
1453        for lab in np.unique(labels):
1454            gr = self.find_group(lab)
1455            if gr is None:
1456                continue
1457            if numeric:
1458                gr = self.groups.keys().index() + 1
1459            indexes = (np.argwhere(labels == lab)).flatten()
1460            for ind in indexes:
1461                groups[ind] = gr
1462        return groups

Add the group info of the given labels (repeated)

def cells_ingroup(self, labels, group):
1464    def cells_ingroup(self, labels, group):
1465        """Put the cell "label" in group group, add it if new group"""
1466        presents = self.has_labels(labels)
1467        labels = np.array(labels)[presents]
1468        if group not in self.groups.keys():
1469            self.groups[group] = []
1470            self.update_group_lists()
1471        ## add only non present label(s)
1472        grlabels = self.groups[group]
1473        self.groups[group] = list(set(grlabels + labels.tolist()))

Put the cell "label" in group group, add it if new group

def group_of_labels(self):
1475    def group_of_labels(self):
1476        """List the group of each label"""
1477        res = {}
1478        for group, labels in self.groups.items():
1479            for label in labels:
1480                res[label] = group
1481        return res

List the group of each label

def find_group(self, label):
1483    def find_group(self, label):
1484        """Find in which group the label is"""
1485        for gr, labs in self.groups.items():
1486            if label in labs:
1487                return gr
1488        return None

Find in which group the label is

def cell_removegroup(self, label):
1490    def cell_removegroup(self, label):
1491        """Detach the cell from its group"""
1492        if not self.has_label(label):
1493            if self.verbose > 1:
1494                print("Cell " + str(label) + " missing")
1495        group = self.find_group(label)
1496        if group is not None:
1497            self.groups[group].remove(label)
1498            if len(self.groups[group]) <= 0:
1499                del self.groups[group]
1500                self.update_group_lists()

Detach the cell from its group

def update_group_lists(self):
1502    def update_group_lists(self):
1503        """Update all the lists depending on the group names"""
1504        if self.outputing is not None:
1505            self.outputing.update_selection_list()
1506        if self.editing is not None:
1507            self.editing.update_group_lists()

Update all the lists depending on the group names

def reset_group(self, group_name):
1509    def reset_group(self, group_name):
1510        """Reset/remove a given group"""
1511        if group_name == "All":
1512            self.reset_groups()
1513            return
1514        if group_name in self.groups.keys():
1515            del self.groups[group_name]
1516            self.update_group_lists()

Reset/remove a given group

def reset_groups(self):
1518    def reset_groups(self):
1519        """Remove all group information for all cells"""
1520        self.groups = {}
1521        self.update_group_lists()

Remove all group information for all cells

def draw_groups(self):
1523    def draw_groups(self):
1524        """Draw all the epicells colored by their group"""
1525        grouped = np.zeros(self.seg.shape, np.uint8)
1526        if (self.groups is None) or len(self.groups.keys()) == 0:
1527            return grouped
1528        for group, labels in self.groups.items():
1529            igroup = self.get_group_index(group) + 1
1530            np.place(grouped, np.isin(self.seg, labels), igroup)
1531        return grouped

Draw all the epicells colored by their group

def get_group_index(self, group):
1533    def get_group_index(self, group):
1534        """Get the index of group in the list of groups"""
1535        if group in list(self.groups.keys()):
1536            igroup = list(self.groups.keys()).index(group)
1537            return igroup
1538        return -1

Get the index of group in the list of groups

def only_current_roi(self, frame):
1541    def only_current_roi(self, frame):
1542        """Put 0 everywhere outside the current ROI"""
1543        roi_labels = self.editing.get_labels_inside()
1544        if roi_labels is None:
1545            return None
1546        # remove all other labels that are not in roi_labels
1547        roilab = np.copy(self.seg[frame])
1548        np.place(roilab, np.isin(roilab, roi_labels, invert=True), 0)
1549        return roilab

Put 0 everywhere outside the current ROI