epicure.epicuring

EpiCure main class.

Open and initialize the files. Launch the main widget composed of the segmentation and tracking editing features. All other classes are linked to this one.

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

Initialize the EpiCure viewer instance.

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

Napari viewer that is used for this session

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

Reset all the parameters to the default values

def init_epicure_metadata(self):
84    def init_epicure_metadata(self):
85        """ Fills metadata with default values """
86        ## scalings and unit names
87        self.epi_metadata = {}
88        self.epi_metadata["ScaleXY"] = 1
89        self.epi_metadata["UnitXY"] = "um"
90        self.epi_metadata["ScaleT"] = 1
91        self.epi_metadata["UnitT"] = "min"
92        self.epi_metadata["MainChannel"] = 0
93        self.epi_metadata["Allow gaps"] = True
94        self.epi_metadata["Verbose"] = 1
95        self.epi_metadata["Scale bar"] = True
96        self.epi_metadata["MovieFile"] = ""
97        self.epi_metadata["SegmentationFile"] = ""
98        self.epi_metadata["EpithelialCells"] = True  ## epithelial (packed) cells
99        self.epi_metadata["Reloading"] = False  ## Never been epiCured yet

Fills metadata with default values

def get_resetbtn_color(self):
101    def get_resetbtn_color(self):
102        """Returns the color of Reset buttons if defined"""
103        if "Display" in self.settings:
104            if "Colors" in self.settings["Display"]:
105                if "Reset button" in self.settings["Display"]["Colors"]:
106                    return self.settings["Display"]["Colors"]["Reset button"]
107        return None

Returns the color of Reset buttons if defined

def set_thickness(self, thick):
109    def set_thickness(self, thick):
110        """
111        Thickness of junctions (half thickness)
112        
113        :param: thick set thickness value to input value
114        """
115        self.thickness = thick

Thickness of junctions (half thickness)

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

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

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

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

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

Load the intensity movie, and get metadata

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

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

def set_verbose(self, verbose):
218    def set_verbose(self, verbose):
219        """
220        Set verbose level
221        
222        :param: verbose: amount of message that will be displayed in the Terminal console, from 0 (none) to 4 (a lot, for debugging)
223        """
224        self.verbose = verbose
225        self.epi_metadata["Verbose"] = verbose

Set verbose level

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

Set the mode for gap allowing/forbid in tracks

Parameters
  • allow_gap: boolean. Indicates if gap in tracks (missing cell in one or more frames) should be allowed or not.
def set_epithelia(self, epithelia):
235    def set_epithelia(self, epithelia):
236        """
237        Set the mode for cell packing (touching or not especially)
238        
239        :param: epithelia: boolean, True if cells are touching
240        """
241        self.epi_metadata["EpithelialCells"] = epithelia

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

Parameters
  • epithelia: boolean, True if cells are touching
def set_scalebar(self, show_scalebar):
243    def set_scalebar(self, show_scalebar):
244        """
245        Show or not the scale bar, and set its value
246        
247        :param: show_scalebar: boolean, set the visibility of the scale bar
248        """
249        self.epi_metadata["Scale bar"] = show_scalebar
250        if self.viewer is not None:
251            self.viewer.scale_bar.visible = show_scalebar
252            self.viewer.scale_bar.unit = self.epi_metadata["UnitXY"]
253            for lay in self.viewer.layers:
254                lay.scale = [1, self.epi_metadata["ScaleXY"], self.epi_metadata["ScaleXY"]]
255            self.viewer.reset_view()

Show or not the scale bar, and set its value

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

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

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

Update the movie to the correct chanel

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

Open other channels if option selected

def import_trackmate(self, segpath, verbose=0):
319    def import_trackmate(self, segpath, verbose=0):
320        """ Load segmentation and tracks from TrackMate XML file """
321        if verbose > 1:
322            print("Importing segmentation and tracks from TrackMate XML file")
323        np.set_printoptions(suppress=True, floatmode="maxprec_equal")
324
325        img_data_tag = tm._get_ImageData_tag(segpath)
326        metadata = tm._get_metadata(img_data_tag)
327        seg_shape = (int(metadata["nframes"]), int(metadata["height"]), int(metadata["width"]))
328        segmentation = np.zeros(seg_shape, dtype=np.uint16)-1
329        positions, tracks = tm._parse_Model_tag(segpath, metadata, segmentation)
330        label_mapping = tm._build_label_mapping(positions, tracks)
331        positions = tm.relabel_positions(label_mapping, positions)
332        tracks = tm.relabel_tracks(label_mapping, tracks)
333        segmentation = tm.relabel_segmentation(label_mapping, segmentation)
334        return segmentation, tracks

Load segmentation and tracks from TrackMate XML file

def load_segmentation(self, seg_input):
337    def load_segmentation(self, seg_input):
338        """Load the segmentation file"""
339        start_time = ut.start_time()
340        self.graph = None ## no loaded graph
341        ## compatibility to string input, the path to the image or a dictionnary
342        if isinstance(seg_input, dict):
343            segpath = seg_input["File"]
344        else:
345            segpath = seg_input
346        self.epi_metadata["SegmentationFile"] = segpath
347        if isinstance(seg_input, dict) and "Layer" in seg_input:
348            ## take the segmentation data and close it
349            self.seg = seg_input["Layer"].data
350            ut.remove_layer(self.viewer, seg_input["Layer"])
351        else:
352            if str(segpath).endswith(".xml"):
353                ## import a TrackMate file
354                self.seg, self.graph = self.import_trackmate(segpath, verbose=self.verbose>1)
355            else:
356                self.seg, _, _, _, _, _ = ut.open_image(segpath, get_metadata=False, verbose=self.verbose > 1)
357        self.seg = np.uint32(self.seg)
358        ## transform static image to movie (add temporal dimension)
359        if len(self.seg.shape) == 2:
360            self.seg = np.expand_dims(self.seg, axis=0)
361        ## ensure that the shapes are correctly set
362        self.imgshape = self.seg.shape
363        self.imgshape2D = self.seg.shape[1:3]
364        self.nframes = self.seg.shape[0]
365        ## if the segmentation is a junction file, transform it to a label image
366        if ut.is_binary(self.seg):
367            self.junctions_to_label()
368            self.tracked = 0
369        else:
370            self.has_been_tracked()
371            self.prepare_labels()
372
373        ## define a reference size of the movie to scale default parameters
374        self.reference_size = np.max(self.imgshape2D)
375        self.epi_metadata["Reloading"] = True  ## has been formatted to EpiCure format
376
377        # display the segmentation file movie
378        if self.viewer is not None:
379            if "Movie" in self.viewer.layers:
380                scale = self.viewer.layers["Movie"].scale
381            else:
382                scale = (1,1,1)
383            self.seglayer = self.viewer.add_labels(self.seg, name="Segmentation", blending="additive", opacity=0.5, scale=scale)
384            self.viewer.dims.set_point(0, 0)
385            self.seglayer.brush_size = 4  ## default label pencil drawing size
386        if self.verbose > 0:
387            ut.show_duration(start_time, header="Segmentation loaded in ")

Load the segmentation file

def load_tracks(self, progress_bar):
390    def load_tracks(self, progress_bar):
391        """From the segmentation, get all the metadata"""
392        tracked = "tracked"
393        self.tracking.init_tracks()
394        if self.tracked == 0:
395            tracked = "untracked"
396        else:
397            if self.graph is not None:
398                self.tracking.set_graph(self.graph)
399            if self.forbid_gaps:
400                progress_bar.set_description("check and fix track gaps")
401                self.handle_gaps(track_list=None, verbose=1)
402        ut.show_info("" + str(len(self.tracking.get_track_list())) + " " + tracked + " cells loaded")

From the segmentation, get all the metadata

def has_been_tracked(self):
404    def has_been_tracked(self):
405        """Look if has been tracked already (some labels are in several frames)"""
406        nb = 0
407        for frame in range(self.seg.shape[0]):
408            if frame > 0:
409                inter = np.intersect1d(np.unique(self.seg[frame - 1]), np.unique(self.seg[frame]))
410                if len(inter) > 1:
411                    self.tracked = 1
412                    return
413        self.tracked = 0
414        return

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

def suggest_segfile(self, outdir):
416    def suggest_segfile(self, outdir):
417        """Check if a segmentation file from EpiCure already exists"""
418        if (self.epi_metadata["SegmentationFile"] != "") and ut.found_segfile(self.epi_metadata["SegmentationFile"]):
419            return self.epi_metadata["SegmentationFile"]
420        imgname, imgdir, out = ut.extract_names(self.epi_metadata["MovieFile"], outdir, mkdir=False)
421        return ut.suggest_segfile(out, imgname)

Check if a segmentation file from EpiCure already exists

def outname(self):
423    def outname(self):
424        return os.path.join(self.outdir, self.imgname)
def set_names(self, outdir):
426    def set_names(self, outdir):
427        """Extract default names from imgpath"""
428        self.imgname, self.imgdir, self.outdir = ut.extract_names(self.epi_metadata["MovieFile"], outdir, mkdir=True)

Extract default names from imgpath

def go_epicure(self, outdir='epics', segmentation_input=None):
430    def go_epicure(self, outdir="epics", segmentation_input=None):
431        """Initialize everything and start the main widget"""
432        self.set_names(outdir)
433        if segmentation_input is None:
434            segmentation_input = {}
435            segmentation_input["File"] = self.suggest_segfile(outdir)
436        self.viewer.window._status_bar._toggle_activity_dock(True)
437        progress_bar = progress(total=5)
438        progress_bar.set_description("Reading segmented image")
439        ## load the segmentation
440        self.load_segmentation(segmentation_input)
441        if isinstance(segmentation_input, dict):
442            self.epi_metadata["SegmentationFile"] = segmentation_input["File"]
443        else:
444            self.epi_metadata["SegmentationFile"] = segmentation_input
445        progress_bar.update(1)
446        ut.set_active_layer(self.viewer, "Segmentation")
447
448        ## setup the main interface and shortcuts
449        start_time = ut.start_time()
450        progress_bar.set_description("Active EpiCure shortcuts")
451        self.key_bindings()
452        progress_bar.update(2)
453        progress_bar.set_description("Prepare widget")
454        self.main_widget()
455        progress_bar.update(3)
456        progress_bar.set_description("Load tracks")
457        self.load_tracks(progress_bar)
458        progress_bar.update(4)
459
460        ## load graph if it exists
461        epiname = os.path.join(self.outdir, self.imgname + "_epidata.pkl")
462        if os.path.exists(epiname):
463            progress_bar.set_description("Load EpiCure informations")
464            self.load_epicure_data(epiname)
465        if self.verbose > 0:
466            ut.show_duration(start_time, header="Tracks and graph loaded in ")
467        progress_bar.update(5)
468        self.apply_settings()
469        progress_bar.close()
470        self.viewer.window._status_bar._toggle_activity_dock(False)

Initialize everything and start the main widget

def apply_settings(self):
473    def apply_settings(self):
474        """Apply all default or prefered settings"""
475        for sety, val in self.settings.items():
476            if sety == "Display":
477                self.display.apply_settings(val)
478                if "Show help" in val:
479                    index = int(val["Show help"])
480                    self.switchOverlayText(index)
481                if "Contour" in val:
482                    contour = int(val["Contour"])
483                    self.seglayer.contour = contour
484                    self.seglayer.refresh()
485                if "Colors" in val:
486                    color = val["Colors"]["button"]
487                    check_color = val["Colors"]["checkbox"]
488                    line_edit_color = val["Colors"]["line edit"]
489                    group_color = val["Colors"]["group"]
490                    self.main_gui.setStyleSheet(
491                        "QPushButton {background-color: "
492                        + color
493                        + "} QCheckBox::indicator {background-color: "
494                        + check_color
495                        + "} QLineEdit {background-color: "
496                        + line_edit_color
497                        + "} QGroupBox {color: grey; background-color: "
498                        + group_color
499                        + "} "
500                    )
501                    self.display_colors = val["Colors"]
502            if sety == "events":
503                self.inspecting.apply_settings(val)
504            if sety == "Output":
505                self.outputing.apply_settings(val)
506            if sety == "Track":
507                self.tracking.apply_settings(val)
508            if sety == "Edit":
509                self.editing.apply_settings(val)
510            # case _:
511            #       continue
512            ## match is not compatible with python 3.9

Apply all default or prefered settings

def update_settings(self):
514    def update_settings(self):
515        """Returns all the prefered settings"""
516        disp = self.settings
517        ## load display current settings (layers visibility)
518        disp["Display"] = self.display.get_current_settings()
519        disp["Display"]["Show help"] = self.help_index
520        disp["Display"]["Contour"] = self.seglayer.contour
521        ## load suspect current settings
522        disp["events"] = self.inspecting.get_current_settings()
523        ## get outputs current settings
524        disp["Output"] = self.outputing.get_current_settings()
525        disp["Track"] = self.tracking.get_current_settings()
526        disp["Edit"] = self.editing.get_current_settings()

Returns all the prefered settings

def main_widget(self):
530    def main_widget(self):
531        """Open the main widget interface"""
532        self.main_gui = QWidget()
533
534        layout = QVBoxLayout()
535        tabs = QTabWidget()
536        tabs.setObjectName("main")
537        layout.addWidget(tabs)
538        self.main_gui.setLayout(layout)
539
540        self.editing = Editing(self.viewer, self)
541        tabs.addTab(self.editing, "Edit")
542        self.inspecting = Inspecting(self.viewer, self)
543        tabs.addTab(self.inspecting, "Inspect")
544        self.tracking = Tracking(self.viewer, self)
545        tabs.addTab(self.tracking, "Track")
546        self.outputing = Outputing(self.viewer, self)
547        tabs.addTab(self.outputing, "Output")
548        self.display = Displaying(self.viewer, self)
549        tabs.addTab(self.display, "Display")
550        self.main_gui.setStyleSheet("QPushButton {background-color: rgb(40, 60, 75)} QCheckBox::indicator {background-color: rgb(40,52,65)}")
551
552        self.viewer.window.add_dock_widget(self.main_gui, name="Main")

Open the main widget interface

def key_bindings(self):
554    def key_bindings(self):
555        """Activate shortcuts"""
556        self.text = "-------------- ShortCuts -------------- \n "
557        self.text += "!! Shortcuts work if Segmentation layer is active !! \n"
558        # for sctype, scvals in self.shortcuts.items():
559        self.text += "\n---" + "General" + " options---\n"
560        sg = self.shortcuts["General"]
561        self.text += ut.print_shortcuts(sg)
562        self.text = self.text + "\n"
563
564        if self.verbose > 0:
565            print("Activating key shortcuts on segmentation layer")
566            print("Press <" + str(sg["show help"]["key"]) + "> to show/hide the main shortcuts")
567            print("Press <" + str(sg["show all"]["key"]) + "> to show ALL shortcuts")
568        ut.setOverlayText(self.viewer, self.text, size=12)
569
570        @self.seglayer.bind_key(sg["show help"]["key"], overwrite=True)
571        def switch_shortcuts(seglayer):
572            # index = (self.help_index+1)%(len(self.overtext.keys())+1)
573            # self.switchOverlayText(index)
574            index = (self.help_index + 1) % 2
575            self.switchOverlayText(index)
576
577        @self.seglayer.bind_key(sg["show all"]["key"], overwrite=True)
578        def list_all_shortcuts(seglayer):
579            self.switchOverlayText(0)  ## hide display message in main window
580            text = "**************** EPICURE *********************** \n"
581            text += "\n"
582            text += self.text
583            text += "\n"
584            text += ut.napari_shortcuts()
585            for key, val in self.overtext.items():
586                text += "\n"
587                text += val
588            self.update_text_window(text)
589
590        @self.seglayer.bind_key(sg["save segmentation"]["key"], overwrite=True)
591        def save_seglayer(seglayer):
592            self.save_epicures()
593
594        @self.viewer.bind_key(sg["save movie"]["key"], overwrite=True)
595        def save_movie(seglayer):
596            endname = "_frames.tif"
597            outname = os.path.join(self.outdir, self.imgname + endname)
598            self.save_movie(outname)

Activate shortcuts

def switchOverlayText(self, index):
602    def switchOverlayText(self, index):
603        """Switch overlay display text to index"""
604        self.help_index = index
605        if index == 0:
606            ut.showOverlayText(self.viewer, vis=False)
607            return
608        else:
609            ut.showOverlayText(self.viewer, vis=True)
610        # self.setCurrentOverlayText()
611        self.setGeneralOverlayText()

Switch overlay display text to index

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

Creates and opens a pop-up window with shortcut list

def update_text_window(self, message):
617    def update_text_window(self, message):
618        """Update message in separate window"""
619        self.init_text_window()
620        self.blabla.value = message

Update message in separate window

def setGeneralOverlayText(self):
622    def setGeneralOverlayText(self):
623        """set overlay help message to general message"""
624        text = self.text
625        ut.setOverlayText(self.viewer, text, size=12)

set overlay help message to general message

def setCurrentOverlayText(self):
627    def setCurrentOverlayText(self):
628        """Set overlay help text message to current selected options list"""
629        text = self.text
630        dispkey = list(self.overtext.keys())[self.help_index - 1]
631        text += self.overtext[dispkey]
632        ut.setOverlayText(self.viewer, text, size=12)

Set overlay help text message to current selected options list

def get_summary(self):
634    def get_summary(self):
635        """Get a summary of the infos of the movie"""
636        summ = "----------- EpiCure summary ----------- \n"
637        summ += "--- Image infos \n"
638        summ += "Movie name: " + str(self.epi_metadata["MovieFile"]) + "\n"
639        summ += "Movie size (x,y): " + str(self.imgshape2D) + "\n"
640        if self.nframes is not None:
641            summ += "Nb frames: " + str(self.nframes) + "\n"
642        summ += "\n"
643        summ += "--- Segmentation infos \n"
644        summ += "Segmentation file: " + str(self.epi_metadata["SegmentationFile"]) + "\n"
645        summ += "Nb tracks: " + str(len(self.tracking.get_track_list())) + "\n"
646        tracked = "yes"
647        if self.tracked == 0:
648            tracked = "no"
649        summ += "Tracked: " + tracked + "\n"
650        nb_labels, mean_duration, mean_area = ut.summary_labels(self.seg)
651        summ += "Nb cells: " + str(nb_labels) + "\n"
652        summ += "Average track lengths: " + str(mean_duration) + " frames\n"
653        summ += "Average cell area: " + str(mean_area) + " pixels^2\n"
654        summ += "Nb suspect events: " + str(self.inspecting.nb_events(only_suspect=True)) + "\n"
655        summ += "Nb divisions: " + str(self.nb_divisions()) + "\n"
656        summ += "Nb extrusions: " + str(self.inspecting.nb_type("extrusion")) + "\n"
657        summ += "\n"
658        summ += "--- Parameter infos \n"
659        summ += "Junction thickness: " + str(self.thickness) + "\n"
660        return summ

Get a summary of the infos of the movie

def nb_divisions(self):
662    def nb_divisions(self):
663        """ Return the number of divisions """
664        return self.inspecting.nb_type("division")

Return the number of divisions

def set_contour(self, width):
666    def set_contour(self, width):
667        """ 
668        Set the width of the contour of the cells to display the segmentation
669
670        :param: width: width of the contours of the segmentation (napari contour parameter). If 0 the cell will be filled by its label 
671        """
672        self.seglayer.contour = width

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

Parameters
  • width: width of the contours of the segmentation (napari contour parameter). If 0 the cell will be filled by its label
def check_layers(self):
676    def check_layers(self):
677        """Check that the necessary layers are present"""
678        if self.editing.shapelayer_name not in self.viewer.layers:
679            if self.verbose > 0:
680                print("Reput shape layer")
681            self.editing.create_shapelayer()
682        if self.inspecting.eventlayer_name not in self.viewer.layers:
683            if self.verbose > 0:
684                print("Reput event layer")
685            self.inspecting.create_eventlayer()
686        if "Movie" not in self.viewer.layers:
687            if self.verbose > 0:
688                print("Reput movie layer")
689            mview = self.viewer.add_image(self.img, name="Movie", blending="additive", colormap="gray", scale=[1, self.epi_metadata["ScaleXY"], self.epi_metadata["ScaleXY"]])
690            # mview.reset_contrast_limits()
691            mview.contrast_limits = self.quantiles()
692            mview.gamma = 0.95
693        if "Segmentation" not in self.viewer.layers:
694            if self.verbose > 0:
695                print("Reput segmentation")
696            self.seglayer = self.viewer.add_labels(self.seg, name="Segmentation", blending="additive", opacity=0.5, scale=self.viewer.layers["Movie"].scale)
697
698        self.finish_update()

Check that the necessary layers are present

def finish_update(self, contour=None):
700    def finish_update(self, contour=None):
701        """
702        After doing modifications on some layer(s), select back the main layer Segmentation as active (important for shortcut bindings) and refresh it
703        """
704        if contour is not None:
705            self.seglayer.contour = contour
706        ut.set_active_layer(self.viewer, "Segmentation")
707        self.seglayer.refresh()
708        duplayers = ["PrevSegmentation"]
709        for dlay in duplayers:
710            if dlay in self.viewer.layers:
711                (self.viewer.layers[dlay]).refresh()

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

def read_epicure_metadata(self):
713    def read_epicure_metadata(self):
714        """Load saved infos from file"""
715        epiname = self.outname() + "_epidata.pkl"
716        if os.path.exists(epiname):
717            infile = open(epiname, "rb")
718            try:
719                epidata = pickle.load(infile)
720                if "EpiMetaData" in epidata.keys():
721                    for key, vals in epidata["EpiMetaData"].items():
722                        self.epi_metadata[key] = vals
723                infile.close()
724            except:
725                ut.show_warning("Could not read EpiCure metadata file " + epiname)

Load saved infos from file

def save_epicures(self, imtype='float32'):
727    def save_epicures(self, imtype="float32"):
728        """
729        Save all the current data: the segmentation, the metadata (metadata of the image, last parameters used), the events and some display settings.
730        """
731        outname = os.path.join(self.outdir, self.imgname + "_labels.tif")
732        ut.writeTif(self.seg, outname, self.epi_metadata["ScaleXY"], imtype, what="Segmentation")
733        epiname = os.path.join(self.outdir, self.imgname + "_epidata.pkl")
734        outfile = open(epiname, "wb")
735        self.epi_metadata["MainChannel"] = self.main_channel 
736        epidata = {}
737        epidata["EpiMetaData"] = self.epi_metadata
738        if self.groups is not None:
739            epidata["Group"] = self.groups
740        if self.tracking.graph is not None:
741            epidata["Graph"] = self.tracking.graph
742        if self.inspecting is not None and self.inspecting.events is not None:
743            epidata["Events"] = {}
744            if self.inspecting.events.data is not None:
745                epidata["Events"]["Points"] = self.inspecting.events.data
746                epidata["Events"]["Props"] = self.inspecting.events.properties
747                epidata["Events"]["Types"] = self.inspecting.event_types
748                # epidata["Events"]["Symbols"] = self.inspecting.events.symbol
749                # epidata["Events"]["Colors"] = self.inspecting.events.face_color
750        if "Movie" in self.viewer.layers:
751            ## to keep movie layer display settings for this file
752            epidata["Display"] = {}
753            epidata["Display"]["MovieContrast"] = self.viewer.layers["Movie"].contrast_limits
754        pickle.dump(epidata, outfile)
755        outfile.close()

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

def read_group_data(self, groups):
757    def read_group_data(self, groups):
758        """Read the group EpiCure data from opened file"""
759        if self.verbose > 0:
760            print("Loaded cell groups info: " + str(list(groups.keys())))
761            if self.verbose > 2:
762                print("Cell groups: " + str(groups))
763        return groups

Read the group EpiCure data from opened file

def read_graph_data(self, infile):
765    def read_graph_data(self, infile):
766        """
767        Read the graph EpiCure data from opened pickle file
768
769        :param: infile: instance of pickle file being read. This will read the next part of the pickle file and load it in the track graph.
770        """
771        try:
772            graph = pickle.load(infile)
773            if self.verbose > 0:
774                print("Graph (lineage) loaded")
775            return graph
776        except:
777            if self.verbose > 1:
778                print("No graph infos found")
779            return None

Read the graph EpiCure data from opened pickle file

Parameters
  • infile: instance of pickle file being read. This will read the next part of the pickle file and load it in the track graph.
def read_events_data(self, infile):
781    def read_events_data(self, infile):
782        """Read info of EpiCure events (suspects, divisions) from opened file"""
783        try:
784            events_pts = pickle.load(infile)
785            if events_pts is not None:
786                events_props = pickle.load(infile)
787                events_type = pickle.load(infile)
788                try:
789                    symbols = pickle.load(infile)
790                    colors = pickle.load(infile)
791                except:
792                    if self.verbose > 1:
793                        print("No events display info found")
794                    symbols = None
795                    colors = None
796                return events_pts, events_props, events_type
797            else:
798                return None, None, None
799        except:
800            if self.verbose > 1:
801                print("events info not complete")
802            return None, None, None

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

def load_epicure_data(self, epiname):
804    def load_epicure_data(self, epiname):
805        """Load saved infos from file"""
806        infile = open(epiname, "rb")
807        try:
808            if ut.is_windows():
809               import pathlib
810               pathlib.PosixPath = pathlib.WindowsPath
811               #epidata = pickle.load( infile, encoding="utf8" )
812            epidata = pickle.load( infile )
813            #print(epidata)
814            if "EpiMetaData" in epidata.keys():
815                # version of epicure file after Epicure 0.2.0
816                self.read_epidata(epidata)
817                infile.close()
818            else:
819                # version anterior of Epicure 0.2.0
820                self.load_epicure_data_old(epidata, infile)
821        except Exception as e:
822            if self.verbose > 1:
823                print(f" {type(e)} {e} - Could not read EpiCure data file {epiname}")
824            else:
825                ut.show_warning(f"Could not read EpiCure data file {epiname}")
826                print(f" {type(e)} {e} - Could not read EpiCure data file {epiname}")

Load saved infos from file

def read_epidata(self, epidata):
828    def read_epidata(self, epidata):
829        """Read the dict of saved state and initialize all instances with it"""
830        for key, vals in epidata.items():
831            if key == "EpiMetaData":
832                ## image data is read on the previous step
833                continue
834            if key == "Group":
835                ## Load groups information
836                self.groups = self.read_group_data(vals)
837                self.update_group_lists()
838            if key == "Graph":
839                ## Load graph (lineage) informations
840                self.tracking.graph = vals
841                if self.tracking.graph is not None:
842                    self.tracking.tracklayer.refresh()
843                if self.verbose > 2:
844                    print(f"Loaded track graph: {self.tracking.graph}")
845            if key == "Events":
846                ## Load events information
847                if "Points" in vals.keys():
848                    pts = vals["Points"]
849                if "Props" in vals.keys():
850                    props = vals["Props"]
851                if "Types" in vals.keys():
852                    event_types = vals["Types"]
853                # if "Symbols" in vals.keys():
854                #    symbols = vals["Symbols"]
855                # if "Colors" in vals.keys():
856                #    colors = vals["Colors"]
857                if pts is not None:
858                    if len(pts) > 0:
859                        self.inspecting.load_events(pts, props, event_types)
860                    if len(pts) > 0 and self.verbose > 0:
861                        print("events loaded")
862                    ut.show_info("Loaded " + str(len(pts)) + " events")
863            if key == "Display":
864                if vals is not None:
865                    ## load display setting
866                    if "MovieContrast" in vals.keys():
867                        self.viewer.layers["Movie"].contrast_limits = vals["MovieContrast"]

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

def load_epicure_data_old(self, groups, infile):
869    def load_epicure_data_old(self, groups, infile):
870        """Load saved infos from file"""
871        ## Load groups information
872        self.groups = self.read_group_data(groups)
873        for group in self.groups.keys():
874            self.editing.update_group_list(group)
875        self.outputing.update_selection_list()
876        ## Load graph (lineage) informations
877        self.tracking.graph = self.read_graph_data(infile)
878        if self.tracking.graph is not None:
879            self.tracking.tracklayer.refresh()
880        ## Load events information
881        pts, props, event_types = self.read_events_data(infile)
882        if pts is not None:
883            if len(pts) > 0:
884                self.inspecting.load_events(pts, props, event_types)
885                if len(pts) > 0 and self.verbose > 0:
886                    print("events loaded")
887                    ut.show_info("Loaded " + str(len(pts)) + " events")
888        infile.close()

Load saved infos from file

def save_movie(self, outname):
890    def save_movie(self, outname):
891        """Save movie with current display parameters, except zoom"""
892        save_view = self.viewer.camera.copy()
893        save_frame = ut.current_frame(self.viewer)
894        ## place the view to see the whole image
895        self.viewer.reset_view()
896        # self.viewer.camera.zoom = 1
897        sizex = (self.imgshape2D[0] * self.viewer.camera.zoom) / 2
898        sizey = (self.imgshape2D[1] * self.viewer.camera.zoom) / 2
899        if os.path.exists(outname):
900            os.remove(outname)
901
902        ## take a screenshot of each frame
903        for frame in range(self.nframes):
904            self.viewer.dims.set_point(0, frame)
905            shot = self.viewer.window.screenshot(canvas_only=True, flash=False)
906            ## remove border: movie is at the center
907            centx = int(shot.shape[0] / 2) + 1
908            centy = int(shot.shape[1] / 2) + 1
909            shot = shot[
910                int(centx - sizex) : int(centx + sizex),
911                int(centy - sizey) : int(centy + sizey),
912            ]
913            ut.appendToTif(shot, outname)
914        self.viewer.camera.update(save_view)
915        if save_frame is not None:
916            self.viewer.dims.set_point(0, save_frame)
917        ut.show_info("Movie " + outname + " saved")

Save movie with current display parameters, except zoom

def reset_data(self):
919    def reset_data(self):
920        """Reset EpiCure data (group, suspect, graph)"""
921        self.inspecting.reset_all_events()
922        self.reset_groups()
923        self.tracking.graph = None

Reset EpiCure data (group, suspect, graph)

def junctions_to_label(self):
925    def junctions_to_label(self):
926        """convert epyseg/skeleton result (junctions) to labels map"""
927        ## ensure that skeleton is thin enough
928        for z in range(self.seg.shape[0]):
929            self.skel_one_frame(z)
930        self.seg = ut.reset_labels(self.seg, closing=True)

convert epyseg/skeleton result (junctions) to labels map

def skel_one_frame(self, z):
932    def skel_one_frame(self, z):
933        """From segmentation of junctions of one frame, get it as a correct skeleton"""
934        skel = skeletonize(self.seg[z] / np.max(self.seg[z]))
935        skel = ut.copy_border(skel, self.seg[z])
936        self.seg[z] = np.invert(skel)

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

def reset_labels(self):
938    def reset_labels(self):
939        """Reset all labels, ensure unicity"""
940        if self.epi_metadata["EpithelialCells"]:
941            ### packed (contiguous cells), ensure that they are separated by one pixel only
942            skel = self.get_skeleton()
943            skel = np.uint32(skel)
944            self.seg = skel
945            self.seglayer.data = skel
946            self.junctions_to_label()
947            self.seglayer.data = self.seg
948        else:
949            self.get_cells()

Reset all labels, ensure unicity

def check_extrusions_sanity(self):
951    def check_extrusions_sanity(self):
952        """Check that extrusions seem to be correct (last of tracks )"""
953        extrusions = self.inspecting.get_events_from_type("extrusion")
954        nrem = 0
955        if (extrusions is not None) and (extrusions != []):
956            for extr_id in extrusions:
957                pos, label = self.inspecting.get_event_infos(extr_id)
958                last_frame = self.tracking.get_last_frame(label)
959                if pos[0] != last_frame:
960                    if self.verbose > 1:
961                        print("Extrusion " + str(extr_id) + " at frame " + str(pos[0]) + " not at the end of track " + str(label))
962                        print("Removing it")
963                    self.inspecting.remove_one_event(extr_id)
964                    nrem = nrem + 1
965            print("Removed " + str(nrem) + " extrusions that dit not correspond to the end of tracks")

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

def prepare_labels(self):
967    def prepare_labels(self):
968        """Process the labels to be in a correct Epicurable format"""
969        if self.epi_metadata["EpithelialCells"]:
970            if self.epi_metadata["Reloading"]:
971                ## if opening an already EpiCured movie, assume it's in correct format
972                return
973            ### packed (contiguous cells), ensure that they are separated by one pixel only
974            self.thin_boundaries()
975        else:
976            self.get_cells()

Process the labels to be in a correct Epicurable format

def get_cells(self):
978    def get_cells(self):
979        """Non jointive cells: check label unicity"""
980        for frame in self.seg:
981            if ut.non_unique_labels(frame):
982                self.seg = ut.reset_labels(self.seg, closing=True)
983                return

Non jointive cells: check label unicity

def thin_boundaries(self):
985    def thin_boundaries(self):
986        """ " Assure that all boundaries are only 1 pixel thick"""
987        if self.process_parallel:
988            self.seg = Parallel(n_jobs=self.nparallel)(delayed(ut.thin_seg_one_frame)(zframe) for zframe in self.seg)
989            self.seg = np.array(self.seg)
990        else:
991            for z in range(self.seg.shape[0]):
992                self.seg[z] = ut.thin_seg_one_frame(self.seg[z])

" Assure that all boundaries are only 1 pixel thick

def add_skeleton(self):
 994    def add_skeleton(self):
 995        """add a layer containing the skeleton movie of the segmentation"""
 996        # display the segmentation file movie
 997        if self.viewer is not None:
 998            skel = np.zeros(self.seg.shape, dtype="uint8")
 999            skel[self.seg == 0] = 1
1000            skel = self.get_skeleton(viewer=self.viewer)
1001            ut.remove_layer(self.viewer, "Skeleton")
1002            skellayer = self.viewer.add_image(skel, name="Skeleton", blending="additive", opacity=1, scale=self.viewer.layers["Movie"].scale)
1003            skellayer.reset_contrast_limits()
1004            skellayer.contrast_limits = (0, 1)

add a layer containing the skeleton movie of the segmentation

def get_skeleton(self, viewer=None):
1006    def get_skeleton(self, viewer=None):
1007        """convert labels movie to skeleton (thin boundaries)"""
1008        if self.seg is None:
1009            return None
1010        parallel = 0
1011        if self.process_parallel:
1012            parallel = self.nparallel
1013        return ut.get_skeleton(self.seg, viewer=viewer, verbose=self.verbose, parallel=parallel)

convert labels movie to skeleton (thin boundaries)

def get_free_labels(self, nlab):
1017    def get_free_labels(self, nlab):
1018        """Get the nlab smallest unused labels"""
1019        used = set(self.tracking.get_track_list())
1020        return ut.get_free_labels(used, nlab)

Get the nlab smallest unused labels

def get_free_label(self):
1022    def get_free_label(self):
1023        """Return the first free label"""
1024        return self.get_free_labels(1)[0]

Return the first free label

def has_label(self, label):
1026    def has_label(self, label):
1027        """Check if label is present in the tracks"""
1028        return self.tracking.has_track(label)

Check if label is present in the tracks

def has_labels(self, labels):
1030    def has_labels(self, labels):
1031        """Check if labels are present in the tracks"""
1032        return self.tracking.has_tracks(labels)

Check if labels are present in the tracks

def nlabels(self):
1034    def nlabels(self):
1035        """Number of unique tracks"""
1036        return self.tracking.nb_tracks()

Number of unique tracks

def get_labels(self):
1038    def get_labels(self):
1039        """Return list of labels in tracks"""
1040        return list(self.tracking.get_track_list())

Return list of labels in tracks

def delete_tracks(self, tracks):
1043    def delete_tracks(self, tracks):
1044        """Remove all the tracks from the Track layer"""
1045        self.tracking.remove_tracks(tracks)

Remove all the tracks from the Track layer

def delete_track(self, label, frame=None):
1047    def delete_track(self, label, frame=None):
1048        """Remove (part of) the track"""
1049        if frame is None:
1050            self.tracking.remove_track(label)
1051        else:
1052            self.tracking.remove_one_frame(label, frame, handle_gaps=self.forbid_gaps)

Remove (part of) the track

def update_centroid(self, label, frame):
1054    def update_centroid(self, label, frame):
1055        """Track label has been change at given frame"""
1056        if label not in self.tracking.has_track(label):
1057            if self.verbose > 1:
1058                print("Track " + str(label) + " not found")
1059            return
1060        self.tracking.update_centroid(label, frame)

Track label has been change at given frame

def get_label_indexes(self, label, start_frame=0):
1063    def get_label_indexes(self, label, start_frame=0):
1064        """Returns the indexes where label is present in segmentation, starting from start_frame"""
1065        indmodif = []
1066        if self.verbose > 2:
1067            start_time = ut.start_time()
1068        pos = self.tracking.get_track_column(track_id=label, column="fullpos")
1069        pos = pos[pos[:, 0] >= start_frame]
1070        ## if nothing in pos, pb with track data
1071        if pos is None or len(pos) == 0:
1072            ut.show_warning("Something wrong in the track data. Resetting track data (can take time)")
1073            self.tracking.reset_tracks()
1074            self.get_label_indexes(label, start_frame)
1075
1076        indmodif = np.argwhere(self.seg[pos[:, 0]] == label)
1077        indmodif = ut.shiftFrames(indmodif, pos[:, 0])
1078        if self.verbose > 2:
1079            ut.show_duration(start_time, header="Label indexes found in ")
1080        return indmodif

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

def replace_label(self, label, new_label, start_frame=0):
1082    def replace_label(self, label, new_label, start_frame=0):
1083        """Replace label with new_label from start_frame - Relabelling only"""
1084        indmodif = self.get_label_indexes(label, start_frame)
1085        new_labels = [new_label] * len(indmodif)
1086        self.change_labels(indmodif, new_labels, replacing=True)

Replace label with new_label from start_frame - Relabelling only

def change_labels_frommerge(self, indmodif, new_labels, remove_labels):
1088    def change_labels_frommerge(self, indmodif, new_labels, remove_labels):
1089        """Change the value at pixels indmodif to new_labels and update tracks/graph. Full remove of the two merged labels"""
1090        if len(indmodif) > 0:
1091            ## get effectively changed labels
1092            indmodif, new_labels, _ = ut.setNewLabel(self.seglayer, indmodif, new_labels, add_frame=None, return_old=False)
1093            if len(new_labels) > 0:
1094                self.update_added_labels(indmodif, new_labels)
1095                self.update_removed_labels(indmodif, remove_labels)
1096        self.seglayer.refresh()

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

def change_labels(self, indmodif, new_labels, replacing=False):
1098    def change_labels(self, indmodif, new_labels, replacing=False):
1099        """Change the value at pixels indmodif to new_labels and update tracks/graph
1100
1101        Assume that only label at current frame can have its shape modified. Other changed label is only relabelling at frames > current frame (child propagation)
1102        """
1103        if len(indmodif) > 0:
1104            ## get effectively changed labels
1105            indmodif, new_labels, old_labels = ut.setNewLabel(self.seglayer, indmodif, new_labels, add_frame=None)
1106            if len(new_labels) > 0:
1107                if replacing:
1108                    self.update_replaced_labels(indmodif, new_labels, old_labels)
1109                else:
1110                    ## the only label to change are the current frame (smaller one), the other are only relabelling (propagation)
1111                    cur_frame = np.min(indmodif[0])
1112                    to_reshape = indmodif[0] == cur_frame
1113                    self.update_changed_labels((indmodif[0][to_reshape], indmodif[1][to_reshape], indmodif[2][to_reshape]), new_labels[to_reshape], old_labels[to_reshape])
1114                    to_relab = np.invert(to_reshape)
1115                    self.update_replaced_labels((indmodif[0][to_relab], indmodif[1][to_relab], indmodif[2][to_relab]), new_labels[to_relab], old_labels[to_relab])
1116        self.seglayer.refresh()

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

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

def get_mask(self, label, start=None, end=None):
1118    def get_mask(self, label, start=None, end=None):
1119        """Get mask of label from frame start to frame end"""
1120        if (start is None) or (end is None):
1121            start, end = self.tracking.get_extreme_frames(label)
1122        crop = self.seg[start : (end + 1)]
1123        mask = np.isin(crop, [label]) * 1
1124        return mask

Get mask of label from frame start to frame end

def get_label_movie(self, label, extend=1.25):
1126    def get_label_movie(self, label, extend=1.25):
1127        """Get movie centered on label"""
1128        start, end = self.tracking.get_extreme_frames(label)
1129        mask = self.get_mask(label, start, end)
1130        boxes = []
1131        centers = []
1132        max_box = 0
1133        for frame in mask:
1134            props = regionprops(frame)
1135            bbox = props[0].bbox
1136            boxes.append(bbox)
1137            centers.append(props[0].centroid)
1138            for i in range(2):
1139                max_box = max(max_box, bbox[i + 2] - bbox[i])
1140
1141        box_size = int(max_box * extend)
1142        movie = np.zeros((end - start + 1, box_size, box_size))
1143        for i, frame in enumerate(range(start, end + 1)):
1144            xmin = int(centers[i][0] - box_size / 2)
1145            xminshift = 0
1146            if xmin < 0:
1147                xminshift = -xmin
1148                xmin = 0
1149            xmax = xmin + box_size - xminshift
1150            xmaxshift = box_size
1151            if xmax > self.imgshape2D[0]:
1152                xmaxshift = self.imgshape2D[0] - xmax
1153                xmax = self.imgshape2D[0]
1154
1155            ymin = int(centers[i][1] - max_box / 2)
1156            yminshift = 0
1157            if ymin < 0:
1158                yminshift = -ymin
1159                ymin = 0
1160            ymax = ymin + box_size - yminshift
1161            ymaxshift = box_size
1162            if ymax > self.imgshape2D[1]:
1163                ymaxshift = self.imgshape2D[1] - ymax
1164                ymax = self.imgshape2D[1]
1165
1166            movie[i, xminshift:xmaxshift, yminshift:ymaxshift] = self.img[frame, xmin:xmax, ymin:ymax]
1167        return movie

Get movie centered on label

def cell_radius(self, label, frame):
1170    def cell_radius(self, label, frame):
1171        """Approximate the cell radius at given frame"""
1172        area = np.sum(self.seg[frame] == label)
1173        radius = math.sqrt(area / math.pi)
1174        return radius

Approximate the cell radius at given frame

def cell_area(self, label, frame):
1176    def cell_area(self, label, frame):
1177        """Approximate the cell radius at given frame"""
1178        area = np.sum(self.seg[frame] == label)
1179        return area

Approximate the cell radius at given frame

def cell_on_border(self, label, frame):
1181    def cell_on_border(self, label, frame):
1182        """Check if a given cell is on border of the image"""
1183        bbox = ut.getBBox2D(self.seg[frame], label)
1184        out = ut.outerBBox2D(bbox, self.imgshape2D, margin=3)
1185        return out

Check if a given cell is on border of the image

def add_label(self, labels, frame=None):
1188    def add_label(self, labels, frame=None):
1189        """Add a label to the tracks"""
1190        if frame is not None:
1191            if np.isscalar(labels):
1192                labels = [labels]
1193            self.tracking.add_one_frame(labels, frame, refresh=True)
1194        else:
1195            if self.verbose > 1:
1196                print("TODO add label no frame")

Add a label to the tracks

def add_one_label_to_track(self, label):
1198    def add_one_label_to_track(self, label):
1199        """Add the track data of a given label if missing"""
1200        iframe = 0
1201        while (iframe < self.nframes) and (label not in self.seg[iframe]):
1202            iframe = iframe + 1
1203        while (iframe < self.nframes) and (label in self.seg[iframe]):
1204            self.tracking.add_one_frame([label], iframe)
1205            iframe = iframe + 1

Add the track data of a given label if missing

def update_label(self, label, frame):
1207    def update_label(self, label, frame):
1208        """Update the given label at given frame"""
1209        self.tracking.update_track_on_frame([label], frame)

Update the given label at given frame

def update_changed_labels(self, indmodif, new_labels, old_labels, full=False):
1211    def update_changed_labels(self, indmodif, new_labels, old_labels, full=False):
1212        """Check what had been modified, and update tracks from it, looking frame by frame"""
1213        ## check all the old_labels if still present or not
1214        if self.verbose > 1:
1215            start_time = time.time()
1216        frames = np.unique(indmodif[0])
1217        all_deleted = []
1218        debug_verb = self.verbose > 2
1219        if debug_verb:
1220            print("Updating labels in frames " + str(frames))
1221        for frame in frames:
1222            keep = indmodif[0] == frame
1223            ## check old labels if totally removed or not
1224            deleted = np.setdiff1d(old_labels[keep], self.seg[frame])
1225            left = np.setdiff1d(old_labels[keep], deleted)
1226            if deleted.shape[0] > 0:
1227                self.tracking.remove_one_frame(deleted, frame, handle_gaps=False, refresh=False)
1228                if self.forbid_gaps:
1229                    all_deleted = all_deleted + list(set(deleted) - set(all_deleted))
1230            if left.shape[0] > 0:
1231                self.tracking.update_track_on_frame(left, frame)
1232            ## now check new labels
1233            nlabels = np.unique(new_labels[keep])
1234            if nlabels.shape[0] > 0:
1235                self.tracking.update_track_on_frame(nlabels, frame)
1236            if debug_verb:
1237                print("Labels deleted at frame " + str(frame) + " " + str(deleted) + " or added " + str(nlabels))

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

def update_added_labels(self, indmodif, new_labels):
1239    def update_added_labels(self, indmodif, new_labels):
1240        """Update tracks of labels that have been fully added"""
1241        if self.verbose > 1:
1242            start_time = time.time()
1243
1244        ## Deleted labels
1245        frames = np.unique(indmodif[0])
1246        self.tracking.add_tracks_fromindices(indmodif, new_labels)
1247        if self.forbid_gaps:
1248            ## Check if some gaps has been created in tracks (remove middle(s) frame(s))
1249            added = list(set(new_labels))
1250            if len(added) > 0:
1251                self.handle_gaps(added, verbose=0)
1252
1253        if self.verbose > 1:
1254            ut.show_duration(start_time, "updated added tracks in ")

Update tracks of labels that have been fully added

def update_removed_labels(self, indmodif, old_labels):
1256    def update_removed_labels(self, indmodif, old_labels):
1257        """Update tracks of labels that have been fully removed"""
1258        if self.verbose > 1:
1259            start_time = time.time()
1260
1261        ## Deleted labels
1262        frames = np.unique(indmodif[0])
1263        self.tracking.remove_on_frames(np.unique(old_labels), frames)
1264        if self.forbid_gaps:
1265            ## Check if some gaps has been created in tracks (remove middle(s) frame(s))
1266            deleted = list(set(old_labels))
1267            if len(deleted) > 0:
1268                self.handle_gaps(deleted, verbose=0)
1269
1270        if self.verbose > 1:
1271            ut.show_duration(start_time, "updated removed tracks in ")

Update tracks of labels that have been fully removed

def update_replaced_labels(self, indmodif, new_labels, old_labels):
1273    def update_replaced_labels(self, indmodif, new_labels, old_labels):
1274        """Old_labels were fully replaced by new_labels on some frames, update tracks from it"""
1275        if self.verbose > 1:
1276            start_time = time.time()
1277
1278        ## Deleted labels
1279        frames = np.unique(indmodif[0])
1280        self.tracking.replace_on_frames(np.unique(old_labels), np.unique(new_labels), frames)
1281        if self.forbid_gaps:
1282            ## Check if some gaps has been created in tracks (remove middle(s) frame(s))
1283            deleted = list(set(old_labels))
1284            if len(deleted) > 0:
1285                self.handle_gaps(deleted, verbose=0)
1286
1287        if self.verbose > 1:
1288            ut.show_duration(start_time, "updated replaced tracks in ")

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

def handle_gaps(self, track_list, verbose=None):
1290    def handle_gaps(self, track_list, verbose=None):
1291        """Check and fix gaps in tracks"""
1292        if verbose is None:
1293            verbose = self.verbose
1294        gaped = self.tracking.check_gap(track_list, verbose=verbose)
1295        if len(gaped) > 0:
1296            if self.verbose > 0:
1297                print("Relabelling tracks with gaps")
1298            self.fix_gaps(gaped)

Check and fix gaps in tracks

def fix_gaps(self, gaps):
1300    def fix_gaps(self, gaps):
1301        """Fix when some gaps has been created in tracks"""
1302        for gap in gaps:
1303            gap_frames = self.tracking.gap_frames(gap)
1304            cur_gap = gap
1305            for gapy in gap_frames:
1306                new_value = self.get_free_label()
1307                self.replace_label(cur_gap, new_value, gapy)
1308                cur_gap = new_value

Fix when some gaps has been created in tracks

def swap_labels(self, lab, olab, frame):
1310    def swap_labels(self, lab, olab, frame):
1311        """Exchange two labels"""
1312        self.tracking.swap_frame_id(lab, olab, frame)

Exchange two labels

def swap_tracks(self, lab, olab, start_frame):
1314    def swap_tracks(self, lab, olab, start_frame):
1315        """Exchange two tracks"""
1316        ## split the two labels to unused value
1317        tmp_labels = self.get_free_labels(2)
1318        for i, laby in enumerate([lab, olab]):
1319            self.replace_label(laby, tmp_labels[i], start_frame)
1320
1321        ## replace the two initial labels, in inversed order
1322        self.replace_label(tmp_labels[0], olab, start_frame)
1323        self.replace_label(tmp_labels[1], lab, start_frame)

Exchange two tracks

def split_track(self, label, frame):
1325    def split_track(self, label, frame):
1326        """Split a track at given frame"""
1327        new_label = self.get_free_label()
1328        self.replace_label(label, new_label, frame)
1329        if self.verbose > 0:
1330            ut.show_info("Split track " + str(label) + " from frame " + str(frame))
1331        return new_label

Split a track at given frame

def update_changed_labels_img(self, img_before, img_after, added=True, removed=True):
1333    def update_changed_labels_img(self, img_before, img_after, added=True, removed=True):
1334        """Update tracks from changes between the two labelled images"""
1335        if self.verbose > 1:
1336            print("Updating changed labels from images")
1337        indmodif = np.argwhere(img_before != img_after).tolist()
1338        if len(indmodif) <= 0:
1339            return
1340        indmodif = tuple(np.array(indmodif).T)
1341        new_labels = img_after[indmodif]
1342        old_labels = img_before[indmodif]
1343        self.update_changed_labels(indmodif, new_labels, old_labels)

Update tracks from changes between the two labelled images

def added_labels_oneframe(self, frame, img_before, img_after):
1345    def added_labels_oneframe(self, frame, img_before, img_after):
1346        """Update added tracks between the two labelled images at frame"""
1347        ## Look for added labels
1348        added_labels = np.setdiff1d(img_after, img_before)
1349        self.tracking.add_one_frame(added_labels, frame, refresh=True)

Update added tracks between the two labelled images at frame

def removed_labels(self, img_before, img_after, frame=None):
1351    def removed_labels(self, img_before, img_after, frame=None):
1352        """Update removed tracks between the two labelled images"""
1353        ## Look for added labels
1354        deleted_labels = np.setdiff1d(img_before, img_after)
1355        if frame is None:
1356            self.tracking.remove_tracks(deleted_labels)
1357        else:
1358            self.tracking.remove_one_frame(track_id=deleted_labels.tolist(), frame=frame, handle_gaps=self.forbid_gaps)

Update removed tracks between the two labelled images

def remove_label(self, label, force=False):
1360    def remove_label(self, label, force=False):
1361        """Remove a given label if allowed"""
1362        ut.changeLabel(self.seglayer, label, 0)
1363        self.tracking.remove_tracks(label)
1364        self.seglayer.refresh()

Remove a given label if allowed

def remove_labels(self, labels, force=False):
1366    def remove_labels(self, labels, force=False):
1367        """Remove all allowed labels"""
1368        inds = []
1369        for lab in labels:
1370            # if (force) or (not self.locked_label(label)):
1371            inds = inds + ut.getLabelIndexes(self.seglayer.data, lab, None)
1372        ut.setNewLabel(self.seglayer, inds, 0)
1373        self.tracking.remove_tracks(labels)

Remove all allowed labels

def keep_labels(self, labels, force=True):
1375    def keep_labels(self, labels, force=True):
1376        """Remove all other labels that are not in labels"""
1377        inds = []
1378        toremove = list(set(self.tracking.get_track_list()) - set(labels))
1379        # for lab in self.tracking.get_track_list():
1380        #    if lab not in labels:
1381        # if (force) or (not self.locked_label(label)):
1382        for lab in toremove:
1383            inds = inds + ut.getLabelIndexes(self.seglayer.data, lab, None)
1384        #        toremove.append(lab)
1385        ut.setNewLabel(self.seglayer, inds, 0)
1386        self.tracking.remove_tracks(toremove)

Remove all other labels that are not in labels

def get_frame_features(self, frame):
1388    def get_frame_features(self, frame):
1389        """Measure the label properties of given frame"""
1390        return regionprops(self.seg[frame])

Measure the label properties of given frame

def updates_after_tracking(self):
1392    def updates_after_tracking(self):
1393        """When tracking has been done, update events, others"""
1394        self.inspecting.get_divisions()

When tracking has been done, update events, others

def get_all_groups(self, numeric=False):
1398    def get_all_groups(self, numeric=False):
1399        """Add all groups info"""
1400        if numeric:
1401            groups = [0] * self.nlabels()
1402        else:
1403            groups = ["None"] * self.nlabels()
1404        for igroup, gr in self.groups.keys():
1405            indexes = self.tracking.get_track_indexes(self.groups[gr])
1406            if numeric:
1407                groups[indexes] = igroup + 1
1408            else:
1409                groups[indexes] = gr
1410        return groups

Add all groups info

def get_groups(self, labels, numeric=False):
1412    def get_groups(self, labels, numeric=False):
1413        """Add the group info of the given labels (repeated)"""
1414        if numeric:
1415            groups = [0] * len(labels)
1416        else:
1417            groups = ["Ungrouped"] * len(labels)
1418        for lab in np.unique(labels):
1419            gr = self.find_group(lab)
1420            if gr is None:
1421                continue
1422            if numeric:
1423                gr = self.groups.keys().index() + 1
1424            indexes = (np.argwhere(labels == lab)).flatten()
1425            for ind in indexes:
1426                groups[ind] = gr
1427        return groups

Add the group info of the given labels (repeated)

def cells_ingroup(self, labels, group):
1429    def cells_ingroup(self, labels, group):
1430        """Put the cell "label" in group group, add it if new group"""
1431        presents = self.has_labels(labels)
1432        labels = np.array(labels)[presents]
1433        if group not in self.groups.keys():
1434            self.groups[group] = []
1435            self.update_group_lists()
1436        ## add only non present label(s)
1437        grlabels = self.groups[group]
1438        self.groups[group] = list(set(grlabels + labels.tolist()))

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

def group_of_labels(self):
1440    def group_of_labels(self):
1441        """List the group of each label"""
1442        res = {}
1443        for group, labels in self.groups.items():
1444            for label in labels:
1445                res[label] = group
1446        return res

List the group of each label

def find_group(self, label):
1448    def find_group(self, label):
1449        """Find in which group the label is"""
1450        for gr, labs in self.groups.items():
1451            if label in labs:
1452                return gr
1453        return None

Find in which group the label is

def cell_removegroup(self, label):
1455    def cell_removegroup(self, label):
1456        """Detach the cell from its group"""
1457        if not self.has_label(label):
1458            if self.verbose > 1:
1459                print("Cell " + str(label) + " missing")
1460        group = self.find_group(label)
1461        if group is not None:
1462            self.groups[group].remove(label)
1463            if len(self.groups[group]) <= 0:
1464                del self.groups[group]
1465                self.update_group_lists()

Detach the cell from its group

def update_group_lists(self):
1467    def update_group_lists(self):
1468        """Update all the lists depending on the group names"""
1469        if self.outputing is not None:
1470            self.outputing.update_selection_list()
1471        if self.editing is not None:
1472            self.editing.update_group_lists()

Update all the lists depending on the group names

def reset_group(self, group_name):
1474    def reset_group(self, group_name):
1475        """Reset/remove a given group"""
1476        if group_name == "All":
1477            self.reset_groups()
1478            return
1479        if group_name in self.groups.keys():
1480            del self.groups[group_name]
1481            self.update_group_lists()

Reset/remove a given group

def reset_groups(self):
1483    def reset_groups(self):
1484        """Remove all group information for all cells"""
1485        self.groups = {}
1486        self.update_group_lists()

Remove all group information for all cells

def draw_groups(self):
1488    def draw_groups(self):
1489        """Draw all the epicells colored by their group"""
1490        grouped = np.zeros(self.seg.shape, np.uint8)
1491        if (self.groups is None) or len(self.groups.keys()) == 0:
1492            return grouped
1493        for group, labels in self.groups.items():
1494            igroup = self.get_group_index(group) + 1
1495            np.place(grouped, np.isin(self.seg, labels), igroup)
1496        return grouped

Draw all the epicells colored by their group

def get_group_index(self, group):
1498    def get_group_index(self, group):
1499        """Get the index of group in the list of groups"""
1500        if group in list(self.groups.keys()):
1501            igroup = list(self.groups.keys()).index(group)
1502            return igroup
1503        return -1

Get the index of group in the list of groups

def only_current_roi(self, frame):
1506    def only_current_roi(self, frame):
1507        """Put 0 everywhere outside the current ROI"""
1508        roi_labels = self.editing.get_labels_inside()
1509        if roi_labels is None:
1510            return None
1511        # remove all other labels that are not in roi_labels
1512        roilab = np.copy(self.seg[frame])
1513        np.place(roilab, np.isin(roilab, roi_labels, invert=True), 0)
1514        return roilab

Put 0 everywhere outside the current ROI