epicure.Utils

Diverse functions for EpiCure

Proposes utility functions that do not depend on a class and can be usefull in several classes.

   1"""
   2    **Diverse functions for EpiCure**
   3
   4    Proposes utility functions that do not depend on a class and can be usefull in several classes.
   5"""
   6
   7import numpy as np
   8import os, sys
   9from sys import platform
  10import time
  11import math
  12from skimage.measure import label, regionprops, find_contours, regionprops_table
  13from skimage.segmentation import find_boundaries, expand_labels
  14from napari.utils.translations import trans # type: ignore
  15from napari.utils.notifications import show_info # type: ignore
  16from napari.utils import notifications as nt # type: ignore
  17from skimage.morphology import skeletonize, disk, binary_closing 
  18from scipy.ndimage import center_of_mass, find_objects
  19from scipy.ndimage import label as ndlabel
  20from scipy.ndimage import binary_opening as ndbinary_opening
  21from scipy.ndimage import sum as ndsum
  22from scipy.ndimage import generate_binary_structure as ndi_structure
  23from scipy import signal
  24from skimage.morphology import medial_axis
  25import pandas as pd
  26from epicure.laptrack_centroids import LaptrackCentroids
  27import tifffile as tif # type: ignore
  28import napari
  29from napari.utils import progress # type: ignore
  30from magicgui.widgets import TextEdit
  31from joblib import Parallel, delayed
  32from packaging.version import Version
  33
  34try:
  35    from skimage.graph import RAG
  36except:
  37    from skimage.future.graph import RAG  ## older version of scikit-image
  38
  39import skimage
  40if Version(skimage.__version__) > Version("0.25"):
  41    try:
  42        from skimage.morphology import dilation as binary_dilation 
  43    except:
  44        from skimage.morphology import binary_dilation
  45else:
  46    try:
  47        from skimage.morphology import binary_dilation
  48    except:
  49        from skimage.morphology import dilation as binary_dilation
  50
  51def show_info(message):
  52    """ Display info in napari """
  53    nt.show_info(message)
  54
  55def show_warning(message):
  56    """ Display a warning in napari (napari function show_warning doesn't work) """
  57    mynot = nt.Notification(message, nt.NotificationSeverity.WARNING)
  58    nt.notification_manager.dispatch(mynot)
  59
  60def show_error(message):
  61    """ Display an error in napari (napari function show_error doesn't work) """
  62    mynot = nt.Notification(message, nt.NotificationSeverity.ERROR)
  63    nt.notification_manager.dispatch(mynot)
  64
  65def show_debug(message):
  66    """ Display an info for debug in napari (napari function show_debug doesn't work) """
  67    print(message)
  68
  69def show_documentation():
  70    """ Open browser on main EpiCure documentation page """
  71    import webbrowser
  72    webbrowser.open_new_tab("https://image-analysis-hub.github.io/Epicure/")
  73    return
  74
  75def show_documentation_page(page):
  76    """ 
  77        Open browser on the selected page of EpiCure documentation 
  78        :param: page: name of the documentation page to go to (only the name of the page, without the full path)    
  79    """
  80    import webbrowser
  81    webbrowser.open_new_tab("https://image-analysis-hub.github.io/Epicure/"+page)
  82    return
  83
  84def show_progress( viewer, show ):
  85    """ Show.hide the napari activity bar to see processing progress """
  86    viewer.window._status_bar._toggle_activity_dock( show )
  87
  88def start_progress( viewer, total, descr=None ):
  89    """ Start the progress bar """
  90    show_progress( viewer, True)
  91    progress_bar = progress( total )
  92    if descr is not None:
  93        progress_bar.set_description( descr )
  94    return progress_bar
  95
  96def close_progress( viewer, progress_bar ):
  97    """ Close the progress bar """
  98    progress_bar.close()
  99    show_progress( viewer, False)
 100
 101def version_above( module, version ):
 102    """ Compare if python module is above a given version """
 103    return Version(module.__version__) > Version(version)
 104
 105#### Handle versions of napari
 106def version_napari_above( compare_version ):
 107    """ Compare if the current version of napari is above given version """
 108    return Version(napari.__version__) > Version(compare_version)
 109
 110def version_python_minor(version):
 111    """ Return if python version (minor, so 3.XX) is above given version """
 112    if int(sys.version_info[0]) != 3:
 113        show_warning("Python major version is not 3, not handled")
 114        return False
 115    return int(sys.version_info[1]) >= version
 116
 117def get_directory(imagepath):
 118    return os.path.dirname(imagepath)
 119
 120def extract_names(imagepath, subname="epics", mkdir=True):
 121    """
 122        From the image file path, extracts the name of the directoties to work in
 123
 124        :param: imagepath: file path to the main raw movie
 125        :param: subname (default: "epics"): name of the results directory where all will be saved
 126        
 127        :return: 
 128            - name of the raw movie without the extension, that will be used to save all other files
 129            - path to the directory where the raw movie is
 130            - path to the results directory on which to save all outputs
 131    """
 132    imgname = os.path.splitext(os.path.basename(imagepath))[0]
 133    imgdir = os.path.dirname(imagepath)
 134    resdir = os.path.join(imgdir, subname)
 135    if (not os.path.exists(resdir)) and mkdir:
 136        os.makedirs(resdir)
 137    return imgname, imgdir, resdir
 138
 139def extract_names_segmentation(segpath):
 140    """ Get the output directory and imagename from the segmentation filename """
 141    imgname = os.path.splitext(os.path.basename(segpath))[0]
 142    if imgname.endswith("_labels"):
 143        imgname = imgname[:(len(imgname)-7)]
 144    imgdir = os.path.dirname(segpath)
 145    return imgname, imgdir
 146    
 147def suggest_segfile(out, imgname):
 148    """ Check if a segmentation file from EpiCure already exists """
 149    segfile = os.path.join(out, imgname+"_labels.tif")
 150    if os.path.exists(segfile):
 151        return segfile
 152    else:
 153        return None
 154
 155def found_segfile( filepath ):
 156    """ Check if the segmentation file exists """
 157    return os.path.exists( filepath )
 158    
 159def get_filename(outdir, imgname):
 160    """ Join the directory with the filename """
 161    return os.path.join( outdir, imgname )
 162
 163def napari_info(text):
 164    """ Use napari information window to show a message """
 165    show_info(text)
 166
 167def create_text_window( name ):
 168    """ Create and display help text window """
 169    blabla = TextEdit()
 170    blabla.name = name 
 171    blabla.show()
 172    return blabla
 173
 174
 175def napari_shortcuts():
 176    """ Write main napari shortcuts list """
 177    text = "---- Main napari default shortcuts ----\n"
 178    text += " -- view options \n"
 179    if is_darwin():
 180        text += "  <Command+R> reset view \n"
 181        text += "  <Command+Y> switch 2D/3D view mode \n"
 182        text += "  <Command+G> switch Grid/Overlay view mode \n"
 183    else:
 184        text += "  <Ctrl+R> reset view \n"
 185        text += "  <Ctrl+Y> switch 2D/3D view mode \n"
 186        text += "  <Ctrl+G> switch Grid/Overlay view mode \n"
 187    text += "  <left arrow> got to previous frame \n"
 188    text += "  <right arrow> got to next frame \n"
 189    text += "\n"
 190    text += " -- labels options \n"
 191    text += "  <2> paint brush mode \n"
 192    text += "  <3> fill mode \n"
 193    text += "  <4> pick mode (select label) \n"
 194    text += "  <[> or <]> increase/decrease the paint brush size \n"
 195    text += "  <p> activate/deactivate preserve labels option \n"
 196    return text
 197
 198def removeOverlayText(viewer):
 199    """ Remove all texts that was overlaid on the main window """
 200    viewer.text_overlay.text = trans._("")
 201    viewer.text_overlay.visible = False
 202
 203def getOverlayText(viewer):
 204    """ Returns the current overlay text """
 205    return viewer.text_overlay.text
 206
 207def setOverlayText(viewer, text, size=10 ):
 208    """ 
 209    Set the overlay text
 210    :param: viewer: current napari view
 211    :param: text: new text to display as overlay
 212    :param: size: size of the displayed text
 213    """
 214    viewer.text_overlay.text = trans._(text)
 215    viewer.text_overlay.position = "top_left"
 216    viewer.text_overlay.visible = True
 217    if version_napari_above( "0.6.5" ):
 218        size = size - 2
 219    viewer.text_overlay.font_size = size
 220    viewer.text_overlay.color = "white"
 221    viewer.text_overlay.opacity = 1
 222    viewer.text_overlay.blending = "additive"
 223
 224def showOverlayText(viewer, vis=None):
 225    """
 226    Show the overlay text on/off
 227    :param: viewer: current napari viewer
 228    :param: vis: show it alternatively on/off if vis is None. Or can be a boolean to force the showing or not
 229    """
 230    if vis is None:
 231        viewer.text_overlay.visible = not viewer.text_overlay.visible
 232    else:
 233        viewer.text_overlay.visible = vis 
 234
 235def reactive_bindings(layer, mouse_drag, key_map):
 236    """ Reactive the mouse and key event bindings on layer """
 237    layer.mouse_drag_callbacks = mouse_drag
 238    layer.keymap.update(key_map)
 239
 240def clear_bindings(layer):
 241    """ Clear and returns the current event bindings on layer """
 242    old_mouse_drag = layer.mouse_drag_callbacks.copy()
 243    old_key_map = layer.keymap.copy()
 244    layer.mouse_drag_callbacks = []
 245    layer.keymap.clear()
 246    return old_mouse_drag, old_key_map
 247
 248def is_binary( img ):
 249    """ Test if more than 2 values (skeleton or labelled image) """
 250    return all(len(np.unique(frame)) <= 2 for frame in img)
 251
 252def set_frame(viewer, frame, scale=1):
 253    """ Set current frame """
 254    viewer.dims.set_point(0, frame*scale)
 255
 256def reset_view( viewer, zoom, center ):
 257    """ Reset the view to given camera center and zoom """
 258    viewer.camera.center = center
 259    viewer.camera.zoom = zoom
 260
 261def set_active_layer(viewer, layname):
 262    """ Set the current Napari active layer """
 263    if layname in viewer.layers:
 264        viewer.layers.selection.active = viewer.layers[layname]
 265
 266def set_visibility(viewer, layname, vis):
 267    """ Set visibility of layer layname if exists """
 268    if layname in viewer.layers:
 269        viewer.layers[layname].visible = vis
 270
 271def remove_layer(viewer, layname):
 272    """ Remove a layer with specific name from the viewer """
 273    if layname in viewer.layers:
 274        try:
 275            viewer.layers.remove(layname)
 276        except Exception as e:
 277            print("Remove of layer incomplete")
 278            print(e)
 279
 280def remove_widget(viewer, widname):
 281    """ Remove a widget from the viewer """
 282    if version_napari_above( "0.6.6" ):
 283        if widname in viewer.window.dock_widgets:
 284            import magicgui
 285            wid = viewer.window.dock_widgets[widname]
 286            if type(wid) is magicgui.widgets._function_gui.FunctionGui:
 287                viewer.window.remove_dock_widget( wid.native.parent() )
 288            else:
 289                viewer.window.remove_dock_widget[ wid.parent() ]
 290            del wid
 291    else:
 292        if widname in viewer.window._dock_widgets:
 293            wid = viewer.window._dock_widgets[widname]
 294            wid.setDisabled(True)
 295            try:
 296                wid.disconnect()
 297            except Exception:
 298                pass
 299            del viewer.window._dock_widgets[widname]
 300            wid.destroyOnClose()
 301
 302def remove_all_widgets( viewer ):
 303    """ Remove all widgets """
 304    viewer.window.remove_dock_widget("all")
 305
 306def get_metadata_field(metadata, fieldname):
 307    """ Read an imagej metadata string and get the value of fieldname """
 308    if metadata.index(fieldname+"=") < 0:
 309        return None
 310    submeta = metadata[metadata.index(fieldname+"=")+len(fieldname)+1:]
 311    value = submeta[0:submeta.index("\n")]
 312    return value
 313
 314def get_metadata_json(metadata, fieldname):
 315    """ Read a metadata from json of bioio-bioformats to get value of fieldname """
 316    if metadata.index("\""+fieldname+"\"=") < 0:
 317        return None
 318    submeta = metadata[metadata.index("\""+fieldname+"\"=")+len(fieldname)+3:]
 319    value = submeta[0:submeta.index(",")]
 320    return value
 321
 322
 323def open_image(imagepath, get_metadata=False, verbose=True):
 324    """ Open an image with bioio library """
 325    imagename, extension = os.path.splitext(imagepath)
 326    format = "all"
 327    if (extension==".tif") or (extension==".tiff"):
 328        if verbose:
 329            print("Opening Tif image "+str(imagepath)+" with bioio-tifffile")
 330        import bioio_tifffile
 331        if version_python_minor(10):
 332            from bioio import BioImage
 333            img = BioImage(imagepath, reader=bioio_tifffile.Reader)
 334        else:
 335            ## python 3.9 or under
 336            reader = bioio_tifffile.Reader
 337            img = reader(imagepath)
 338        format = "tif"
 339    else:
 340        import bioio_bioformats
 341        if verbose:
 342            print("Opening "+extension+" image "+str(imagepath)+" with bioio-bioformats")
 343        if version_python_minor(10):
 344            from bioio import BioImage
 345            img = BioImage(imagepath, reader=bioio_bioformats.Reader)
 346        else:
 347            ## python 3.9 or under
 348            reader = bioio_bioformats.Reader
 349            img = reader(imagepath)
 350    image = img.data
 351    if verbose:
 352        print(f"Loaded image shape: {image.shape}")
 353    if (len(image.shape) == 5):
 354        ## correct format of the image and metadata with TCZYX
 355        if (img.dims is not None) and len(img.dims.shape)==5 :
 356            if (img.dims.Z>1) and (img.dims.T == 1):
 357                print("Warning, movie had Z slices instead of T frames. EpiCure handles it but it might not be in other softwares/plugins")
 358                image = np.swapaxes(image, 0, 2)
 359    image = np.squeeze(image)
 360        
 361    if not get_metadata:
 362        return image, 0, 1, None, 1, None
 363
 364    try: 
 365        nchan = img.dims.C
 366        if nchan == 1:
 367            nchan = 0 ### was squeezed above
 368    except:
 369        nchan = 0
 370        pass
 371    
 372    ## spatial metadata
 373    scale_xy, unit_xy, scale_t, unit_t = None, None, None, None
 374    try:
 375        scale_xy = img.scale.X # img.physical_pixel_sizes
 376        unit_xy = img.dimension_properties.X.unit
 377    except:
 378        pass
 379
 380    try: 
 381        if unit_xy is None:
 382            if format == "all":
 383                unit_xy = get_metadata_json(img.metadata.json(), "physical_size_x_unit")
 384            elif format == "tif":
 385                unit_xy = get_metadata_field(img.metadata, "physical_size_x_unit")
 386    except:
 387        print("Reading spatial metadata might have failed. Check it manually")
 388        if scale_xy is None:
 389            scale_xy = 1
 390
 391    ## temporal metadata 
 392    try:
 393        scale_t = img.scale.T
 394        unit_t = img.dimension_properties.T.unit
 395    except:
 396        pass
 397
 398    try: 
 399        if scale_t is None:
 400                # read it from the metadata field (string) 
 401            if format == "all":
 402                scale_t = get_metadata_json(img.metadata.json(), "time_increment_unit")
 403                scale_t = float(scale_t)
 404                unit_t = get_metadata_json(img.metadata.json(), "time_increment")
 405            elif format == "tif":
 406                scale_t = get_metadata_field(img.metadata, "finterval")
 407                scale_t = float(scale_t)
 408                unit_t = get_metadata_field(img.metadata, "tunit")
 409    except:
 410        print("Reading temporal metadata might have failed. Check it manually")
 411        if scale_t is None:
 412            scale_t = 1
 413    if unit_xy is None:
 414        unit_xy = "um"
 415    if unit_t is None:
 416        unit_t = "min"
 417    return image, nchan, scale_xy, unit_xy, scale_t, unit_t
 418
 419def writeTif(img, imgname, scale, imtype, what=""):
 420    """ Write image in tif format """
 421    #TODO: change to make it with bioio
 422    if len(img.shape) == 2:
 423        tif.imwrite(imgname, np.array(img, dtype=imtype), imagej=True, resolution=[1./scale, 1./scale], metadata={'unit': 'um', 'axes': 'YX'})
 424    else:
 425        try:
 426            tif.imwrite(imgname, np.array(img, dtype=imtype), imagej=True, resolution=[1./scale, 1./scale], metadata={'unit': 'um', 'axes': 'TYX'}, compression="zstd")
 427        except:
 428            tif.imwrite(imgname, np.array(img, dtype=imtype), imagej=True, resolution=[1./scale, 1./scale], metadata={'unit': 'um', 'axes': 'TYX'})
 429    show_info(what+" saved in "+imgname)
 430
 431def appendToTif(img, imgname):
 432    """ Append to RGB tif the current image """
 433    tif.imwrite(imgname, img, photometric="rgb", append=True)
 434
 435def getCellValue(label_layer, event):
 436    """ Get the label under the click """
 437    vis = label_layer.visible
 438    if vis == False:
 439        label_layer.visible = True
 440    label = label_layer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 441    if vis == False:
 442        ## put it back to not visible state
 443        label_layer.visible = vis
 444    return label
 445
 446def setCellValue(layer, label_layer, event, newvalue, layer_frame=None, label_frame=None):
 447    """ Get the cell concerned by the event and replace its value by new one"""
 448    # get concerned label (under the cursor), layer has to be visible for this
 449    vis = label_layer.visible
 450    if vis == False:
 451        label_layer.visible = True
 452    label = label_layer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 453    label_layer.visible = vis
 454    if label is not None and label > 0:
 455        # if the seg image is 2D (single frame), label_frame will be None
 456        if label_frame is not None and label_frame >= 0:
 457            ldata = label_layer.data[label_frame,:,:]
 458        else:
 459            ldata = label_layer.data
 460        # if the layer is 2D (single frame), layer_frame will be None
 461        if layer_frame is not None and layer_frame >= 0:
 462            #slice_coord = tuple(sc[keep_coords] for sc in slice_coord)
 463            cdata = layer.data[layer_frame,:,:]
 464        else:
 465            cdata = layer.data
 466            #slice_coord = tuple(sc[keep_coords] for sc in slice_coord)
 467
 468        cdata[np.where(ldata==label)] = newvalue
 469        layer.refresh()
 470        return label
 471
 472def thin_seg_one_frame( segframe ):
 473    """ Boundaries of the frame one pixel thick """
 474    bin_img = binary_closing( find_boundaries(segframe, connectivity=2, mode="outer"), footprint=np.ones((3,3)) )
 475    skel = skeletonize( bin_img )
 476    skel = copy_border( skel, bin_img )
 477    return skeleton_to_label( skel, segframe )
 478    
 479def copy_border( skel, bin ):
 480    """ Copy the pixel border onto skeleton image """
 481    skel[[0, -1], :] = bin[[0, -1], :]  # top and bottom borders
 482    skel[:, [0, -1]] = bin[:, [0, -1]]  # left and right borders
 483    return skel
 484
 485def draw_points(pts, imshape, radius):
 486    """ Draw circle (2D) around the given points in 2D image """  
 487    image = np.zeros(imshape, dtype=bool)
 488    y, x = np.ogrid[:imshape[0], :imshape[1]]
 489    for pt in pts:
 490        # Calculate distance from pt, scaled to compare to radius
 491        distances_sq = ((y - pt[0]))**2 + ((x - pt[1]))**2 
 492        image |= distances_sq <= radius**2
 493    return image
 494
 495def get_vertices(seg, viewer=None, verbose=0, parallel=0):
 496    """ Get the vertices of the segmentation """
 497    skeleton = get_skeleton(seg, viewer, verbose, parallel)
 498    convfilter = np.array([[-1,-1,-1], [-1,3,-1],[-1,-1,-1]])
 499    novert = np.zeros(skeleton.shape, dtype=np.int8)
 500    ## pure skeleton
 501    for ind, skel in enumerate(skeleton):
 502        skeleton[ind] = medial_axis(skel)
 503        novert[ind] = signal.convolve2d(skeleton[ind], convfilter, mode="same")
 504    novert[novert<=0] = 0
 505    nodeimg = skeleton - novert
 506    nodeimg[nodeimg<=0] = 0
 507    nodeimg[nodeimg>0] = 1 
 508    return nodeimg 
 509
 510    
 511def get_skeleton( seg, viewer=None, verbose=0, parallel=0 ) :
 512    """ convert labels movie to skeleton (thin boundaries) """
 513    startt = start_time()
 514    if viewer is not None:
 515        show_progress( viewer, show=True )
 516
 517    def frame_skeleton( frame ):
 518        """ Calculate skeleton on one frame """
 519        expz = expand_labels( frame, distance=1 )
 520        frame_skel = np.zeros( frame.shape, dtype="uint8" )
 521        frame_skel[ (frame==0) * (expz>0) ] = 1
 522        return frame_skel
 523        
 524    if parallel > 0:
 525        skel = Parallel( n_jobs=parallel )(
 526            delayed(frame_skeleton)(frame) for frame in seg
 527        )
 528        skel = np.array(skel)
 529    else:
 530        skel = np.zeros(seg.shape, dtype="uint8")
 531        for z in progress(range(seg.shape[0])):
 532            expz = expand_labels( seg[z], distance=1 )
 533            skel[z][(seg[z] == 0) *(expz > 0)] = 1
 534    if verbose > 0:
 535        show_duration(startt, header="Skeleton calculted in ")
 536    if viewer is not None:
 537        show_progress( viewer, show=False )
 538    return skel
 539
 540
 541def setLabelValue(layer, label_layer, event, newvalue, layer_frame=None, label_frame=None):
 542    """ Change the label value under event position and returns its old value """
 543    ## get concerned label (under the cursor), layer has to be visible for this
 544    vis = label_layer.visible
 545    if vis == False:
 546        label_layer.visible = True
 547    label = label_layer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 548    label_layer.visible = vis
 549    
 550    if label > 0:
 551        inds = getLabelIndexes( label_layer.data, label, label_frame )
 552        setNewLabel(layer, inds, newvalue, add_frame=layer_frame)
 553        layer.refresh()
 554        return label
 555    return None
 556
 557def getLabelIndexes(label_data, label, frame):
 558    """ Get the indixes at which label_layer is label for given frame """
 559    # if the seg image is 2D (single frame), frame will be None
 560    if frame is not None and frame >= 0:
 561        ldata = label_data[frame,:,:]
 562    else:
 563        ldata = label_data
 564    return np.argwhere( ldata==label ).tolist()
 565
 566def getLabelIndexesInFrame(frame_data, label):
 567    """ Get the indexes at which frame data is label """
 568    # if the seg image is 2D (single frame), frame will be None
 569    return np.argwhere( frame_data==label ).tolist()
 570
 571def changeLabel( label_layer, old_value, new_value ):
 572    """ replace the value of label old-value by new_value """
 573    index = np.argwhere( label_layer.data==old_value ).tolist()
 574    setNewLabel( label_layer, index, new_value )
 575
 576def setNewLabel(label_layer, indices, newvalue, add_frame=None, return_old=True):
 577    """ Change the label of all the pixels indicated by indices """
 578    indexs = np.array(indices).T
 579    if add_frame is not None:
 580        indexs = np.vstack((np.repeat(add_frame, indexs.shape[1]), indexs))
 581    changed_indices = label_layer.data[tuple(indexs)] != newvalue
 582    inds = tuple(x[changed_indices] for x in indexs)
 583    oldvalues = None
 584    if return_old:
 585        oldvalues = label_layer.data[inds]
 586    if isinstance(newvalue, list):
 587        newvalue = np.array(newvalue)[np.where(changed_indices)[0]]
 588    label_layer.data_setitem( inds, newvalue )
 589    return inds, newvalue, oldvalues 
 590
 591def convert_coords( coord ):
 592    """ Get the time frame, and the 2D coordinates as int """
 593    int_coord = tuple(np.round(coord).astype(int))
 594    tframe = int(coord[0])
 595    int_coord = int_coord[1:3]
 596    return tframe, int_coord
 597
 598def outerBBox2D(bbox, imshape, margin=0):
 599    if (bbox[0]-margin) <= 0:
 600        return True
 601    if (bbox[2]+margin) >= imshape[0]:
 602        return True
 603    if (bbox[1]-margin) <= 0:
 604        return True
 605    if (bbox[3]+margin) >= imshape[1]:
 606        return True
 607    return False
 608
 609def isInsideBBox( bbox, obbox ):
 610    """ Check if bbox is included in obbox """
 611    if (bbox[0] >= obbox[0]) and (bbox[1] >= obbox[1]):
 612        return (bbox[2] <= obbox[2]) and (bbox[3] <= obbox[3])
 613    return False
 614
 615def setBBox(position, extend, imshape):
 616    bbox = [
 617        max(int(position[0] - extend), 0),
 618        max(int(position[1] - extend), 0),
 619        max(int(position[2] - extend), 0),
 620        min(int(position[0] + extend), imshape[0]),
 621        min(int(position[1] + extend), imshape[1]),
 622        min(int(position[2] + extend), imshape[2])
 623    ]
 624    return bbox
 625
 626def setBBoxXY(position, extend, imshape):
 627    bbox = [
 628        max(int(position[0]), 0),
 629        max(int(position[1] - extend), 0),
 630        max(int(position[2] - extend), 0),
 631        min(int(position[0] + 1), imshape[0]),
 632        min(int(position[1] + extend), imshape[1]),
 633        min(int(position[2] + extend), imshape[2])
 634    ]
 635    return bbox
 636
 637def getBBox2DFromPts(pts, extend, imshape):
 638    """ Get the bounding box surrounding all the points, plus a margin """
 639    arr = np.array(pts)
 640    ptsdim = arr.shape[1]
 641    if ptsdim == 2:
 642        bbox = [
 643            max( int(np.min(arr[:,0])) - extend, 0), 
 644            max( int(np.min(arr[:,1])) - extend, 0), 
 645            min( int(np.max(arr[:,0]))+1+extend, imshape[0]), 
 646            min( int(np.max(arr[:,1]))+1+extend, imshape[1] )
 647            ]
 648    if ptsdim == 3:
 649        bbox = [
 650            max( int(np.min(arr[:,1])) -extend, 0), 
 651            max( int(np.min(arr[:,2])) - extend, 0),
 652            min( int(np.max(arr[:,1]))+1 + extend, imshape[0]), 
 653            min( int(np.max(arr[:,2]))+1 + extend, imshape[1] )
 654            ]
 655
 656    return bbox
 657
 658def getBBoxFromPts(pts, extend, imshape, outdim=None, frame=None):
 659    arr = np.array(pts)
 660    ## get if points are 2D or 3D
 661    ptsdim = arr.shape[1]
 662    ## if not imposed, output the same dimension as input points
 663    if outdim is None:
 664        outdim = ptsdim
 665    ## Get bounding box from points according to dimensions
 666    if ptsdim == 2:
 667        if outdim == 2:
 668            bbox = [int(np.min(arr[:,0])), int(np.min(arr[:,1])), int(np.max(arr[:,0]))+1, int(np.max(arr[:,1]))+1]
 669        else:
 670            bbox = [frame, int(np.min(arr[:,0])), int(np.min(arr[:,1])), frame+1, int(np.max(arr[:,0]))+1, int(np.max(arr[:,1]))+1]
 671    if ptsdim == 3:
 672        if outdim == 2:
 673            bbox = [int(np.min(arr[:,1])), int(np.min(arr[:,2])), int(np.max(arr[:,1]))+1, int(np.max(arr[:,2]))+1]
 674        else:
 675            bbox = [int(np.min(arr[:,0])), int(np.min(arr[:,1])), int(np.min(arr[:,2])), int(np.max(arr[:,0]))+1, int(np.max(arr[:,1]))+1, int(np.max(arr[:,2]))+1]
 676    if extend > 0:
 677        for i in range(outdim):
 678            if i < 2:
 679                bbox[(outdim==3)+i] = max( bbox[(outdim==3)+i] - extend, 0)
 680                bbox[(outdim==3)+i+outdim] = min(bbox[(outdim==3)+i+outdim] + extend, imshape[(outdim==3)+i] )
 681    return bbox
 682
 683def inside_bounds( pt, imshape ):
 684    """ Check if given point is inside image limits """
 685    return all(0 <= pt[i] < imshape[i] for i in range(len(pt)))
 686
 687def extendBBox2D( bbox, extend_factor, imshape ):
 688    """ Extend bounding box with given margin """
 689    extend = max(bbox[2] - bbox[0], bbox[3] - bbox[1]) * extend_factor
 690    bbox = np.array(bbox)
 691    bbox[:2] = np.maximum(bbox[:2] - extend, 0)
 692    bbox[2:] = np.minimum(bbox[2:] + extend, imshape[:2])
 693    return bbox
 694
 695def getBBox2D(img, label):
 696    """ Get bounding box of label """
 697    mask = (img==label)*1
 698    props = regionprops(mask)
 699    for prop in props:
 700        bbox = prop.bbox
 701        return bbox
 702
 703def getPropLabel(img, label):
 704    """ Get the properties of label """
 705    mask = np.uint8(img == label)
 706    props = regionprops(mask)
 707    return props[0]
 708
 709def getBBoxLabel(img, label):
 710    """ Get bounding box of label """
 711    mask_ind = np.where(img==label)
 712    if len(mask_ind) <= 0:
 713        return None
 714    dim = len(img.shape)
 715    bbox = np.zeros(dim*2, int)
 716    for i in range(dim):
 717        bbox[i] = int(np.min(mask_ind[i]))
 718        bbox[i+dim] = int(np.max(mask_ind[i]))+1
 719    return bbox
 720
 721def getBBox2DMerge(img, label, second_label): #, checkTouching=False):
 722    """ Get bounding box of two labels and check if they are in contact """
 723    mask = np.isin( img, [label, second_label] )
 724    props = regionprops(mask*1)
 725    return props[0].bbox, mask 
 726
 727
 728def frame_to_skeleton(frame, connectivity=1):
 729    """ convert labels frame to skeleton (thin boundaries) """
 730    return skeletonize( find_boundaries(frame, connectivity=connectivity, mode="outer") )
 731
 732def remove_boundaries(img):
 733    """ Put the boundaries pixels between labels as 0 """
 734    bound = frame_to_skeleton( img, connectivity=1 )
 735    img[bound>0] = 0
 736    return img
 737
 738def ind_boundaries(img):
 739    """ Get indices of the boundaries pixels between two labels """
 740    bound = frame_to_skeleton( img, connectivity=1 )
 741    return np.argwhere(bound>0)
 742
 743def checkTouchingLabels(img, label, second_label):
 744    """ Returns if labels are in contact (1-2 pixel away) """
 745    disk_one = disk(radius=1)
 746    maska = binary_dilation(img==label, footprint=disk_one)
 747    maskb = binary_dilation(img==second_label, footprint=disk_one)
 748    return np.any(maska & maskb)
 749
 750def positionsIn2DBBox( positions, bbox ):
 751    """ Shift all the positions to their position inside the 2D bounding box """
 752    return [positionIn2DBBox( pos, bbox ) for pos in positions ]
 753
 754def positions2DIn2DBBox( positions, bbox ):
 755    """ Shift all the positions to their position inside the 2D bounding box """
 756    return [position2DIn2DBBox( pos, bbox ) for pos in positions ]
 757
 758def positionIn2DBBox(position, bbox):
 759    """ Returns the position shifted to its position inside the 2D bounding box """
 760    return (int(position[1]-bbox[0]), int(position[2]-bbox[1]))
 761
 762def position2DIn2DBBox(position, bbox):
 763    """ Returns the position shifted to its position inside the 2D bounding box """
 764    return (int(position[0]-bbox[0]), int(position[1]-bbox[1]))
 765
 766def toFullImagePos(indices, bbox):
 767    indices = np.array(indices)
 768    return np.column_stack((indices[:, 0] + bbox[0], indices[:, 1] + bbox[1])).tolist()
 769
 770def addFrameIndices( indices, frame ):
 771    return [ [frame, ind[0], ind[1]] for ind in indices ]
 772
 773def shiftFrameIndices( indices, add_frame ):
 774    if isinstance( indices, list ):
 775        indices = np.array(indices)
 776    indices[:, 0] += add_frame
 777    return indices.tolist()
 778
 779def shiftFrames( indices, frames ):
 780    if isinstance( indices, list ):
 781        indices = np.array(indices)
 782    indices[:, 0] = frames[indices[:, 0]]
 783    return indices.tolist()
 784
 785def toFullMoviePos( indices, bbox, frame=None ):
 786    """ Replace indexes inside bounding box to full movie indexes """
 787    indices = np.array(indices)
 788    if frame is not None:
 789        frame_arr = np.full(len(indices), frame)
 790        return np.column_stack((frame_arr, indices[:, 0] + bbox[0], indices[:, 1] + bbox[1]))
 791    if len(bbox) == 6:
 792        return np.column_stack((indices[:, 0] + bbox[0], indices[:, 1] + bbox[1], indices[:, 2] + bbox[2]))
 793    return np.column_stack((indices[:, 0], indices[:, 1] + bbox[0], indices[:, 2] + bbox[1]))
 794
 795def cropBBox(img, bbox):
 796    slices = tuple(slice(bbox[i], bbox[i + len(bbox) // 2]) for i in range(len(bbox) // 2))
 797    return img[slices]
 798
 799def crop_twoframes( img, bbox, frame ):
 800    """ Crop bounding box with two frames """
 801    return np.copy(img[(frame-1):(frame+1), bbox[0]:bbox[2], bbox[1]:bbox[3]])
 802
 803def cropBBox2D(img, bbox):
 804    return img[bbox[0]:bbox[2], bbox[1]:bbox[3]]
 805
 806def setValueInBBox2D(img, setimg, bbox):
 807    bbimg = img[bbox[0]:bbox[2], bbox[1]:bbox[3]] 
 808    bbimg[setimg>0]= setimg[setimg>0]
 809
 810def addValueInBBox(img, addimg, bbox):
 811    img[bbox[0]:bbox[3], bbox[1]:bbox[4], bbox[2]:bbox[5]] = img[bbox[0]:bbox[3], bbox[1]:bbox[4], bbox[2]:bbox[5]] + addimg
 812
 813def set_maxlabel(layer):
 814    layer.mode = "PAINT"
 815    layer.selected_label = np.max(layer.data)+1
 816    layer.refresh()
 817
 818def set_label(layer, lab):
 819    layer.mode = "PAINT"
 820    layer.selected_label = lab
 821    layer.refresh()
 822
 823def get_free_labels( used, nlab ):
 824    """ Get n-th unused label (not in used list) """
 825    maxlab = max(used)+1
 826    unused = list(set(range(1, maxlab)) - set(used))
 827    if nlab < len(unused):
 828        return unused[0:nlab]
 829    else:
 830        return unused+list(range(maxlab+1, maxlab+1+(nlab-len(unused))))
 831
 832def get_next_label(layer, label):
 833    """ Get the next unused label starting from label """
 834    used = np.unique(layer.data)
 835    i = label+1
 836    while i < np.max(used):
 837        if i>0 and (i not in used):
 838            return i
 839        i = i + 1
 840    return i+1
 841
 842def relabel_layer(layer):
 843    maxlab = np.max(layer.data)
 844    used = np.unique(layer.data)
 845    nlabs = len(used)
 846    if nlabs == maxlab:
 847        #print("already relabelled")
 848        return
 849    for j in range(1, nlabs+1):
 850        if j not in used:
 851            layer.data[layer.data==maxlab] = j
 852            maxlab = np.max(layer.data)
 853    show_info("Labels reordered")
 854    layer.refresh()
 855
 856def inv_visibility(viewer, layername):
 857    """ Switch the visibility of a layer """
 858    if layername in viewer.layers:
 859        layer = viewer.layers[layername]
 860        layer.visible = not layer.visible
 861
 862######## Measure labels
 863def average_area( seg ):
 864    """ Average area of labels (cells) """
 865    # Label the input image
 866    labeled_array, num_features = ndlabel(seg)
 867    
 868    if num_features == 0:
 869        return 0.0
 870    
 871    # Calculate the area of each label
 872    areas = ndsum(seg > 0, labeled_array, index=np.arange(1, num_features + 1))
 873    # Calculate the average area
 874    avg_area = np.mean(areas)   
 875    return avg_area
 876
 877
 878def summary_labels( seg ):
 879    """ Summary of labels (cells) measurements """
 880    props = regionprops(seg)
 881    avg_duration = 0
 882    avg_area = 0.0
 883    for prop in props:
 884        bbox = prop.bbox
 885        nz = 1
 886        if len(bbox)>4:
 887            nz = bbox[3]-bbox[0]
 888        avg_duration += nz
 889        avg_area += prop.area/nz
 890    return len(props), avg_duration/len(props), avg_area/len(props) 
 891
 892def labels_in_cell( sega, segb, label ):
 893    """ Look at the labels of segb inside label from sega """
 894    cell = np.isin( sega, [label] )
 895    labelb = segb[ cell ]
 896    cell_area = np.sum( cell*1, axis=None )
 897    filled_area = np.sum( labelb>0 )
 898    nobj = len(np.unique( labelb ))
 899    if 0 in labelb:
 900        nobj = nobj - 1
 901    return nobj, (filled_area/cell_area), np.unique(labelb)
 902
 903
 904def match_labels( sega, segb ):
 905    """ Match the labels of the two segmentation images """
 906    region_properties = ["label", "centroid"]
 907
 908    df0 = pd.DataFrame( regionprops_table( sega, properties=region_properties ) )
 909    df0["frame"] = 0
 910    df1 = pd.DataFrame( regionprops_table( segb, properties=region_properties ) )
 911    df1["frame"] = 1
 912    df = pd.concat([df0, df1])
 913
 914    ## Link the two frames with LapTrack tracking
 915    laptrack = LaptrackCentroids(None, None)
 916    laptrack.max_distance = 10 
 917    laptrack.set_region_properties(with_extra=False)
 918    laptrack.splitting_cost = False ## disable splitting option
 919    laptrack.merging_cost = False ## disable merging option
 920    labels = list(np.unique(segb))
 921    if 0 in labels:
 922        labels.remove(0)
 923    parent_labels = laptrack.twoframes_track(df, labels)
 924    return parent_labels, labels
 925
 926def labels_table( labimg, intensity_image=None, properties=None, extra_properties=None ):
 927    """ Returns the regionprops_table of the labels """
 928    if properties is None:
 929        properties = ['label', 'centroid']
 930    if intensity_image is not None:
 931        return regionprops_table( labimg, intensity_image=intensity_image, properties=properties, extra_properties=extra_properties )
 932    return regionprops_table( labimg, properties=properties, extra_properties=extra_properties )
 933
 934def labels_to_table( labimg, frame ):
 935    """ Get label and centroid """
 936    labels = np.unique(labimg.ravel())
 937    labels = labels[labels != 0]
 938    centroids = center_of_mass(labimg, labels=labimg, index=labels)
 939    table = np.column_stack((labels, np.full(len(labels), frame), centroids))
 940    return table.astype(int)
 941
 942def labels_to_table_v1( labimg, frame ):
 943    """ Get label and centroid """
 944    props = regionprops( labimg )
 945    n = len(props)
 946    if n == 0:
 947        return np.empty( (0, 2+labimg.ndim) )
 948    res = np.zeros( (n, 2+labimg.ndim), dtype=int )
 949    for i, prop in enumerate(props):
 950        res[i, 0] = prop.label
 951        res[i, 1] = frame
 952        res[i,:2] = np.array(prop.centroid).astype(int)
 953    return res
 954
 955def non_unique_labels( labimg ):
 956    """ Check if contains only unique labels """
 957    relab, nlabels = ndlabel( labimg )
 958    return nlabels > (len( np.unique(labimg) )-1)
 959
 960def reset_labels( labimg, closing=True ):
 961    """ Relabel in 3D all labels (unique labels) """
 962    s = ndi_structure(3,1)
 963    ## ignore 3D connectivity (unique labels in all frames)
 964    s[0,:,:] = 0
 965    s[2,:,:] = 0
 966    if closing:
 967        labimg = ndbinary_opening( labimg, iterations=1, structure=s )
 968    lab = ndlabel( labimg, structure=s )[0]
 969    return lab
 970
 971    
 972def skeleton_to_label( skel, labelled ):
 973    """ Transform a skeleton to label image with numbers from labelled image """
 974    labels = ndlabel( np.invert(skel) )[0]
 975    new_labels = find_objects( labels )
 976    newlab = np.zeros( skel.shape, np.uint32 )   
 977    for i, obj_slice in enumerate(new_labels):
 978        if (obj_slice is not None):
 979            if ((obj_slice[1].stop-obj_slice[1].start) <= 2) and ((obj_slice[0].stop-obj_slice[0].start) <= 2):
 980                continue
 981            label_mask = labels[obj_slice] == (i+1)
 982            label_values = labelled[obj_slice][label_mask]
 983            labvals, counts = np.unique(label_values, return_counts=True )
 984            labval = labvals[ np.argmax(counts) ]
 985            newlab[obj_slice][label_mask] = labval
 986    return newlab
 987
 988def get_most_frequent( labimg, img, label ):
 989    """ Returns which label is the most frequent in mask """
 990    mask = labimg == label
 991    vals, counts = np.unique( img[mask], return_counts=True )
 992    return vals[ np.argmax(counts) ]
 993
 994def binary_properties( labimg ):
 995    """ Returns basic label properties """
 996    return regionprops( label(labimg) )
 997
 998def labels_properties( labimg ):
 999    """ Returns basic label properties """
1000    return regionprops( labimg )
1001
1002def labels_bbox( labimg ):
1003    """ Returns for each label its bounding box """
1004    return regionprops_table( labimg, properties=('label', 'bbox') )
1005
1006def tuple_int(pos):
1007    if len(pos) == 3:
1008        return ( (int(pos[0]), int(pos[1]), int(pos[2])) )
1009    if len(pos) == 2:
1010        return ( (int(pos[0]), int(pos[1])) )
1011
1012def get_consecutives( ordered ):
1013    """ Returns the list of consecutives integers (already sorted) """
1014    gaps = [ [start, end] for start, end in zip( ordered, ordered[1:] ) if start+1 < end ]
1015    edges = iter( ordered[:1] + sum(gaps, []) + ordered[-1:] )
1016    return list( zip(edges, edges) )
1017
1018
1019def prop_to_pos(prop, frame):
1020    return np.array( (frame, int(prop.centroid[0]), int(prop.centroid[1])) )
1021
1022def current_frame(viewer):
1023    return int(viewer.cursor.position[0])
1024
1025def distance( x, y ):
1026    """ 2d distance """
1027    return math.sqrt( (x[0]-y[0])*(x[0]-y[0]) + (x[1]-y[1])*(x[1]-y[1]) )
1028
1029def interm_position( prop, a, b ):
1030    res = [0,0]
1031    res[0] = a[0] + prop*(b[0]-a[0])
1032    res[1] = a[1] + prop*(b[1]-a[1])
1033    return res
1034
1035def nb_frames( seg, lab ):
1036    """ Return nb frames with label lab """
1037    labseg = seg==lab
1038    return np.sum( np.any(labseg, axis=(1,2)) )
1039
1040def keep_orphans( img, comp_img, klabels ):
1041    """ Keep only labels that doesn't have a follower """
1042    valid_labels = np.setdiff1d(img[0], klabels)
1043    if (len(valid_labels)==1) and (valid_labels[0]==0):
1044        return
1045    labels = [val for val in valid_labels if (val!=0) and np.any(comp_img==val)]
1046    mask = np.isin(img, labels)
1047    img[mask] = 0
1048
1049def keep_orphans_3d( img, klabels ):
1050    """ Keep only orphans labels or lab and olab """
1051    for label in np.unique(img[1]):
1052        if label not in klabels:
1053            if nb_frames( img, label ) == 2:
1054                img[img==label] = 0
1055    return img
1056
1057def mean_nonzero( array ):
1058    nonzero = np.count_nonzero(array)
1059    if nonzero > 0:
1060        return np.sum(array)/nonzero
1061    return 0
1062
1063def get_contours( binimg ):
1064    """ Return the contour of a binary shape """
1065    return find_contours( binimg )
1066
1067###### Connectivity labels
1068def touching_labels( img, expand=3 ):
1069    """ Extends the labels to make them touch """
1070    return expand_labels( img, distance=expand )
1071
1072def connectivity_graph( img, distance ):
1073    """ Returns the region adjancy graph of labels """
1074    touchlab = touching_labels( img, expand=distance )
1075    return RAG( touchlab, connectivity=2 )
1076
1077def get_neighbor_graph( img, distance ):
1078    """ Returns the adjancy graph without bg, so only neigbor cells """
1079    graph = connectivity_graph( img, distance=distance ) # be sure that labels touch and get the graph
1080    graph.remove_node(0) if 0 in graph.nodes else None
1081    return graph
1082
1083def get_neighbors( label, graph ):
1084    """ Get the list of neighbors of cell 'label' from the graph """
1085    if label in graph.nodes:
1086        return list(graph.adj[label])
1087    return []
1088    
1089def get_boundary_cells( img ):
1090    """ Return cells on tissu boundary in current image """ 
1091    dilated = binary_dilation( img > 0, disk(3) )
1092    zero = np.invert( dilated )
1093    zero = binary_dilation( zero, disk(5) )
1094    touching = np.unique( img[ zero ] ).tolist()
1095    if 0 in touching:
1096        touching.remove(0)
1097    return touching
1098    
1099def get_border_cells( img ):
1100    """ Return cells on border in current image """ 
1101    height = img.shape[1]
1102    width = img.shape[0]
1103    labels = list( np.unique( img[ :, 0:2 ] ) )   ## top border
1104    labels += list( np.unique( img[ :, (height-2): ] ) )   ## bottom border
1105    labels += list( np.unique( img[ 0:2,] ) )   ## left border
1106    labels += list( np.unique( img[ (width-2):,] ) )   ## right border
1107    labels = list( np.unique(labels) )
1108    return labels
1109
1110def count_neighbors( label_img, label ):
1111    """ Get the number of neighboring labels of given label """
1112    ## much slower than using the RAG graph
1113    # Dilate the labeled image
1114    dilated_mask = binary_dilation( label_img==label, disk(1) )
1115    nonzero = np.nonzero( dilated_mask)
1116        
1117    # Find the unique labels in the dilated region, excluding the current label and background
1118    neighboring_labels = np.unique( label_img[nonzero] ).tolist()
1119        
1120    # Add the number of unique neighboring labels
1121    return len(neighboring_labels) - 1 - 1*(0 in neighboring_labels) ## don't count itself or 0
1122
1123def get_cell_radius( label, labimg ):
1124    """ Get the radius of the cell label in labimg (2D) """
1125    area = np.sum( labimg == label )
1126    return math.sqrt( area / math.pi )
1127
1128
1129####### Distance measures
1130
1131def consecutive_distances( pts_pos ):
1132    """ Distance travelled by the cell between each frame """
1133    diff = np.diff( pts_pos, axis=0 )
1134    disp = np.linalg.norm(diff, axis=1)
1135    return disp
1136
1137def velocities( pts_pos ):
1138    """ Velocity of the cell between each frame (average between previous and next) """
1139    diff = np.diff( pts_pos, axis=0 ).astype(float)
1140    diff = np.vstack( (diff[0], diff) )
1141    diff = np.vstack( (diff, diff[-1]) )
1142    kernel=np.array([0.5,0.5])
1143    adiff = np.zeros( (len(diff)+1, 3) )
1144    for i in range(3):
1145        adiff[:,i] = np.convolve( diff[:,i], kernel )
1146    adiff = adiff[1:-1]
1147    disp = np.linalg.norm(adiff[:,1:3], axis=1)
1148    dt = adiff[:,0] 
1149    return disp/dt
1150
1151def total_distance( pts_pos ):
1152    """ Total distance travelled by point with coordinates xpos and ypos """
1153    diff = np.diff( pts_pos, axis=0 )
1154    disp = np.linalg.norm(diff, axis=1)
1155    return np.sum(disp)
1156
1157def net_distance( pts_pos ):
1158    """ Net distance travelled by point with coordinates xpos and ypos """
1159    disp = pts_pos[len(pts_pos)-1] - pts_pos[0]
1160    return np.sum( np.sqrt( np.square(disp[0]) + np.square(disp[1]) ) )
1161
1162
1163###### Time measures
1164def start_time():
1165    return time.time()
1166
1167def show_duration(start_time, header=None):
1168    if header is None:
1169        header = "Processed in "
1170    #show_info(header+"{:.3f}".format((time.time()-start_time)/60)+" min")
1171    print(header+"{:.3f}".format((time.time()-start_time)/60)+" min")
1172
1173###### Preferences/shortcuts 
1174
1175def shortcut_click_match( shortcut, event ):
1176    """ Test if the click event corresponds to the shortcut """
1177    button = 1
1178    if shortcut["button"] == "Right":
1179        button = 2
1180    if event.button != button:
1181        return False
1182    if "modifiers" in shortcut.keys():
1183        return set(list(event.modifiers)) == set(shortcut["modifiers"])
1184    else:
1185        if len(event.modifiers) > 0:
1186            return False
1187        return True
1188
1189def is_windows():
1190    """ Is running on windows or not """
1191    try:
1192        return platform.lower().startswith("win")
1193    except:
1194        return False
1195
1196def is_darwin():
1197    """ Test if OS is MacOS or not """
1198    try:
1199        return platform.lower() == "darwin"
1200    except:
1201        return False
1202        
1203def print_shortcuts( shortcut_group ):
1204    """ Put to text the subset of shortcuts """
1205    text = ""
1206    for short_name, vals in shortcut_group.items():
1207        if vals["type"] == "key":
1208            text += "  <"+vals["key"]+"> "+vals["text"]+"\n"
1209        if vals["type"] == "click":
1210            modif = ""
1211            if "modifiers" in vals.keys():
1212                modifiers = vals["modifiers"]
1213                for mod in modifiers:
1214                    if mod == "Control":
1215                        if is_darwin():
1216                            modif += "Command"+"-"
1217                        else:
1218                            modif += mod+"-"
1219                    else:
1220                        if mod == "Alt":
1221                            if is_darwin():
1222                                modif += "Option"+"-"
1223                            else:
1224                                modif += mod+"-"
1225                        else:
1226                            modif += mod+"-"
1227            text += "  <"+modif+vals["button"]+"-click> "+vals["text"]+"\n"
1228    return text
def show_info(message):
52def show_info(message):
53    """ Display info in napari """
54    nt.show_info(message)

Display info in napari

def show_warning(message):
56def show_warning(message):
57    """ Display a warning in napari (napari function show_warning doesn't work) """
58    mynot = nt.Notification(message, nt.NotificationSeverity.WARNING)
59    nt.notification_manager.dispatch(mynot)

Display a warning in napari (napari function show_warning doesn't work)

def show_error(message):
61def show_error(message):
62    """ Display an error in napari (napari function show_error doesn't work) """
63    mynot = nt.Notification(message, nt.NotificationSeverity.ERROR)
64    nt.notification_manager.dispatch(mynot)

Display an error in napari (napari function show_error doesn't work)

def show_debug(message):
66def show_debug(message):
67    """ Display an info for debug in napari (napari function show_debug doesn't work) """
68    print(message)

Display an info for debug in napari (napari function show_debug doesn't work)

def show_documentation():
70def show_documentation():
71    """ Open browser on main EpiCure documentation page """
72    import webbrowser
73    webbrowser.open_new_tab("https://image-analysis-hub.github.io/Epicure/")
74    return

Open browser on main EpiCure documentation page

def show_documentation_page(page):
76def show_documentation_page(page):
77    """ 
78        Open browser on the selected page of EpiCure documentation 
79        :param: page: name of the documentation page to go to (only the name of the page, without the full path)    
80    """
81    import webbrowser
82    webbrowser.open_new_tab("https://image-analysis-hub.github.io/Epicure/"+page)
83    return

Open browser on the selected page of EpiCure documentation

Parameters
  • page: name of the documentation page to go to (only the name of the page, without the full path)
def show_progress(viewer, show):
85def show_progress( viewer, show ):
86    """ Show.hide the napari activity bar to see processing progress """
87    viewer.window._status_bar._toggle_activity_dock( show )

Show.hide the napari activity bar to see processing progress

def start_progress(viewer, total, descr=None):
89def start_progress( viewer, total, descr=None ):
90    """ Start the progress bar """
91    show_progress( viewer, True)
92    progress_bar = progress( total )
93    if descr is not None:
94        progress_bar.set_description( descr )
95    return progress_bar

Start the progress bar

def close_progress(viewer, progress_bar):
 97def close_progress( viewer, progress_bar ):
 98    """ Close the progress bar """
 99    progress_bar.close()
100    show_progress( viewer, False)

Close the progress bar

def version_above(module, version):
102def version_above( module, version ):
103    """ Compare if python module is above a given version """
104    return Version(module.__version__) > Version(version)

Compare if python module is above a given version

def version_napari_above(compare_version):
107def version_napari_above( compare_version ):
108    """ Compare if the current version of napari is above given version """
109    return Version(napari.__version__) > Version(compare_version)

Compare if the current version of napari is above given version

def version_python_minor(version):
111def version_python_minor(version):
112    """ Return if python version (minor, so 3.XX) is above given version """
113    if int(sys.version_info[0]) != 3:
114        show_warning("Python major version is not 3, not handled")
115        return False
116    return int(sys.version_info[1]) >= version

Return if python version (minor, so 3.XX) is above given version

def get_directory(imagepath):
118def get_directory(imagepath):
119    return os.path.dirname(imagepath)
def extract_names(imagepath, subname='epics', mkdir=True):
121def extract_names(imagepath, subname="epics", mkdir=True):
122    """
123        From the image file path, extracts the name of the directoties to work in
124
125        :param: imagepath: file path to the main raw movie
126        :param: subname (default: "epics"): name of the results directory where all will be saved
127        
128        :return: 
129            - name of the raw movie without the extension, that will be used to save all other files
130            - path to the directory where the raw movie is
131            - path to the results directory on which to save all outputs
132    """
133    imgname = os.path.splitext(os.path.basename(imagepath))[0]
134    imgdir = os.path.dirname(imagepath)
135    resdir = os.path.join(imgdir, subname)
136    if (not os.path.exists(resdir)) and mkdir:
137        os.makedirs(resdir)
138    return imgname, imgdir, resdir

From the image file path, extracts the name of the directoties to work in

Parameters
  • imagepath: file path to the main raw movie
  • subname (default: "epics"): name of the results directory where all will be saved
Returns
- name of the raw movie without the extension, that will be used to save all other files
- path to the directory where the raw movie is
- path to the results directory on which to save all outputs
def extract_names_segmentation(segpath):
140def extract_names_segmentation(segpath):
141    """ Get the output directory and imagename from the segmentation filename """
142    imgname = os.path.splitext(os.path.basename(segpath))[0]
143    if imgname.endswith("_labels"):
144        imgname = imgname[:(len(imgname)-7)]
145    imgdir = os.path.dirname(segpath)
146    return imgname, imgdir

Get the output directory and imagename from the segmentation filename

def suggest_segfile(out, imgname):
148def suggest_segfile(out, imgname):
149    """ Check if a segmentation file from EpiCure already exists """
150    segfile = os.path.join(out, imgname+"_labels.tif")
151    if os.path.exists(segfile):
152        return segfile
153    else:
154        return None

Check if a segmentation file from EpiCure already exists

def found_segfile(filepath):
156def found_segfile( filepath ):
157    """ Check if the segmentation file exists """
158    return os.path.exists( filepath )

Check if the segmentation file exists

def get_filename(outdir, imgname):
160def get_filename(outdir, imgname):
161    """ Join the directory with the filename """
162    return os.path.join( outdir, imgname )

Join the directory with the filename

def napari_info(text):
164def napari_info(text):
165    """ Use napari information window to show a message """
166    show_info(text)

Use napari information window to show a message

def create_text_window(name):
168def create_text_window( name ):
169    """ Create and display help text window """
170    blabla = TextEdit()
171    blabla.name = name 
172    blabla.show()
173    return blabla

Create and display help text window

def napari_shortcuts():
176def napari_shortcuts():
177    """ Write main napari shortcuts list """
178    text = "---- Main napari default shortcuts ----\n"
179    text += " -- view options \n"
180    if is_darwin():
181        text += "  <Command+R> reset view \n"
182        text += "  <Command+Y> switch 2D/3D view mode \n"
183        text += "  <Command+G> switch Grid/Overlay view mode \n"
184    else:
185        text += "  <Ctrl+R> reset view \n"
186        text += "  <Ctrl+Y> switch 2D/3D view mode \n"
187        text += "  <Ctrl+G> switch Grid/Overlay view mode \n"
188    text += "  <left arrow> got to previous frame \n"
189    text += "  <right arrow> got to next frame \n"
190    text += "\n"
191    text += " -- labels options \n"
192    text += "  <2> paint brush mode \n"
193    text += "  <3> fill mode \n"
194    text += "  <4> pick mode (select label) \n"
195    text += "  <[> or <]> increase/decrease the paint brush size \n"
196    text += "  <p> activate/deactivate preserve labels option \n"
197    return text

Write main napari shortcuts list

def removeOverlayText(viewer):
199def removeOverlayText(viewer):
200    """ Remove all texts that was overlaid on the main window """
201    viewer.text_overlay.text = trans._("")
202    viewer.text_overlay.visible = False

Remove all texts that was overlaid on the main window

def getOverlayText(viewer):
204def getOverlayText(viewer):
205    """ Returns the current overlay text """
206    return viewer.text_overlay.text

Returns the current overlay text

def setOverlayText(viewer, text, size=10):
208def setOverlayText(viewer, text, size=10 ):
209    """ 
210    Set the overlay text
211    :param: viewer: current napari view
212    :param: text: new text to display as overlay
213    :param: size: size of the displayed text
214    """
215    viewer.text_overlay.text = trans._(text)
216    viewer.text_overlay.position = "top_left"
217    viewer.text_overlay.visible = True
218    if version_napari_above( "0.6.5" ):
219        size = size - 2
220    viewer.text_overlay.font_size = size
221    viewer.text_overlay.color = "white"
222    viewer.text_overlay.opacity = 1
223    viewer.text_overlay.blending = "additive"

Set the overlay text

Parameters
  • viewer: current napari view
  • text: new text to display as overlay
  • size: size of the displayed text
def showOverlayText(viewer, vis=None):
225def showOverlayText(viewer, vis=None):
226    """
227    Show the overlay text on/off
228    :param: viewer: current napari viewer
229    :param: vis: show it alternatively on/off if vis is None. Or can be a boolean to force the showing or not
230    """
231    if vis is None:
232        viewer.text_overlay.visible = not viewer.text_overlay.visible
233    else:
234        viewer.text_overlay.visible = vis 

Show the overlay text on/off

Parameters
  • viewer: current napari viewer
  • vis: show it alternatively on/off if vis is None. Or can be a boolean to force the showing or not
def reactive_bindings(layer, mouse_drag, key_map):
236def reactive_bindings(layer, mouse_drag, key_map):
237    """ Reactive the mouse and key event bindings on layer """
238    layer.mouse_drag_callbacks = mouse_drag
239    layer.keymap.update(key_map)

Reactive the mouse and key event bindings on layer

def clear_bindings(layer):
241def clear_bindings(layer):
242    """ Clear and returns the current event bindings on layer """
243    old_mouse_drag = layer.mouse_drag_callbacks.copy()
244    old_key_map = layer.keymap.copy()
245    layer.mouse_drag_callbacks = []
246    layer.keymap.clear()
247    return old_mouse_drag, old_key_map

Clear and returns the current event bindings on layer

def is_binary(img):
249def is_binary( img ):
250    """ Test if more than 2 values (skeleton or labelled image) """
251    return all(len(np.unique(frame)) <= 2 for frame in img)

Test if more than 2 values (skeleton or labelled image)

def set_frame(viewer, frame, scale=1):
253def set_frame(viewer, frame, scale=1):
254    """ Set current frame """
255    viewer.dims.set_point(0, frame*scale)

Set current frame

def reset_view(viewer, zoom, center):
257def reset_view( viewer, zoom, center ):
258    """ Reset the view to given camera center and zoom """
259    viewer.camera.center = center
260    viewer.camera.zoom = zoom

Reset the view to given camera center and zoom

def set_active_layer(viewer, layname):
262def set_active_layer(viewer, layname):
263    """ Set the current Napari active layer """
264    if layname in viewer.layers:
265        viewer.layers.selection.active = viewer.layers[layname]

Set the current Napari active layer

def set_visibility(viewer, layname, vis):
267def set_visibility(viewer, layname, vis):
268    """ Set visibility of layer layname if exists """
269    if layname in viewer.layers:
270        viewer.layers[layname].visible = vis

Set visibility of layer layname if exists

def remove_layer(viewer, layname):
272def remove_layer(viewer, layname):
273    """ Remove a layer with specific name from the viewer """
274    if layname in viewer.layers:
275        try:
276            viewer.layers.remove(layname)
277        except Exception as e:
278            print("Remove of layer incomplete")
279            print(e)

Remove a layer with specific name from the viewer

def remove_widget(viewer, widname):
281def remove_widget(viewer, widname):
282    """ Remove a widget from the viewer """
283    if version_napari_above( "0.6.6" ):
284        if widname in viewer.window.dock_widgets:
285            import magicgui
286            wid = viewer.window.dock_widgets[widname]
287            if type(wid) is magicgui.widgets._function_gui.FunctionGui:
288                viewer.window.remove_dock_widget( wid.native.parent() )
289            else:
290                viewer.window.remove_dock_widget[ wid.parent() ]
291            del wid
292    else:
293        if widname in viewer.window._dock_widgets:
294            wid = viewer.window._dock_widgets[widname]
295            wid.setDisabled(True)
296            try:
297                wid.disconnect()
298            except Exception:
299                pass
300            del viewer.window._dock_widgets[widname]
301            wid.destroyOnClose()

Remove a widget from the viewer

def remove_all_widgets(viewer):
303def remove_all_widgets( viewer ):
304    """ Remove all widgets """
305    viewer.window.remove_dock_widget("all")

Remove all widgets

def get_metadata_field(metadata, fieldname):
307def get_metadata_field(metadata, fieldname):
308    """ Read an imagej metadata string and get the value of fieldname """
309    if metadata.index(fieldname+"=") < 0:
310        return None
311    submeta = metadata[metadata.index(fieldname+"=")+len(fieldname)+1:]
312    value = submeta[0:submeta.index("\n")]
313    return value

Read an imagej metadata string and get the value of fieldname

def get_metadata_json(metadata, fieldname):
315def get_metadata_json(metadata, fieldname):
316    """ Read a metadata from json of bioio-bioformats to get value of fieldname """
317    if metadata.index("\""+fieldname+"\"=") < 0:
318        return None
319    submeta = metadata[metadata.index("\""+fieldname+"\"=")+len(fieldname)+3:]
320    value = submeta[0:submeta.index(",")]
321    return value

Read a metadata from json of bioio-bioformats to get value of fieldname

def open_image(imagepath, get_metadata=False, verbose=True):
324def open_image(imagepath, get_metadata=False, verbose=True):
325    """ Open an image with bioio library """
326    imagename, extension = os.path.splitext(imagepath)
327    format = "all"
328    if (extension==".tif") or (extension==".tiff"):
329        if verbose:
330            print("Opening Tif image "+str(imagepath)+" with bioio-tifffile")
331        import bioio_tifffile
332        if version_python_minor(10):
333            from bioio import BioImage
334            img = BioImage(imagepath, reader=bioio_tifffile.Reader)
335        else:
336            ## python 3.9 or under
337            reader = bioio_tifffile.Reader
338            img = reader(imagepath)
339        format = "tif"
340    else:
341        import bioio_bioformats
342        if verbose:
343            print("Opening "+extension+" image "+str(imagepath)+" with bioio-bioformats")
344        if version_python_minor(10):
345            from bioio import BioImage
346            img = BioImage(imagepath, reader=bioio_bioformats.Reader)
347        else:
348            ## python 3.9 or under
349            reader = bioio_bioformats.Reader
350            img = reader(imagepath)
351    image = img.data
352    if verbose:
353        print(f"Loaded image shape: {image.shape}")
354    if (len(image.shape) == 5):
355        ## correct format of the image and metadata with TCZYX
356        if (img.dims is not None) and len(img.dims.shape)==5 :
357            if (img.dims.Z>1) and (img.dims.T == 1):
358                print("Warning, movie had Z slices instead of T frames. EpiCure handles it but it might not be in other softwares/plugins")
359                image = np.swapaxes(image, 0, 2)
360    image = np.squeeze(image)
361        
362    if not get_metadata:
363        return image, 0, 1, None, 1, None
364
365    try: 
366        nchan = img.dims.C
367        if nchan == 1:
368            nchan = 0 ### was squeezed above
369    except:
370        nchan = 0
371        pass
372    
373    ## spatial metadata
374    scale_xy, unit_xy, scale_t, unit_t = None, None, None, None
375    try:
376        scale_xy = img.scale.X # img.physical_pixel_sizes
377        unit_xy = img.dimension_properties.X.unit
378    except:
379        pass
380
381    try: 
382        if unit_xy is None:
383            if format == "all":
384                unit_xy = get_metadata_json(img.metadata.json(), "physical_size_x_unit")
385            elif format == "tif":
386                unit_xy = get_metadata_field(img.metadata, "physical_size_x_unit")
387    except:
388        print("Reading spatial metadata might have failed. Check it manually")
389        if scale_xy is None:
390            scale_xy = 1
391
392    ## temporal metadata 
393    try:
394        scale_t = img.scale.T
395        unit_t = img.dimension_properties.T.unit
396    except:
397        pass
398
399    try: 
400        if scale_t is None:
401                # read it from the metadata field (string) 
402            if format == "all":
403                scale_t = get_metadata_json(img.metadata.json(), "time_increment_unit")
404                scale_t = float(scale_t)
405                unit_t = get_metadata_json(img.metadata.json(), "time_increment")
406            elif format == "tif":
407                scale_t = get_metadata_field(img.metadata, "finterval")
408                scale_t = float(scale_t)
409                unit_t = get_metadata_field(img.metadata, "tunit")
410    except:
411        print("Reading temporal metadata might have failed. Check it manually")
412        if scale_t is None:
413            scale_t = 1
414    if unit_xy is None:
415        unit_xy = "um"
416    if unit_t is None:
417        unit_t = "min"
418    return image, nchan, scale_xy, unit_xy, scale_t, unit_t

Open an image with bioio library

def writeTif(img, imgname, scale, imtype, what=''):
420def writeTif(img, imgname, scale, imtype, what=""):
421    """ Write image in tif format """
422    #TODO: change to make it with bioio
423    if len(img.shape) == 2:
424        tif.imwrite(imgname, np.array(img, dtype=imtype), imagej=True, resolution=[1./scale, 1./scale], metadata={'unit': 'um', 'axes': 'YX'})
425    else:
426        try:
427            tif.imwrite(imgname, np.array(img, dtype=imtype), imagej=True, resolution=[1./scale, 1./scale], metadata={'unit': 'um', 'axes': 'TYX'}, compression="zstd")
428        except:
429            tif.imwrite(imgname, np.array(img, dtype=imtype), imagej=True, resolution=[1./scale, 1./scale], metadata={'unit': 'um', 'axes': 'TYX'})
430    show_info(what+" saved in "+imgname)

Write image in tif format

def appendToTif(img, imgname):
432def appendToTif(img, imgname):
433    """ Append to RGB tif the current image """
434    tif.imwrite(imgname, img, photometric="rgb", append=True)

Append to RGB tif the current image

def getCellValue(label_layer, event):
436def getCellValue(label_layer, event):
437    """ Get the label under the click """
438    vis = label_layer.visible
439    if vis == False:
440        label_layer.visible = True
441    label = label_layer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
442    if vis == False:
443        ## put it back to not visible state
444        label_layer.visible = vis
445    return label

Get the label under the click

def setCellValue( layer, label_layer, event, newvalue, layer_frame=None, label_frame=None):
447def setCellValue(layer, label_layer, event, newvalue, layer_frame=None, label_frame=None):
448    """ Get the cell concerned by the event and replace its value by new one"""
449    # get concerned label (under the cursor), layer has to be visible for this
450    vis = label_layer.visible
451    if vis == False:
452        label_layer.visible = True
453    label = label_layer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
454    label_layer.visible = vis
455    if label is not None and label > 0:
456        # if the seg image is 2D (single frame), label_frame will be None
457        if label_frame is not None and label_frame >= 0:
458            ldata = label_layer.data[label_frame,:,:]
459        else:
460            ldata = label_layer.data
461        # if the layer is 2D (single frame), layer_frame will be None
462        if layer_frame is not None and layer_frame >= 0:
463            #slice_coord = tuple(sc[keep_coords] for sc in slice_coord)
464            cdata = layer.data[layer_frame,:,:]
465        else:
466            cdata = layer.data
467            #slice_coord = tuple(sc[keep_coords] for sc in slice_coord)
468
469        cdata[np.where(ldata==label)] = newvalue
470        layer.refresh()
471        return label

Get the cell concerned by the event and replace its value by new one

def thin_seg_one_frame(segframe):
473def thin_seg_one_frame( segframe ):
474    """ Boundaries of the frame one pixel thick """
475    bin_img = binary_closing( find_boundaries(segframe, connectivity=2, mode="outer"), footprint=np.ones((3,3)) )
476    skel = skeletonize( bin_img )
477    skel = copy_border( skel, bin_img )
478    return skeleton_to_label( skel, segframe )

Boundaries of the frame one pixel thick

def copy_border(skel, bin):
480def copy_border( skel, bin ):
481    """ Copy the pixel border onto skeleton image """
482    skel[[0, -1], :] = bin[[0, -1], :]  # top and bottom borders
483    skel[:, [0, -1]] = bin[:, [0, -1]]  # left and right borders
484    return skel

Copy the pixel border onto skeleton image

def draw_points(pts, imshape, radius):
486def draw_points(pts, imshape, radius):
487    """ Draw circle (2D) around the given points in 2D image """  
488    image = np.zeros(imshape, dtype=bool)
489    y, x = np.ogrid[:imshape[0], :imshape[1]]
490    for pt in pts:
491        # Calculate distance from pt, scaled to compare to radius
492        distances_sq = ((y - pt[0]))**2 + ((x - pt[1]))**2 
493        image |= distances_sq <= radius**2
494    return image

Draw circle (2D) around the given points in 2D image

def get_vertices(seg, viewer=None, verbose=0, parallel=0):
496def get_vertices(seg, viewer=None, verbose=0, parallel=0):
497    """ Get the vertices of the segmentation """
498    skeleton = get_skeleton(seg, viewer, verbose, parallel)
499    convfilter = np.array([[-1,-1,-1], [-1,3,-1],[-1,-1,-1]])
500    novert = np.zeros(skeleton.shape, dtype=np.int8)
501    ## pure skeleton
502    for ind, skel in enumerate(skeleton):
503        skeleton[ind] = medial_axis(skel)
504        novert[ind] = signal.convolve2d(skeleton[ind], convfilter, mode="same")
505    novert[novert<=0] = 0
506    nodeimg = skeleton - novert
507    nodeimg[nodeimg<=0] = 0
508    nodeimg[nodeimg>0] = 1 
509    return nodeimg 

Get the vertices of the segmentation

def get_skeleton(seg, viewer=None, verbose=0, parallel=0):
512def get_skeleton( seg, viewer=None, verbose=0, parallel=0 ) :
513    """ convert labels movie to skeleton (thin boundaries) """
514    startt = start_time()
515    if viewer is not None:
516        show_progress( viewer, show=True )
517
518    def frame_skeleton( frame ):
519        """ Calculate skeleton on one frame """
520        expz = expand_labels( frame, distance=1 )
521        frame_skel = np.zeros( frame.shape, dtype="uint8" )
522        frame_skel[ (frame==0) * (expz>0) ] = 1
523        return frame_skel
524        
525    if parallel > 0:
526        skel = Parallel( n_jobs=parallel )(
527            delayed(frame_skeleton)(frame) for frame in seg
528        )
529        skel = np.array(skel)
530    else:
531        skel = np.zeros(seg.shape, dtype="uint8")
532        for z in progress(range(seg.shape[0])):
533            expz = expand_labels( seg[z], distance=1 )
534            skel[z][(seg[z] == 0) *(expz > 0)] = 1
535    if verbose > 0:
536        show_duration(startt, header="Skeleton calculted in ")
537    if viewer is not None:
538        show_progress( viewer, show=False )
539    return skel

convert labels movie to skeleton (thin boundaries)

def setLabelValue( layer, label_layer, event, newvalue, layer_frame=None, label_frame=None):
542def setLabelValue(layer, label_layer, event, newvalue, layer_frame=None, label_frame=None):
543    """ Change the label value under event position and returns its old value """
544    ## get concerned label (under the cursor), layer has to be visible for this
545    vis = label_layer.visible
546    if vis == False:
547        label_layer.visible = True
548    label = label_layer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
549    label_layer.visible = vis
550    
551    if label > 0:
552        inds = getLabelIndexes( label_layer.data, label, label_frame )
553        setNewLabel(layer, inds, newvalue, add_frame=layer_frame)
554        layer.refresh()
555        return label
556    return None

Change the label value under event position and returns its old value

def getLabelIndexes(label_data, label, frame):
558def getLabelIndexes(label_data, label, frame):
559    """ Get the indixes at which label_layer is label for given frame """
560    # if the seg image is 2D (single frame), frame will be None
561    if frame is not None and frame >= 0:
562        ldata = label_data[frame,:,:]
563    else:
564        ldata = label_data
565    return np.argwhere( ldata==label ).tolist()

Get the indixes at which label_layer is label for given frame

def getLabelIndexesInFrame(frame_data, label):
567def getLabelIndexesInFrame(frame_data, label):
568    """ Get the indexes at which frame data is label """
569    # if the seg image is 2D (single frame), frame will be None
570    return np.argwhere( frame_data==label ).tolist()

Get the indexes at which frame data is label

def changeLabel(label_layer, old_value, new_value):
572def changeLabel( label_layer, old_value, new_value ):
573    """ replace the value of label old-value by new_value """
574    index = np.argwhere( label_layer.data==old_value ).tolist()
575    setNewLabel( label_layer, index, new_value )

replace the value of label old-value by new_value

def setNewLabel(label_layer, indices, newvalue, add_frame=None, return_old=True):
577def setNewLabel(label_layer, indices, newvalue, add_frame=None, return_old=True):
578    """ Change the label of all the pixels indicated by indices """
579    indexs = np.array(indices).T
580    if add_frame is not None:
581        indexs = np.vstack((np.repeat(add_frame, indexs.shape[1]), indexs))
582    changed_indices = label_layer.data[tuple(indexs)] != newvalue
583    inds = tuple(x[changed_indices] for x in indexs)
584    oldvalues = None
585    if return_old:
586        oldvalues = label_layer.data[inds]
587    if isinstance(newvalue, list):
588        newvalue = np.array(newvalue)[np.where(changed_indices)[0]]
589    label_layer.data_setitem( inds, newvalue )
590    return inds, newvalue, oldvalues 

Change the label of all the pixels indicated by indices

def convert_coords(coord):
592def convert_coords( coord ):
593    """ Get the time frame, and the 2D coordinates as int """
594    int_coord = tuple(np.round(coord).astype(int))
595    tframe = int(coord[0])
596    int_coord = int_coord[1:3]
597    return tframe, int_coord

Get the time frame, and the 2D coordinates as int

def outerBBox2D(bbox, imshape, margin=0):
599def outerBBox2D(bbox, imshape, margin=0):
600    if (bbox[0]-margin) <= 0:
601        return True
602    if (bbox[2]+margin) >= imshape[0]:
603        return True
604    if (bbox[1]-margin) <= 0:
605        return True
606    if (bbox[3]+margin) >= imshape[1]:
607        return True
608    return False
def isInsideBBox(bbox, obbox):
610def isInsideBBox( bbox, obbox ):
611    """ Check if bbox is included in obbox """
612    if (bbox[0] >= obbox[0]) and (bbox[1] >= obbox[1]):
613        return (bbox[2] <= obbox[2]) and (bbox[3] <= obbox[3])
614    return False

Check if bbox is included in obbox

def setBBox(position, extend, imshape):
616def setBBox(position, extend, imshape):
617    bbox = [
618        max(int(position[0] - extend), 0),
619        max(int(position[1] - extend), 0),
620        max(int(position[2] - extend), 0),
621        min(int(position[0] + extend), imshape[0]),
622        min(int(position[1] + extend), imshape[1]),
623        min(int(position[2] + extend), imshape[2])
624    ]
625    return bbox
def setBBoxXY(position, extend, imshape):
627def setBBoxXY(position, extend, imshape):
628    bbox = [
629        max(int(position[0]), 0),
630        max(int(position[1] - extend), 0),
631        max(int(position[2] - extend), 0),
632        min(int(position[0] + 1), imshape[0]),
633        min(int(position[1] + extend), imshape[1]),
634        min(int(position[2] + extend), imshape[2])
635    ]
636    return bbox
def getBBox2DFromPts(pts, extend, imshape):
638def getBBox2DFromPts(pts, extend, imshape):
639    """ Get the bounding box surrounding all the points, plus a margin """
640    arr = np.array(pts)
641    ptsdim = arr.shape[1]
642    if ptsdim == 2:
643        bbox = [
644            max( int(np.min(arr[:,0])) - extend, 0), 
645            max( int(np.min(arr[:,1])) - extend, 0), 
646            min( int(np.max(arr[:,0]))+1+extend, imshape[0]), 
647            min( int(np.max(arr[:,1]))+1+extend, imshape[1] )
648            ]
649    if ptsdim == 3:
650        bbox = [
651            max( int(np.min(arr[:,1])) -extend, 0), 
652            max( int(np.min(arr[:,2])) - extend, 0),
653            min( int(np.max(arr[:,1]))+1 + extend, imshape[0]), 
654            min( int(np.max(arr[:,2]))+1 + extend, imshape[1] )
655            ]
656
657    return bbox

Get the bounding box surrounding all the points, plus a margin

def getBBoxFromPts(pts, extend, imshape, outdim=None, frame=None):
659def getBBoxFromPts(pts, extend, imshape, outdim=None, frame=None):
660    arr = np.array(pts)
661    ## get if points are 2D or 3D
662    ptsdim = arr.shape[1]
663    ## if not imposed, output the same dimension as input points
664    if outdim is None:
665        outdim = ptsdim
666    ## Get bounding box from points according to dimensions
667    if ptsdim == 2:
668        if outdim == 2:
669            bbox = [int(np.min(arr[:,0])), int(np.min(arr[:,1])), int(np.max(arr[:,0]))+1, int(np.max(arr[:,1]))+1]
670        else:
671            bbox = [frame, int(np.min(arr[:,0])), int(np.min(arr[:,1])), frame+1, int(np.max(arr[:,0]))+1, int(np.max(arr[:,1]))+1]
672    if ptsdim == 3:
673        if outdim == 2:
674            bbox = [int(np.min(arr[:,1])), int(np.min(arr[:,2])), int(np.max(arr[:,1]))+1, int(np.max(arr[:,2]))+1]
675        else:
676            bbox = [int(np.min(arr[:,0])), int(np.min(arr[:,1])), int(np.min(arr[:,2])), int(np.max(arr[:,0]))+1, int(np.max(arr[:,1]))+1, int(np.max(arr[:,2]))+1]
677    if extend > 0:
678        for i in range(outdim):
679            if i < 2:
680                bbox[(outdim==3)+i] = max( bbox[(outdim==3)+i] - extend, 0)
681                bbox[(outdim==3)+i+outdim] = min(bbox[(outdim==3)+i+outdim] + extend, imshape[(outdim==3)+i] )
682    return bbox
def inside_bounds(pt, imshape):
684def inside_bounds( pt, imshape ):
685    """ Check if given point is inside image limits """
686    return all(0 <= pt[i] < imshape[i] for i in range(len(pt)))

Check if given point is inside image limits

def extendBBox2D(bbox, extend_factor, imshape):
688def extendBBox2D( bbox, extend_factor, imshape ):
689    """ Extend bounding box with given margin """
690    extend = max(bbox[2] - bbox[0], bbox[3] - bbox[1]) * extend_factor
691    bbox = np.array(bbox)
692    bbox[:2] = np.maximum(bbox[:2] - extend, 0)
693    bbox[2:] = np.minimum(bbox[2:] + extend, imshape[:2])
694    return bbox

Extend bounding box with given margin

def getBBox2D(img, label):
696def getBBox2D(img, label):
697    """ Get bounding box of label """
698    mask = (img==label)*1
699    props = regionprops(mask)
700    for prop in props:
701        bbox = prop.bbox
702        return bbox

Get bounding box of label

def getPropLabel(img, label):
704def getPropLabel(img, label):
705    """ Get the properties of label """
706    mask = np.uint8(img == label)
707    props = regionprops(mask)
708    return props[0]

Get the properties of label

def getBBoxLabel(img, label):
710def getBBoxLabel(img, label):
711    """ Get bounding box of label """
712    mask_ind = np.where(img==label)
713    if len(mask_ind) <= 0:
714        return None
715    dim = len(img.shape)
716    bbox = np.zeros(dim*2, int)
717    for i in range(dim):
718        bbox[i] = int(np.min(mask_ind[i]))
719        bbox[i+dim] = int(np.max(mask_ind[i]))+1
720    return bbox

Get bounding box of label

def getBBox2DMerge(img, label, second_label):
722def getBBox2DMerge(img, label, second_label): #, checkTouching=False):
723    """ Get bounding box of two labels and check if they are in contact """
724    mask = np.isin( img, [label, second_label] )
725    props = regionprops(mask*1)
726    return props[0].bbox, mask 

Get bounding box of two labels and check if they are in contact

def frame_to_skeleton(frame, connectivity=1):
729def frame_to_skeleton(frame, connectivity=1):
730    """ convert labels frame to skeleton (thin boundaries) """
731    return skeletonize( find_boundaries(frame, connectivity=connectivity, mode="outer") )

convert labels frame to skeleton (thin boundaries)

def remove_boundaries(img):
733def remove_boundaries(img):
734    """ Put the boundaries pixels between labels as 0 """
735    bound = frame_to_skeleton( img, connectivity=1 )
736    img[bound>0] = 0
737    return img

Put the boundaries pixels between labels as 0

def ind_boundaries(img):
739def ind_boundaries(img):
740    """ Get indices of the boundaries pixels between two labels """
741    bound = frame_to_skeleton( img, connectivity=1 )
742    return np.argwhere(bound>0)

Get indices of the boundaries pixels between two labels

def checkTouchingLabels(img, label, second_label):
744def checkTouchingLabels(img, label, second_label):
745    """ Returns if labels are in contact (1-2 pixel away) """
746    disk_one = disk(radius=1)
747    maska = binary_dilation(img==label, footprint=disk_one)
748    maskb = binary_dilation(img==second_label, footprint=disk_one)
749    return np.any(maska & maskb)

Returns if labels are in contact (1-2 pixel away)

def positionsIn2DBBox(positions, bbox):
751def positionsIn2DBBox( positions, bbox ):
752    """ Shift all the positions to their position inside the 2D bounding box """
753    return [positionIn2DBBox( pos, bbox ) for pos in positions ]

Shift all the positions to their position inside the 2D bounding box

def positions2DIn2DBBox(positions, bbox):
755def positions2DIn2DBBox( positions, bbox ):
756    """ Shift all the positions to their position inside the 2D bounding box """
757    return [position2DIn2DBBox( pos, bbox ) for pos in positions ]

Shift all the positions to their position inside the 2D bounding box

def positionIn2DBBox(position, bbox):
759def positionIn2DBBox(position, bbox):
760    """ Returns the position shifted to its position inside the 2D bounding box """
761    return (int(position[1]-bbox[0]), int(position[2]-bbox[1]))

Returns the position shifted to its position inside the 2D bounding box

def position2DIn2DBBox(position, bbox):
763def position2DIn2DBBox(position, bbox):
764    """ Returns the position shifted to its position inside the 2D bounding box """
765    return (int(position[0]-bbox[0]), int(position[1]-bbox[1]))

Returns the position shifted to its position inside the 2D bounding box

def toFullImagePos(indices, bbox):
767def toFullImagePos(indices, bbox):
768    indices = np.array(indices)
769    return np.column_stack((indices[:, 0] + bbox[0], indices[:, 1] + bbox[1])).tolist()
def addFrameIndices(indices, frame):
771def addFrameIndices( indices, frame ):
772    return [ [frame, ind[0], ind[1]] for ind in indices ]
def shiftFrameIndices(indices, add_frame):
774def shiftFrameIndices( indices, add_frame ):
775    if isinstance( indices, list ):
776        indices = np.array(indices)
777    indices[:, 0] += add_frame
778    return indices.tolist()
def shiftFrames(indices, frames):
780def shiftFrames( indices, frames ):
781    if isinstance( indices, list ):
782        indices = np.array(indices)
783    indices[:, 0] = frames[indices[:, 0]]
784    return indices.tolist()
def toFullMoviePos(indices, bbox, frame=None):
786def toFullMoviePos( indices, bbox, frame=None ):
787    """ Replace indexes inside bounding box to full movie indexes """
788    indices = np.array(indices)
789    if frame is not None:
790        frame_arr = np.full(len(indices), frame)
791        return np.column_stack((frame_arr, indices[:, 0] + bbox[0], indices[:, 1] + bbox[1]))
792    if len(bbox) == 6:
793        return np.column_stack((indices[:, 0] + bbox[0], indices[:, 1] + bbox[1], indices[:, 2] + bbox[2]))
794    return np.column_stack((indices[:, 0], indices[:, 1] + bbox[0], indices[:, 2] + bbox[1]))

Replace indexes inside bounding box to full movie indexes

def cropBBox(img, bbox):
796def cropBBox(img, bbox):
797    slices = tuple(slice(bbox[i], bbox[i + len(bbox) // 2]) for i in range(len(bbox) // 2))
798    return img[slices]
def crop_twoframes(img, bbox, frame):
800def crop_twoframes( img, bbox, frame ):
801    """ Crop bounding box with two frames """
802    return np.copy(img[(frame-1):(frame+1), bbox[0]:bbox[2], bbox[1]:bbox[3]])

Crop bounding box with two frames

def cropBBox2D(img, bbox):
804def cropBBox2D(img, bbox):
805    return img[bbox[0]:bbox[2], bbox[1]:bbox[3]]
def setValueInBBox2D(img, setimg, bbox):
807def setValueInBBox2D(img, setimg, bbox):
808    bbimg = img[bbox[0]:bbox[2], bbox[1]:bbox[3]] 
809    bbimg[setimg>0]= setimg[setimg>0]
def addValueInBBox(img, addimg, bbox):
811def addValueInBBox(img, addimg, bbox):
812    img[bbox[0]:bbox[3], bbox[1]:bbox[4], bbox[2]:bbox[5]] = img[bbox[0]:bbox[3], bbox[1]:bbox[4], bbox[2]:bbox[5]] + addimg
def set_maxlabel(layer):
814def set_maxlabel(layer):
815    layer.mode = "PAINT"
816    layer.selected_label = np.max(layer.data)+1
817    layer.refresh()
def set_label(layer, lab):
819def set_label(layer, lab):
820    layer.mode = "PAINT"
821    layer.selected_label = lab
822    layer.refresh()
def get_free_labels(used, nlab):
824def get_free_labels( used, nlab ):
825    """ Get n-th unused label (not in used list) """
826    maxlab = max(used)+1
827    unused = list(set(range(1, maxlab)) - set(used))
828    if nlab < len(unused):
829        return unused[0:nlab]
830    else:
831        return unused+list(range(maxlab+1, maxlab+1+(nlab-len(unused))))

Get n-th unused label (not in used list)

def get_next_label(layer, label):
833def get_next_label(layer, label):
834    """ Get the next unused label starting from label """
835    used = np.unique(layer.data)
836    i = label+1
837    while i < np.max(used):
838        if i>0 and (i not in used):
839            return i
840        i = i + 1
841    return i+1

Get the next unused label starting from label

def relabel_layer(layer):
843def relabel_layer(layer):
844    maxlab = np.max(layer.data)
845    used = np.unique(layer.data)
846    nlabs = len(used)
847    if nlabs == maxlab:
848        #print("already relabelled")
849        return
850    for j in range(1, nlabs+1):
851        if j not in used:
852            layer.data[layer.data==maxlab] = j
853            maxlab = np.max(layer.data)
854    show_info("Labels reordered")
855    layer.refresh()
def inv_visibility(viewer, layername):
857def inv_visibility(viewer, layername):
858    """ Switch the visibility of a layer """
859    if layername in viewer.layers:
860        layer = viewer.layers[layername]
861        layer.visible = not layer.visible

Switch the visibility of a layer

def average_area(seg):
864def average_area( seg ):
865    """ Average area of labels (cells) """
866    # Label the input image
867    labeled_array, num_features = ndlabel(seg)
868    
869    if num_features == 0:
870        return 0.0
871    
872    # Calculate the area of each label
873    areas = ndsum(seg > 0, labeled_array, index=np.arange(1, num_features + 1))
874    # Calculate the average area
875    avg_area = np.mean(areas)   
876    return avg_area

Average area of labels (cells)

def summary_labels(seg):
879def summary_labels( seg ):
880    """ Summary of labels (cells) measurements """
881    props = regionprops(seg)
882    avg_duration = 0
883    avg_area = 0.0
884    for prop in props:
885        bbox = prop.bbox
886        nz = 1
887        if len(bbox)>4:
888            nz = bbox[3]-bbox[0]
889        avg_duration += nz
890        avg_area += prop.area/nz
891    return len(props), avg_duration/len(props), avg_area/len(props) 

Summary of labels (cells) measurements

def labels_in_cell(sega, segb, label):
893def labels_in_cell( sega, segb, label ):
894    """ Look at the labels of segb inside label from sega """
895    cell = np.isin( sega, [label] )
896    labelb = segb[ cell ]
897    cell_area = np.sum( cell*1, axis=None )
898    filled_area = np.sum( labelb>0 )
899    nobj = len(np.unique( labelb ))
900    if 0 in labelb:
901        nobj = nobj - 1
902    return nobj, (filled_area/cell_area), np.unique(labelb)

Look at the labels of segb inside label from sega

def match_labels(sega, segb):
905def match_labels( sega, segb ):
906    """ Match the labels of the two segmentation images """
907    region_properties = ["label", "centroid"]
908
909    df0 = pd.DataFrame( regionprops_table( sega, properties=region_properties ) )
910    df0["frame"] = 0
911    df1 = pd.DataFrame( regionprops_table( segb, properties=region_properties ) )
912    df1["frame"] = 1
913    df = pd.concat([df0, df1])
914
915    ## Link the two frames with LapTrack tracking
916    laptrack = LaptrackCentroids(None, None)
917    laptrack.max_distance = 10 
918    laptrack.set_region_properties(with_extra=False)
919    laptrack.splitting_cost = False ## disable splitting option
920    laptrack.merging_cost = False ## disable merging option
921    labels = list(np.unique(segb))
922    if 0 in labels:
923        labels.remove(0)
924    parent_labels = laptrack.twoframes_track(df, labels)
925    return parent_labels, labels

Match the labels of the two segmentation images

def labels_table(labimg, intensity_image=None, properties=None, extra_properties=None):
927def labels_table( labimg, intensity_image=None, properties=None, extra_properties=None ):
928    """ Returns the regionprops_table of the labels """
929    if properties is None:
930        properties = ['label', 'centroid']
931    if intensity_image is not None:
932        return regionprops_table( labimg, intensity_image=intensity_image, properties=properties, extra_properties=extra_properties )
933    return regionprops_table( labimg, properties=properties, extra_properties=extra_properties )

Returns the regionprops_table of the labels

def labels_to_table(labimg, frame):
935def labels_to_table( labimg, frame ):
936    """ Get label and centroid """
937    labels = np.unique(labimg.ravel())
938    labels = labels[labels != 0]
939    centroids = center_of_mass(labimg, labels=labimg, index=labels)
940    table = np.column_stack((labels, np.full(len(labels), frame), centroids))
941    return table.astype(int)

Get label and centroid

def labels_to_table_v1(labimg, frame):
943def labels_to_table_v1( labimg, frame ):
944    """ Get label and centroid """
945    props = regionprops( labimg )
946    n = len(props)
947    if n == 0:
948        return np.empty( (0, 2+labimg.ndim) )
949    res = np.zeros( (n, 2+labimg.ndim), dtype=int )
950    for i, prop in enumerate(props):
951        res[i, 0] = prop.label
952        res[i, 1] = frame
953        res[i,:2] = np.array(prop.centroid).astype(int)
954    return res

Get label and centroid

def non_unique_labels(labimg):
956def non_unique_labels( labimg ):
957    """ Check if contains only unique labels """
958    relab, nlabels = ndlabel( labimg )
959    return nlabels > (len( np.unique(labimg) )-1)

Check if contains only unique labels

def reset_labels(labimg, closing=True):
961def reset_labels( labimg, closing=True ):
962    """ Relabel in 3D all labels (unique labels) """
963    s = ndi_structure(3,1)
964    ## ignore 3D connectivity (unique labels in all frames)
965    s[0,:,:] = 0
966    s[2,:,:] = 0
967    if closing:
968        labimg = ndbinary_opening( labimg, iterations=1, structure=s )
969    lab = ndlabel( labimg, structure=s )[0]
970    return lab

Relabel in 3D all labels (unique labels)

def skeleton_to_label(skel, labelled):
973def skeleton_to_label( skel, labelled ):
974    """ Transform a skeleton to label image with numbers from labelled image """
975    labels = ndlabel( np.invert(skel) )[0]
976    new_labels = find_objects( labels )
977    newlab = np.zeros( skel.shape, np.uint32 )   
978    for i, obj_slice in enumerate(new_labels):
979        if (obj_slice is not None):
980            if ((obj_slice[1].stop-obj_slice[1].start) <= 2) and ((obj_slice[0].stop-obj_slice[0].start) <= 2):
981                continue
982            label_mask = labels[obj_slice] == (i+1)
983            label_values = labelled[obj_slice][label_mask]
984            labvals, counts = np.unique(label_values, return_counts=True )
985            labval = labvals[ np.argmax(counts) ]
986            newlab[obj_slice][label_mask] = labval
987    return newlab

Transform a skeleton to label image with numbers from labelled image

def get_most_frequent(labimg, img, label):
989def get_most_frequent( labimg, img, label ):
990    """ Returns which label is the most frequent in mask """
991    mask = labimg == label
992    vals, counts = np.unique( img[mask], return_counts=True )
993    return vals[ np.argmax(counts) ]

Returns which label is the most frequent in mask

def binary_properties(labimg):
995def binary_properties( labimg ):
996    """ Returns basic label properties """
997    return regionprops( label(labimg) )

Returns basic label properties

def labels_properties(labimg):
 999def labels_properties( labimg ):
1000    """ Returns basic label properties """
1001    return regionprops( labimg )

Returns basic label properties

def labels_bbox(labimg):
1003def labels_bbox( labimg ):
1004    """ Returns for each label its bounding box """
1005    return regionprops_table( labimg, properties=('label', 'bbox') )

Returns for each label its bounding box

def tuple_int(pos):
1007def tuple_int(pos):
1008    if len(pos) == 3:
1009        return ( (int(pos[0]), int(pos[1]), int(pos[2])) )
1010    if len(pos) == 2:
1011        return ( (int(pos[0]), int(pos[1])) )
def get_consecutives(ordered):
1013def get_consecutives( ordered ):
1014    """ Returns the list of consecutives integers (already sorted) """
1015    gaps = [ [start, end] for start, end in zip( ordered, ordered[1:] ) if start+1 < end ]
1016    edges = iter( ordered[:1] + sum(gaps, []) + ordered[-1:] )
1017    return list( zip(edges, edges) )

Returns the list of consecutives integers (already sorted)

def prop_to_pos(prop, frame):
1020def prop_to_pos(prop, frame):
1021    return np.array( (frame, int(prop.centroid[0]), int(prop.centroid[1])) )
def current_frame(viewer):
1023def current_frame(viewer):
1024    return int(viewer.cursor.position[0])
def distance(x, y):
1026def distance( x, y ):
1027    """ 2d distance """
1028    return math.sqrt( (x[0]-y[0])*(x[0]-y[0]) + (x[1]-y[1])*(x[1]-y[1]) )

2d distance

def interm_position(prop, a, b):
1030def interm_position( prop, a, b ):
1031    res = [0,0]
1032    res[0] = a[0] + prop*(b[0]-a[0])
1033    res[1] = a[1] + prop*(b[1]-a[1])
1034    return res
def nb_frames(seg, lab):
1036def nb_frames( seg, lab ):
1037    """ Return nb frames with label lab """
1038    labseg = seg==lab
1039    return np.sum( np.any(labseg, axis=(1,2)) )

Return nb frames with label lab

def keep_orphans(img, comp_img, klabels):
1041def keep_orphans( img, comp_img, klabels ):
1042    """ Keep only labels that doesn't have a follower """
1043    valid_labels = np.setdiff1d(img[0], klabels)
1044    if (len(valid_labels)==1) and (valid_labels[0]==0):
1045        return
1046    labels = [val for val in valid_labels if (val!=0) and np.any(comp_img==val)]
1047    mask = np.isin(img, labels)
1048    img[mask] = 0

Keep only labels that doesn't have a follower

def keep_orphans_3d(img, klabels):
1050def keep_orphans_3d( img, klabels ):
1051    """ Keep only orphans labels or lab and olab """
1052    for label in np.unique(img[1]):
1053        if label not in klabels:
1054            if nb_frames( img, label ) == 2:
1055                img[img==label] = 0
1056    return img

Keep only orphans labels or lab and olab

def mean_nonzero(array):
1058def mean_nonzero( array ):
1059    nonzero = np.count_nonzero(array)
1060    if nonzero > 0:
1061        return np.sum(array)/nonzero
1062    return 0
def get_contours(binimg):
1064def get_contours( binimg ):
1065    """ Return the contour of a binary shape """
1066    return find_contours( binimg )

Return the contour of a binary shape

def touching_labels(img, expand=3):
1069def touching_labels( img, expand=3 ):
1070    """ Extends the labels to make them touch """
1071    return expand_labels( img, distance=expand )

Extends the labels to make them touch

def connectivity_graph(img, distance):
1073def connectivity_graph( img, distance ):
1074    """ Returns the region adjancy graph of labels """
1075    touchlab = touching_labels( img, expand=distance )
1076    return RAG( touchlab, connectivity=2 )

Returns the region adjancy graph of labels

def get_neighbor_graph(img, distance):
1078def get_neighbor_graph( img, distance ):
1079    """ Returns the adjancy graph without bg, so only neigbor cells """
1080    graph = connectivity_graph( img, distance=distance ) # be sure that labels touch and get the graph
1081    graph.remove_node(0) if 0 in graph.nodes else None
1082    return graph

Returns the adjancy graph without bg, so only neigbor cells

def get_neighbors(label, graph):
1084def get_neighbors( label, graph ):
1085    """ Get the list of neighbors of cell 'label' from the graph """
1086    if label in graph.nodes:
1087        return list(graph.adj[label])
1088    return []

Get the list of neighbors of cell 'label' from the graph

def get_boundary_cells(img):
1090def get_boundary_cells( img ):
1091    """ Return cells on tissu boundary in current image """ 
1092    dilated = binary_dilation( img > 0, disk(3) )
1093    zero = np.invert( dilated )
1094    zero = binary_dilation( zero, disk(5) )
1095    touching = np.unique( img[ zero ] ).tolist()
1096    if 0 in touching:
1097        touching.remove(0)
1098    return touching

Return cells on tissu boundary in current image

def get_border_cells(img):
1100def get_border_cells( img ):
1101    """ Return cells on border in current image """ 
1102    height = img.shape[1]
1103    width = img.shape[0]
1104    labels = list( np.unique( img[ :, 0:2 ] ) )   ## top border
1105    labels += list( np.unique( img[ :, (height-2): ] ) )   ## bottom border
1106    labels += list( np.unique( img[ 0:2,] ) )   ## left border
1107    labels += list( np.unique( img[ (width-2):,] ) )   ## right border
1108    labels = list( np.unique(labels) )
1109    return labels

Return cells on border in current image

def count_neighbors(label_img, label):
1111def count_neighbors( label_img, label ):
1112    """ Get the number of neighboring labels of given label """
1113    ## much slower than using the RAG graph
1114    # Dilate the labeled image
1115    dilated_mask = binary_dilation( label_img==label, disk(1) )
1116    nonzero = np.nonzero( dilated_mask)
1117        
1118    # Find the unique labels in the dilated region, excluding the current label and background
1119    neighboring_labels = np.unique( label_img[nonzero] ).tolist()
1120        
1121    # Add the number of unique neighboring labels
1122    return len(neighboring_labels) - 1 - 1*(0 in neighboring_labels) ## don't count itself or 0

Get the number of neighboring labels of given label

def get_cell_radius(label, labimg):
1124def get_cell_radius( label, labimg ):
1125    """ Get the radius of the cell label in labimg (2D) """
1126    area = np.sum( labimg == label )
1127    return math.sqrt( area / math.pi )

Get the radius of the cell label in labimg (2D)

def consecutive_distances(pts_pos):
1132def consecutive_distances( pts_pos ):
1133    """ Distance travelled by the cell between each frame """
1134    diff = np.diff( pts_pos, axis=0 )
1135    disp = np.linalg.norm(diff, axis=1)
1136    return disp

Distance travelled by the cell between each frame

def velocities(pts_pos):
1138def velocities( pts_pos ):
1139    """ Velocity of the cell between each frame (average between previous and next) """
1140    diff = np.diff( pts_pos, axis=0 ).astype(float)
1141    diff = np.vstack( (diff[0], diff) )
1142    diff = np.vstack( (diff, diff[-1]) )
1143    kernel=np.array([0.5,0.5])
1144    adiff = np.zeros( (len(diff)+1, 3) )
1145    for i in range(3):
1146        adiff[:,i] = np.convolve( diff[:,i], kernel )
1147    adiff = adiff[1:-1]
1148    disp = np.linalg.norm(adiff[:,1:3], axis=1)
1149    dt = adiff[:,0] 
1150    return disp/dt

Velocity of the cell between each frame (average between previous and next)

def total_distance(pts_pos):
1152def total_distance( pts_pos ):
1153    """ Total distance travelled by point with coordinates xpos and ypos """
1154    diff = np.diff( pts_pos, axis=0 )
1155    disp = np.linalg.norm(diff, axis=1)
1156    return np.sum(disp)

Total distance travelled by point with coordinates xpos and ypos

def net_distance(pts_pos):
1158def net_distance( pts_pos ):
1159    """ Net distance travelled by point with coordinates xpos and ypos """
1160    disp = pts_pos[len(pts_pos)-1] - pts_pos[0]
1161    return np.sum( np.sqrt( np.square(disp[0]) + np.square(disp[1]) ) )

Net distance travelled by point with coordinates xpos and ypos

def start_time():
1165def start_time():
1166    return time.time()
def show_duration(start_time, header=None):
1168def show_duration(start_time, header=None):
1169    if header is None:
1170        header = "Processed in "
1171    #show_info(header+"{:.3f}".format((time.time()-start_time)/60)+" min")
1172    print(header+"{:.3f}".format((time.time()-start_time)/60)+" min")
def shortcut_click_match(shortcut, event):
1176def shortcut_click_match( shortcut, event ):
1177    """ Test if the click event corresponds to the shortcut """
1178    button = 1
1179    if shortcut["button"] == "Right":
1180        button = 2
1181    if event.button != button:
1182        return False
1183    if "modifiers" in shortcut.keys():
1184        return set(list(event.modifiers)) == set(shortcut["modifiers"])
1185    else:
1186        if len(event.modifiers) > 0:
1187            return False
1188        return True

Test if the click event corresponds to the shortcut

def is_windows():
1190def is_windows():
1191    """ Is running on windows or not """
1192    try:
1193        return platform.lower().startswith("win")
1194    except:
1195        return False

Is running on windows or not

def is_darwin():
1197def is_darwin():
1198    """ Test if OS is MacOS or not """
1199    try:
1200        return platform.lower() == "darwin"
1201    except:
1202        return False

Test if OS is MacOS or not