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 widname in viewer.window._dock_widgets:
 283        wid = viewer.window._dock_widgets[widname]
 284        wid.setDisabled(True)
 285        try:
 286            wid.disconnect()
 287        except Exception:
 288            pass
 289        del viewer.window._dock_widgets[widname]
 290        wid.destroyOnClose()
 291
 292def remove_all_widgets( viewer ):
 293    """ Remove all widgets """
 294    viewer.window.remove_dock_widget("all")
 295
 296def get_metadata_field(metadata, fieldname):
 297    """ Read an imagej metadata string and get the value of fieldname """
 298    if metadata.index(fieldname+"=") < 0:
 299        return None
 300    submeta = metadata[metadata.index(fieldname+"=")+len(fieldname)+1:]
 301    value = submeta[0:submeta.index("\n")]
 302    return value
 303
 304def get_metadata_json(metadata, fieldname):
 305    """ Read a metadata from json of bioio-bioformats to get value of fieldname """
 306    if metadata.index("\""+fieldname+"\"=") < 0:
 307        return None
 308    submeta = metadata[metadata.index("\""+fieldname+"\"=")+len(fieldname)+3:]
 309    value = submeta[0:submeta.index(",")]
 310    return value
 311
 312
 313def open_image(imagepath, get_metadata=False, verbose=True):
 314    """ Open an image with bioio library """
 315    imagename, extension = os.path.splitext(imagepath)
 316    format = "all"
 317    if (extension==".tif") or (extension==".tiff"):
 318        if verbose:
 319            print("Opening Tif image "+str(imagepath)+" with bioio-tifffile")
 320        import bioio_tifffile
 321        if version_python_minor(10):
 322            from bioio import BioImage
 323            img = BioImage(imagepath, reader=bioio_tifffile.Reader)
 324        else:
 325            ## python 3.9 or under
 326            reader = bioio_tifffile.Reader
 327            img = reader(imagepath)
 328        format = "tif"
 329    else:
 330        import bioio_bioformats
 331        if verbose:
 332            print("Opening "+extension+" image "+str(imagepath)+" with bioio-bioformats")
 333        if version_python_minor(10):
 334            from bioio import BioImage
 335            img = BioImage(imagepath, reader=bioio_bioformats.Reader)
 336        else:
 337            ## python 3.9 or under
 338            reader = bioio_bioformats.Reader
 339            img = reader(imagepath)
 340    image = img.data
 341    if verbose:
 342        print(f"Loaded image shape: {image.shape}")
 343    if (len(image.shape) == 5):
 344        ## correct format of the image and metadata with TCZYX
 345        if (img.dims is not None) and len(img.dims.shape)==5 :
 346            if (img.dims.Z>1) and (img.dims.T == 1):
 347                print("Warning, movie had Z slices instead of T frames. EpiCure handles it but it might not be in other softwares/plugins")
 348                image = np.swapaxes(image, 0, 2)
 349    image = np.squeeze(image)
 350        
 351    if not get_metadata:
 352        return image, 0, 1, None, 1, None
 353
 354    try: 
 355        nchan = img.dims.C
 356        if nchan == 1:
 357            nchan = 0 ### was squeezed above
 358    except:
 359        nchan = 0
 360        pass
 361    
 362    ## spatial metadata
 363    scale_xy, unit_xy, scale_t, unit_t = None, None, None, None
 364    try:
 365        scale_xy = img.scale.X # img.physical_pixel_sizes
 366        unit_xy = img.dimension_properties.X.unit
 367    except:
 368        pass
 369
 370    try: 
 371        if unit_xy is None:
 372            if format == "all":
 373                unit_xy = get_metadata_json(img.metadata.json(), "physical_size_x_unit")
 374            elif format == "tif":
 375                unit_xy = get_metadata_field(img.metadata, "physical_size_x_unit")
 376    except:
 377        print("Reading spatial metadata might have failed. Check it manually")
 378        if scale_xy is None:
 379            scale_xy = 1
 380
 381    ## temporal metadata 
 382    try:
 383        scale_t = img.scale.T
 384        unit_t = img.dimension_properties.T.unit
 385    except:
 386        pass
 387
 388    try: 
 389        if scale_t is None:
 390                # read it from the metadata field (string) 
 391            if format == "all":
 392                scale_t = get_metadata_json(img.metadata.json(), "time_increment_unit")
 393                scale_t = float(scale_t)
 394                unit_t = get_metadata_json(img.metadata.json(), "time_increment")
 395            elif format == "tif":
 396                scale_t = get_metadata_field(img.metadata, "finterval")
 397                scale_t = float(scale_t)
 398                unit_t = get_metadata_field(img.metadata, "tunit")
 399    except:
 400        print("Reading temporal metadata might have failed. Check it manually")
 401        if scale_t is None:
 402            scale_t = 1
 403    if unit_xy is None:
 404        unit_xy = "um"
 405    if unit_t is None:
 406        unit_t = "min"
 407    return image, nchan, scale_xy, unit_xy, scale_t, unit_t
 408
 409def writeTif(img, imgname, scale, imtype, what=""):
 410    """ Write image in tif format """
 411    #TODO: change to make it with bioio
 412    if len(img.shape) == 2:
 413        tif.imwrite(imgname, np.array(img, dtype=imtype), imagej=True, resolution=[1./scale, 1./scale], metadata={'unit': 'um', 'axes': 'YX'})
 414    else:
 415        try:
 416            tif.imwrite(imgname, np.array(img, dtype=imtype), imagej=True, resolution=[1./scale, 1./scale], metadata={'unit': 'um', 'axes': 'TYX'}, compression="zstd")
 417        except:
 418            tif.imwrite(imgname, np.array(img, dtype=imtype), imagej=True, resolution=[1./scale, 1./scale], metadata={'unit': 'um', 'axes': 'TYX'})
 419    show_info(what+" saved in "+imgname)
 420
 421def appendToTif(img, imgname):
 422    """ Append to RGB tif the current image """
 423    tif.imwrite(imgname, img, photometric="rgb", append=True)
 424
 425def getCellValue(label_layer, event):
 426    """ Get the label under the click """
 427    vis = label_layer.visible
 428    if vis == False:
 429        label_layer.visible = True
 430    label = label_layer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 431    if vis == False:
 432        ## put it back to not visible state
 433        label_layer.visible = vis
 434    return label
 435
 436def setCellValue(layer, label_layer, event, newvalue, layer_frame=None, label_frame=None):
 437    """ Get the cell concerned by the event and replace its value by new one"""
 438    # get concerned label (under the cursor), layer has to be visible for this
 439    vis = label_layer.visible
 440    if vis == False:
 441        label_layer.visible = True
 442    label = label_layer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 443    label_layer.visible = vis
 444    if label is not None and label > 0:
 445        # if the seg image is 2D (single frame), label_frame will be None
 446        if label_frame is not None and label_frame >= 0:
 447            ldata = label_layer.data[label_frame,:,:]
 448        else:
 449            ldata = label_layer.data
 450        # if the layer is 2D (single frame), layer_frame will be None
 451        if layer_frame is not None and layer_frame >= 0:
 452            #slice_coord = tuple(sc[keep_coords] for sc in slice_coord)
 453            cdata = layer.data[layer_frame,:,:]
 454        else:
 455            cdata = layer.data
 456            #slice_coord = tuple(sc[keep_coords] for sc in slice_coord)
 457
 458        cdata[np.where(ldata==label)] = newvalue
 459        layer.refresh()
 460        return label
 461
 462def thin_seg_one_frame( segframe ):
 463    """ Boundaries of the frame one pixel thick """
 464    bin_img = binary_closing( find_boundaries(segframe, connectivity=2, mode="outer"), footprint=np.ones((3,3)) )
 465    skel = skeletonize( bin_img )
 466    skel = copy_border( skel, bin_img )
 467    return skeleton_to_label( skel, segframe )
 468    
 469def copy_border( skel, bin ):
 470    """ Copy the pixel border onto skeleton image """
 471    skel[[0, -1], :] = bin[[0, -1], :]  # top and bottom borders
 472    skel[:, [0, -1]] = bin[:, [0, -1]]  # left and right borders
 473    return skel
 474
 475def draw_points(pts, imshape, radius):
 476    """ Draw circle (2D) around the given points in 2D image """  
 477    image = np.zeros(imshape, dtype=bool)
 478    y, x = np.ogrid[:imshape[0], :imshape[1]]
 479    for pt in pts:
 480        # Calculate distance from pt, scaled to compare to radius
 481        distances_sq = ((y - pt[0]))**2 + ((x - pt[1]))**2 
 482        image |= distances_sq <= radius**2
 483    return image
 484
 485def get_vertices(seg, viewer=None, verbose=0, parallel=0):
 486    """ Get the vertices of the segmentation """
 487    skeleton = get_skeleton(seg, viewer, verbose, parallel)
 488    convfilter = np.array([[-1,-1,-1], [-1,3,-1],[-1,-1,-1]])
 489    novert = np.zeros(skeleton.shape, dtype=np.int8)
 490    ## pure skeleton
 491    for ind, skel in enumerate(skeleton):
 492        skeleton[ind] = medial_axis(skel)
 493        novert[ind] = signal.convolve2d(skeleton[ind], convfilter, mode="same")
 494    novert[novert<=0] = 0
 495    nodeimg = skeleton - novert
 496    nodeimg[nodeimg<=0] = 0
 497    nodeimg[nodeimg>0] = 1 
 498    return nodeimg 
 499
 500    
 501def get_skeleton( seg, viewer=None, verbose=0, parallel=0 ) :
 502    """ convert labels movie to skeleton (thin boundaries) """
 503    startt = start_time()
 504    if viewer is not None:
 505        show_progress( viewer, show=True )
 506
 507    def frame_skeleton( frame ):
 508        """ Calculate skeleton on one frame """
 509        expz = expand_labels( frame, distance=1 )
 510        frame_skel = np.zeros( frame.shape, dtype="uint8" )
 511        frame_skel[ (frame==0) * (expz>0) ] = 1
 512        return frame_skel
 513        
 514    if parallel > 0:
 515        skel = Parallel( n_jobs=parallel )(
 516            delayed(frame_skeleton)(frame) for frame in seg
 517        )
 518        skel = np.array(skel)
 519    else:
 520        skel = np.zeros(seg.shape, dtype="uint8")
 521        for z in progress(range(seg.shape[0])):
 522            expz = expand_labels( seg[z], distance=1 )
 523            skel[z][(seg[z] == 0) *(expz > 0)] = 1
 524    if verbose > 0:
 525        show_duration(startt, header="Skeleton calculted in ")
 526    if viewer is not None:
 527        show_progress( viewer, show=False )
 528    return skel
 529
 530
 531def setLabelValue(layer, label_layer, event, newvalue, layer_frame=None, label_frame=None):
 532    """ Change the label value under event position and returns its old value """
 533    ## get concerned label (under the cursor), layer has to be visible for this
 534    vis = label_layer.visible
 535    if vis == False:
 536        label_layer.visible = True
 537    label = label_layer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
 538    label_layer.visible = vis
 539    
 540    if label > 0:
 541        inds = getLabelIndexes( label_layer.data, label, label_frame )
 542        setNewLabel(layer, inds, newvalue, add_frame=layer_frame)
 543        layer.refresh()
 544        return label
 545    return None
 546
 547def getLabelIndexes(label_data, label, frame):
 548    """ Get the indixes at which label_layer is label for given frame """
 549    # if the seg image is 2D (single frame), frame will be None
 550    if frame is not None and frame >= 0:
 551        ldata = label_data[frame,:,:]
 552    else:
 553        ldata = label_data
 554    return np.argwhere( ldata==label ).tolist()
 555
 556def getLabelIndexesInFrame(frame_data, label):
 557    """ Get the indexes at which frame data is label """
 558    # if the seg image is 2D (single frame), frame will be None
 559    return np.argwhere( frame_data==label ).tolist()
 560
 561def changeLabel( label_layer, old_value, new_value ):
 562    """ replace the value of label old-value by new_value """
 563    index = np.argwhere( label_layer.data==old_value ).tolist()
 564    setNewLabel( label_layer, index, new_value )
 565
 566def setNewLabel(label_layer, indices, newvalue, add_frame=None, return_old=True):
 567    """ Change the label of all the pixels indicated by indices """
 568    indexs = np.array(indices).T
 569    if add_frame is not None:
 570        indexs = np.vstack((np.repeat(add_frame, indexs.shape[1]), indexs))
 571    changed_indices = label_layer.data[tuple(indexs)] != newvalue
 572    inds = tuple(x[changed_indices] for x in indexs)
 573    oldvalues = None
 574    if return_old:
 575        oldvalues = label_layer.data[inds]
 576    if isinstance(newvalue, list):
 577        newvalue = np.array(newvalue)[np.where(changed_indices)[0]]
 578    label_layer.data_setitem( inds, newvalue )
 579    return inds, newvalue, oldvalues 
 580
 581def convert_coords( coord ):
 582    """ Get the time frame, and the 2D coordinates as int """
 583    int_coord = tuple(np.round(coord).astype(int))
 584    tframe = int(coord[0])
 585    int_coord = int_coord[1:3]
 586    return tframe, int_coord
 587
 588def outerBBox2D(bbox, imshape, margin=0):
 589    if (bbox[0]-margin) <= 0:
 590        return True
 591    if (bbox[2]+margin) >= imshape[0]:
 592        return True
 593    if (bbox[1]-margin) <= 0:
 594        return True
 595    if (bbox[3]+margin) >= imshape[1]:
 596        return True
 597    return False
 598
 599def isInsideBBox( bbox, obbox ):
 600    """ Check if bbox is included in obbox """
 601    if (bbox[0] >= obbox[0]) and (bbox[1] >= obbox[1]):
 602        return (bbox[2] <= obbox[2]) and (bbox[3] <= obbox[3])
 603    return False
 604
 605def setBBox(position, extend, imshape):
 606    bbox = [
 607        max(int(position[0] - extend), 0),
 608        max(int(position[1] - extend), 0),
 609        max(int(position[2] - extend), 0),
 610        min(int(position[0] + extend), imshape[0]),
 611        min(int(position[1] + extend), imshape[1]),
 612        min(int(position[2] + extend), imshape[2])
 613    ]
 614    return bbox
 615
 616def setBBoxXY(position, extend, imshape):
 617    bbox = [
 618        max(int(position[0]), 0),
 619        max(int(position[1] - extend), 0),
 620        max(int(position[2] - extend), 0),
 621        min(int(position[0] + 1), imshape[0]),
 622        min(int(position[1] + extend), imshape[1]),
 623        min(int(position[2] + extend), imshape[2])
 624    ]
 625    return bbox
 626
 627def getBBox2DFromPts(pts, extend, imshape):
 628    """ Get the bounding box surrounding all the points, plus a margin """
 629    arr = np.array(pts)
 630    ptsdim = arr.shape[1]
 631    if ptsdim == 2:
 632        bbox = [
 633            max( int(np.min(arr[:,0])) - extend, 0), 
 634            max( int(np.min(arr[:,1])) - extend, 0), 
 635            min( int(np.max(arr[:,0]))+1+extend, imshape[0]), 
 636            min( int(np.max(arr[:,1]))+1+extend, imshape[1] )
 637            ]
 638    if ptsdim == 3:
 639        bbox = [
 640            max( int(np.min(arr[:,1])) -extend, 0), 
 641            max( int(np.min(arr[:,2])) - extend, 0),
 642            min( int(np.max(arr[:,1]))+1 + extend, imshape[0]), 
 643            min( int(np.max(arr[:,2]))+1 + extend, imshape[1] )
 644            ]
 645
 646    return bbox
 647
 648def getBBoxFromPts(pts, extend, imshape, outdim=None, frame=None):
 649    arr = np.array(pts)
 650    ## get if points are 2D or 3D
 651    ptsdim = arr.shape[1]
 652    ## if not imposed, output the same dimension as input points
 653    if outdim is None:
 654        outdim = ptsdim
 655    ## Get bounding box from points according to dimensions
 656    if ptsdim == 2:
 657        if outdim == 2:
 658            bbox = [int(np.min(arr[:,0])), int(np.min(arr[:,1])), int(np.max(arr[:,0]))+1, int(np.max(arr[:,1]))+1]
 659        else:
 660            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]
 661    if ptsdim == 3:
 662        if outdim == 2:
 663            bbox = [int(np.min(arr[:,1])), int(np.min(arr[:,2])), int(np.max(arr[:,1]))+1, int(np.max(arr[:,2]))+1]
 664        else:
 665            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]
 666    if extend > 0:
 667        for i in range(outdim):
 668            if i < 2:
 669                bbox[(outdim==3)+i] = max( bbox[(outdim==3)+i] - extend, 0)
 670                bbox[(outdim==3)+i+outdim] = min(bbox[(outdim==3)+i+outdim] + extend, imshape[(outdim==3)+i] )
 671    return bbox
 672
 673def inside_bounds( pt, imshape ):
 674    """ Check if given point is inside image limits """
 675    return all(0 <= pt[i] < imshape[i] for i in range(len(pt)))
 676
 677def extendBBox2D( bbox, extend_factor, imshape ):
 678    """ Extend bounding box with given margin """
 679    extend = max(bbox[2] - bbox[0], bbox[3] - bbox[1]) * extend_factor
 680    bbox = np.array(bbox)
 681    bbox[:2] = np.maximum(bbox[:2] - extend, 0)
 682    bbox[2:] = np.minimum(bbox[2:] + extend, imshape[:2])
 683    return bbox
 684
 685def getBBox2D(img, label):
 686    """ Get bounding box of label """
 687    mask = (img==label)*1
 688    props = regionprops(mask)
 689    for prop in props:
 690        bbox = prop.bbox
 691        return bbox
 692
 693def getPropLabel(img, label):
 694    """ Get the properties of label """
 695    mask = np.uint8(img == label)
 696    props = regionprops(mask)
 697    return props[0]
 698
 699def getBBoxLabel(img, label):
 700    """ Get bounding box of label """
 701    mask_ind = np.where(img==label)
 702    if len(mask_ind) <= 0:
 703        return None
 704    dim = len(img.shape)
 705    bbox = np.zeros(dim*2, int)
 706    for i in range(dim):
 707        bbox[i] = int(np.min(mask_ind[i]))
 708        bbox[i+dim] = int(np.max(mask_ind[i]))+1
 709    return bbox
 710
 711def getBBox2DMerge(img, label, second_label): #, checkTouching=False):
 712    """ Get bounding box of two labels and check if they are in contact """
 713    mask = np.isin( img, [label, second_label] )
 714    props = regionprops(mask*1)
 715    return props[0].bbox, mask 
 716
 717
 718def frame_to_skeleton(frame, connectivity=1):
 719    """ convert labels frame to skeleton (thin boundaries) """
 720    return skeletonize( find_boundaries(frame, connectivity=connectivity, mode="outer") )
 721
 722def remove_boundaries(img):
 723    """ Put the boundaries pixels between labels as 0 """
 724    bound = frame_to_skeleton( img, connectivity=1 )
 725    img[bound>0] = 0
 726    return img
 727
 728def ind_boundaries(img):
 729    """ Get indices of the boundaries pixels between two labels """
 730    bound = frame_to_skeleton( img, connectivity=1 )
 731    return np.argwhere(bound>0)
 732
 733def checkTouchingLabels(img, label, second_label):
 734    """ Returns if labels are in contact (1-2 pixel away) """
 735    disk_one = disk(radius=1)
 736    maska = binary_dilation(img==label, footprint=disk_one)
 737    maskb = binary_dilation(img==second_label, footprint=disk_one)
 738    return np.any(maska & maskb)
 739
 740def positionsIn2DBBox( positions, bbox ):
 741    """ Shift all the positions to their position inside the 2D bounding box """
 742    return [positionIn2DBBox( pos, bbox ) for pos in positions ]
 743
 744def positions2DIn2DBBox( positions, bbox ):
 745    """ Shift all the positions to their position inside the 2D bounding box """
 746    return [position2DIn2DBBox( pos, bbox ) for pos in positions ]
 747
 748def positionIn2DBBox(position, bbox):
 749    """ Returns the position shifted to its position inside the 2D bounding box """
 750    return (int(position[1]-bbox[0]), int(position[2]-bbox[1]))
 751
 752def position2DIn2DBBox(position, bbox):
 753    """ Returns the position shifted to its position inside the 2D bounding box """
 754    return (int(position[0]-bbox[0]), int(position[1]-bbox[1]))
 755
 756def toFullImagePos(indices, bbox):
 757    indices = np.array(indices)
 758    return np.column_stack((indices[:, 0] + bbox[0], indices[:, 1] + bbox[1])).tolist()
 759
 760def addFrameIndices( indices, frame ):
 761    return [ [frame, ind[0], ind[1]] for ind in indices ]
 762
 763def shiftFrameIndices( indices, add_frame ):
 764    if isinstance( indices, list ):
 765        indices = np.array(indices)
 766    indices[:, 0] += add_frame
 767    return indices.tolist()
 768
 769def shiftFrames( indices, frames ):
 770    if isinstance( indices, list ):
 771        indices = np.array(indices)
 772    indices[:, 0] = frames[indices[:, 0]]
 773    return indices.tolist()
 774
 775def toFullMoviePos( indices, bbox, frame=None ):
 776    """ Replace indexes inside bounding box to full movie indexes """
 777    indices = np.array(indices)
 778    if frame is not None:
 779        frame_arr = np.full(len(indices), frame)
 780        return np.column_stack((frame_arr, indices[:, 0] + bbox[0], indices[:, 1] + bbox[1]))
 781    if len(bbox) == 6:
 782        return np.column_stack((indices[:, 0] + bbox[0], indices[:, 1] + bbox[1], indices[:, 2] + bbox[2]))
 783    return np.column_stack((indices[:, 0], indices[:, 1] + bbox[0], indices[:, 2] + bbox[1]))
 784
 785def cropBBox(img, bbox):
 786    slices = tuple(slice(bbox[i], bbox[i + len(bbox) // 2]) for i in range(len(bbox) // 2))
 787    return img[slices]
 788
 789def crop_twoframes( img, bbox, frame ):
 790    """ Crop bounding box with two frames """
 791    return np.copy(img[(frame-1):(frame+1), bbox[0]:bbox[2], bbox[1]:bbox[3]])
 792
 793def cropBBox2D(img, bbox):
 794    return img[bbox[0]:bbox[2], bbox[1]:bbox[3]]
 795
 796def setValueInBBox2D(img, setimg, bbox):
 797    bbimg = img[bbox[0]:bbox[2], bbox[1]:bbox[3]] 
 798    bbimg[setimg>0]= setimg[setimg>0]
 799
 800def addValueInBBox(img, addimg, bbox):
 801    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
 802
 803def set_maxlabel(layer):
 804    layer.mode = "PAINT"
 805    layer.selected_label = np.max(layer.data)+1
 806    layer.refresh()
 807
 808def set_label(layer, lab):
 809    layer.mode = "PAINT"
 810    layer.selected_label = lab
 811    layer.refresh()
 812
 813def get_free_labels( used, nlab ):
 814    """ Get n-th unused label (not in used list) """
 815    maxlab = max(used)+1
 816    unused = list(set(range(1, maxlab)) - set(used))
 817    if nlab < len(unused):
 818        return unused[0:nlab]
 819    else:
 820        return unused+list(range(maxlab+1, maxlab+1+(nlab-len(unused))))
 821
 822def get_next_label(layer, label):
 823    """ Get the next unused label starting from label """
 824    used = np.unique(layer.data)
 825    i = label+1
 826    while i < np.max(used):
 827        if i>0 and (i not in used):
 828            return i
 829        i = i + 1
 830    return i+1
 831
 832def relabel_layer(layer):
 833    maxlab = np.max(layer.data)
 834    used = np.unique(layer.data)
 835    nlabs = len(used)
 836    if nlabs == maxlab:
 837        #print("already relabelled")
 838        return
 839    for j in range(1, nlabs+1):
 840        if j not in used:
 841            layer.data[layer.data==maxlab] = j
 842            maxlab = np.max(layer.data)
 843    show_info("Labels reordered")
 844    layer.refresh()
 845
 846def inv_visibility(viewer, layername):
 847    """ Switch the visibility of a layer """
 848    if layername in viewer.layers:
 849        layer = viewer.layers[layername]
 850        layer.visible = not layer.visible
 851
 852######## Measure labels
 853def average_area( seg ):
 854    """ Average area of labels (cells) """
 855    # Label the input image
 856    labeled_array, num_features = ndlabel(seg)
 857    
 858    if num_features == 0:
 859        return 0.0
 860    
 861    # Calculate the area of each label
 862    areas = ndsum(seg > 0, labeled_array, index=np.arange(1, num_features + 1))
 863    # Calculate the average area
 864    avg_area = np.mean(areas)   
 865    return avg_area
 866
 867
 868def summary_labels( seg ):
 869    """ Summary of labels (cells) measurements """
 870    props = regionprops(seg)
 871    avg_duration = 0
 872    avg_area = 0.0
 873    for prop in props:
 874        bbox = prop.bbox
 875        nz = 1
 876        if len(bbox)>4:
 877            nz = bbox[3]-bbox[0]
 878        avg_duration += nz
 879        avg_area += prop.area/nz
 880    return len(props), avg_duration/len(props), avg_area/len(props) 
 881
 882def labels_in_cell( sega, segb, label ):
 883    """ Look at the labels of segb inside label from sega """
 884    cell = np.isin( sega, [label] )
 885    labelb = segb[ cell ]
 886    cell_area = np.sum( cell*1, axis=None )
 887    filled_area = np.sum( labelb>0 )
 888    nobj = len(np.unique( labelb ))
 889    if 0 in labelb:
 890        nobj = nobj - 1
 891    return nobj, (filled_area/cell_area), np.unique(labelb)
 892
 893
 894def match_labels( sega, segb ):
 895    """ Match the labels of the two segmentation images """
 896    region_properties = ["label", "centroid"]
 897
 898    df0 = pd.DataFrame( regionprops_table( sega, properties=region_properties ) )
 899    df0["frame"] = 0
 900    df1 = pd.DataFrame( regionprops_table( segb, properties=region_properties ) )
 901    df1["frame"] = 1
 902    df = pd.concat([df0, df1])
 903
 904    ## Link the two frames with LapTrack tracking
 905    laptrack = LaptrackCentroids(None, None)
 906    laptrack.max_distance = 10 
 907    laptrack.set_region_properties(with_extra=False)
 908    laptrack.splitting_cost = False ## disable splitting option
 909    laptrack.merging_cost = False ## disable merging option
 910    labels = list(np.unique(segb))
 911    if 0 in labels:
 912        labels.remove(0)
 913    parent_labels = laptrack.twoframes_track(df, labels)
 914    return parent_labels, labels
 915
 916def labels_table( labimg, intensity_image=None, properties=None, extra_properties=None ):
 917    """ Returns the regionprops_table of the labels """
 918    if properties is None:
 919        properties = ['label', 'centroid']
 920    if intensity_image is not None:
 921        return regionprops_table( labimg, intensity_image=intensity_image, properties=properties, extra_properties=extra_properties )
 922    return regionprops_table( labimg, properties=properties, extra_properties=extra_properties )
 923
 924def labels_to_table( labimg, frame ):
 925    """ Get label and centroid """
 926    labels = np.unique(labimg.ravel())
 927    labels = labels[labels != 0]
 928    centroids = center_of_mass(labimg, labels=labimg, index=labels)
 929    table = np.column_stack((labels, np.full(len(labels), frame), centroids))
 930    return table.astype(int)
 931
 932def labels_to_table_v1( labimg, frame ):
 933    """ Get label and centroid """
 934    props = regionprops( labimg )
 935    n = len(props)
 936    if n == 0:
 937        return np.empty( (0, 2+labimg.ndim) )
 938    res = np.zeros( (n, 2+labimg.ndim), dtype=int )
 939    for i, prop in enumerate(props):
 940        res[i, 0] = prop.label
 941        res[i, 1] = frame
 942        res[i,:2] = np.array(prop.centroid).astype(int)
 943    return res
 944
 945def non_unique_labels( labimg ):
 946    """ Check if contains only unique labels """
 947    relab, nlabels = ndlabel( labimg )
 948    return nlabels > (len( np.unique(labimg) )-1)
 949
 950def reset_labels( labimg, closing=True ):
 951    """ Relabel in 3D all labels (unique labels) """
 952    s = ndi_structure(3,1)
 953    ## ignore 3D connectivity (unique labels in all frames)
 954    s[0,:,:] = 0
 955    s[2,:,:] = 0
 956    if closing:
 957        labimg = ndbinary_opening( labimg, iterations=1, structure=s )
 958    lab = ndlabel( labimg, structure=s )[0]
 959    return lab
 960
 961    
 962def skeleton_to_label( skel, labelled ):
 963    """ Transform a skeleton to label image with numbers from labelled image """
 964    labels = ndlabel( np.invert(skel) )[0]
 965    new_labels = find_objects( labels )
 966    newlab = np.zeros( skel.shape, np.uint32 )   
 967    for i, obj_slice in enumerate(new_labels):
 968        if (obj_slice is not None):
 969            if ((obj_slice[1].stop-obj_slice[1].start) <= 2) and ((obj_slice[0].stop-obj_slice[0].start) <= 2):
 970                continue
 971            label_mask = labels[obj_slice] == (i+1)
 972            label_values = labelled[obj_slice][label_mask]
 973            labvals, counts = np.unique(label_values, return_counts=True )
 974            labval = labvals[ np.argmax(counts) ]
 975            newlab[obj_slice][label_mask] = labval
 976    return newlab
 977
 978def get_most_frequent( labimg, img, label ):
 979    """ Returns which label is the most frequent in mask """
 980    mask = labimg == label
 981    vals, counts = np.unique( img[mask], return_counts=True )
 982    return vals[ np.argmax(counts) ]
 983
 984def binary_properties( labimg ):
 985    """ Returns basic label properties """
 986    return regionprops( label(labimg) )
 987
 988def labels_properties( labimg ):
 989    """ Returns basic label properties """
 990    return regionprops( labimg )
 991
 992def labels_bbox( labimg ):
 993    """ Returns for each label its bounding box """
 994    return regionprops_table( labimg, properties=('label', 'bbox') )
 995
 996def tuple_int(pos):
 997    if len(pos) == 3:
 998        return ( (int(pos[0]), int(pos[1]), int(pos[2])) )
 999    if len(pos) == 2:
1000        return ( (int(pos[0]), int(pos[1])) )
1001
1002def get_consecutives( ordered ):
1003    """ Returns the list of consecutives integers (already sorted) """
1004    gaps = [ [start, end] for start, end in zip( ordered, ordered[1:] ) if start+1 < end ]
1005    edges = iter( ordered[:1] + sum(gaps, []) + ordered[-1:] )
1006    return list( zip(edges, edges) )
1007
1008
1009def prop_to_pos(prop, frame):
1010    return np.array( (frame, int(prop.centroid[0]), int(prop.centroid[1])) )
1011
1012def current_frame(viewer):
1013    return int(viewer.cursor.position[0])
1014
1015def distance( x, y ):
1016    """ 2d distance """
1017    return math.sqrt( (x[0]-y[0])*(x[0]-y[0]) + (x[1]-y[1])*(x[1]-y[1]) )
1018
1019def interm_position( prop, a, b ):
1020    res = [0,0]
1021    res[0] = a[0] + prop*(b[0]-a[0])
1022    res[1] = a[1] + prop*(b[1]-a[1])
1023    return res
1024
1025def nb_frames( seg, lab ):
1026    """ Return nb frames with label lab """
1027    labseg = seg==lab
1028    return np.sum( np.any(labseg, axis=(1,2)) )
1029
1030def keep_orphans( img, comp_img, klabels ):
1031    """ Keep only labels that doesn't have a follower """
1032    valid_labels = np.setdiff1d(img[0], klabels)
1033    if (len(valid_labels)==1) and (valid_labels[0]==0):
1034        return
1035    labels = [val for val in valid_labels if (val!=0) and np.any(comp_img==val)]
1036    mask = np.isin(img, labels)
1037    img[mask] = 0
1038
1039def keep_orphans_3d( img, klabels ):
1040    """ Keep only orphans labels or lab and olab """
1041    for label in np.unique(img[1]):
1042        if label not in klabels:
1043            if nb_frames( img, label ) == 2:
1044                img[img==label] = 0
1045    return img
1046
1047def mean_nonzero( array ):
1048    nonzero = np.count_nonzero(array)
1049    if nonzero > 0:
1050        return np.sum(array)/nonzero
1051    return 0
1052
1053def get_contours( binimg ):
1054    """ Return the contour of a binary shape """
1055    return find_contours( binimg )
1056
1057###### Connectivity labels
1058def touching_labels( img, expand=3 ):
1059    """ Extends the labels to make them touch """
1060    return expand_labels( img, distance=expand )
1061
1062def connectivity_graph( img, distance ):
1063    """ Returns the region adjancy graph of labels """
1064    touchlab = touching_labels( img, expand=distance )
1065    return RAG( touchlab, connectivity=2 )
1066
1067def get_neighbor_graph( img, distance ):
1068    """ Returns the adjancy graph without bg, so only neigbor cells """
1069    graph = connectivity_graph( img, distance=distance ) # be sure that labels touch and get the graph
1070    graph.remove_node(0) if 0 in graph.nodes else None
1071    return graph
1072
1073def get_neighbors( label, graph ):
1074    """ Get the list of neighbors of cell 'label' from the graph """
1075    if label in graph.nodes:
1076        return list(graph.adj[label])
1077    return []
1078    
1079def get_boundary_cells( img ):
1080    """ Return cells on tissu boundary in current image """ 
1081    dilated = binary_dilation( img > 0, disk(3) )
1082    zero = np.invert( dilated )
1083    zero = binary_dilation( zero, disk(5) )
1084    touching = np.unique( img[ zero ] ).tolist()
1085    if 0 in touching:
1086        touching.remove(0)
1087    return touching
1088    
1089def get_border_cells( img ):
1090    """ Return cells on border in current image """ 
1091    height = img.shape[1]
1092    width = img.shape[0]
1093    labels = list( np.unique( img[ :, 0:2 ] ) )   ## top border
1094    labels += list( np.unique( img[ :, (height-2): ] ) )   ## bottom border
1095    labels += list( np.unique( img[ 0:2,] ) )   ## left border
1096    labels += list( np.unique( img[ (width-2):,] ) )   ## right border
1097    labels = list( np.unique(labels) )
1098    return labels
1099
1100def count_neighbors( label_img, label ):
1101    """ Get the number of neighboring labels of given label """
1102    ## much slower than using the RAG graph
1103    # Dilate the labeled image
1104    dilated_mask = binary_dilation( label_img==label, disk(1) )
1105    nonzero = np.nonzero( dilated_mask)
1106        
1107    # Find the unique labels in the dilated region, excluding the current label and background
1108    neighboring_labels = np.unique( label_img[nonzero] ).tolist()
1109        
1110    # Add the number of unique neighboring labels
1111    return len(neighboring_labels) - 1 - 1*(0 in neighboring_labels) ## don't count itself or 0
1112
1113def get_cell_radius( label, labimg ):
1114    """ Get the radius of the cell label in labimg (2D) """
1115    area = np.sum( labimg == label )
1116    return math.sqrt( area / math.pi )
1117
1118
1119####### Distance measures
1120
1121def consecutive_distances( pts_pos ):
1122    """ Distance travelled by the cell between each frame """
1123    diff = np.diff( pts_pos, axis=0 )
1124    disp = np.linalg.norm(diff, axis=1)
1125    return disp
1126
1127def velocities( pts_pos ):
1128    """ Velocity of the cell between each frame (average between previous and next) """
1129    diff = np.diff( pts_pos, axis=0 ).astype(float)
1130    diff = np.vstack( (diff[0], diff) )
1131    diff = np.vstack( (diff, diff[-1]) )
1132    kernel=np.array([0.5,0.5])
1133    adiff = np.zeros( (len(diff)+1, 3) )
1134    for i in range(3):
1135        adiff[:,i] = np.convolve( diff[:,i], kernel )
1136    adiff = adiff[1:-1]
1137    disp = np.linalg.norm(adiff[:,1:3], axis=1)
1138    dt = adiff[:,0] 
1139    return disp/dt
1140
1141def total_distance( pts_pos ):
1142    """ Total distance travelled by point with coordinates xpos and ypos """
1143    diff = np.diff( pts_pos, axis=0 )
1144    disp = np.linalg.norm(diff, axis=1)
1145    return np.sum(disp)
1146
1147def net_distance( pts_pos ):
1148    """ Net distance travelled by point with coordinates xpos and ypos """
1149    disp = pts_pos[len(pts_pos)-1] - pts_pos[0]
1150    return np.sum( np.sqrt( np.square(disp[0]) + np.square(disp[1]) ) )
1151
1152
1153###### Time measures
1154def start_time():
1155    return time.time()
1156
1157def show_duration(start_time, header=None):
1158    if header is None:
1159        header = "Processed in "
1160    #show_info(header+"{:.3f}".format((time.time()-start_time)/60)+" min")
1161    print(header+"{:.3f}".format((time.time()-start_time)/60)+" min")
1162
1163###### Preferences/shortcuts 
1164
1165def shortcut_click_match( shortcut, event ):
1166    """ Test if the click event corresponds to the shortcut """
1167    button = 1
1168    if shortcut["button"] == "Right":
1169        button = 2
1170    if event.button != button:
1171        return False
1172    if "modifiers" in shortcut.keys():
1173        return set(list(event.modifiers)) == set(shortcut["modifiers"])
1174    else:
1175        if len(event.modifiers) > 0:
1176            return False
1177        return True
1178
1179def is_windows():
1180    """ Is running on windows or not """
1181    try:
1182        return platform.lower().startswith("win")
1183    except:
1184        return False
1185
1186def is_darwin():
1187    """ Test if OS is MacOS or not """
1188    try:
1189        return platform.lower() == "darwin"
1190    except:
1191        return False
1192        
1193def print_shortcuts( shortcut_group ):
1194    """ Put to text the subset of shortcuts """
1195    text = ""
1196    for short_name, vals in shortcut_group.items():
1197        if vals["type"] == "key":
1198            text += "  <"+vals["key"]+"> "+vals["text"]+"\n"
1199        if vals["type"] == "click":
1200            modif = ""
1201            if "modifiers" in vals.keys():
1202                modifiers = vals["modifiers"]
1203                for mod in modifiers:
1204                    if mod == "Control":
1205                        if is_darwin():
1206                            modif += "Command"+"-"
1207                        else:
1208                            modif += mod+"-"
1209                    else:
1210                        if mod == "Alt":
1211                            if is_darwin():
1212                                modif += "Option"+"-"
1213                            else:
1214                                modif += mod+"-"
1215                        else:
1216                            modif += mod+"-"
1217            text += "  <"+modif+vals["button"]+"-click> "+vals["text"]+"\n"
1218    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 widname in viewer.window._dock_widgets:
284        wid = viewer.window._dock_widgets[widname]
285        wid.setDisabled(True)
286        try:
287            wid.disconnect()
288        except Exception:
289            pass
290        del viewer.window._dock_widgets[widname]
291        wid.destroyOnClose()

Remove a widget from the viewer

def remove_all_widgets(viewer):
293def remove_all_widgets( viewer ):
294    """ Remove all widgets """
295    viewer.window.remove_dock_widget("all")

Remove all widgets

def get_metadata_field(metadata, fieldname):
297def get_metadata_field(metadata, fieldname):
298    """ Read an imagej metadata string and get the value of fieldname """
299    if metadata.index(fieldname+"=") < 0:
300        return None
301    submeta = metadata[metadata.index(fieldname+"=")+len(fieldname)+1:]
302    value = submeta[0:submeta.index("\n")]
303    return value

Read an imagej metadata string and get the value of fieldname

def get_metadata_json(metadata, fieldname):
305def get_metadata_json(metadata, fieldname):
306    """ Read a metadata from json of bioio-bioformats to get value of fieldname """
307    if metadata.index("\""+fieldname+"\"=") < 0:
308        return None
309    submeta = metadata[metadata.index("\""+fieldname+"\"=")+len(fieldname)+3:]
310    value = submeta[0:submeta.index(",")]
311    return value

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

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

Open an image with bioio library

def writeTif(img, imgname, scale, imtype, what=''):
410def writeTif(img, imgname, scale, imtype, what=""):
411    """ Write image in tif format """
412    #TODO: change to make it with bioio
413    if len(img.shape) == 2:
414        tif.imwrite(imgname, np.array(img, dtype=imtype), imagej=True, resolution=[1./scale, 1./scale], metadata={'unit': 'um', 'axes': 'YX'})
415    else:
416        try:
417            tif.imwrite(imgname, np.array(img, dtype=imtype), imagej=True, resolution=[1./scale, 1./scale], metadata={'unit': 'um', 'axes': 'TYX'}, compression="zstd")
418        except:
419            tif.imwrite(imgname, np.array(img, dtype=imtype), imagej=True, resolution=[1./scale, 1./scale], metadata={'unit': 'um', 'axes': 'TYX'})
420    show_info(what+" saved in "+imgname)

Write image in tif format

def appendToTif(img, imgname):
422def appendToTif(img, imgname):
423    """ Append to RGB tif the current image """
424    tif.imwrite(imgname, img, photometric="rgb", append=True)

Append to RGB tif the current image

def getCellValue(label_layer, event):
426def getCellValue(label_layer, event):
427    """ Get the label under the click """
428    vis = label_layer.visible
429    if vis == False:
430        label_layer.visible = True
431    label = label_layer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
432    if vis == False:
433        ## put it back to not visible state
434        label_layer.visible = vis
435    return label

Get the label under the click

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

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

def thin_seg_one_frame(segframe):
463def thin_seg_one_frame( segframe ):
464    """ Boundaries of the frame one pixel thick """
465    bin_img = binary_closing( find_boundaries(segframe, connectivity=2, mode="outer"), footprint=np.ones((3,3)) )
466    skel = skeletonize( bin_img )
467    skel = copy_border( skel, bin_img )
468    return skeleton_to_label( skel, segframe )

Boundaries of the frame one pixel thick

def copy_border(skel, bin):
470def copy_border( skel, bin ):
471    """ Copy the pixel border onto skeleton image """
472    skel[[0, -1], :] = bin[[0, -1], :]  # top and bottom borders
473    skel[:, [0, -1]] = bin[:, [0, -1]]  # left and right borders
474    return skel

Copy the pixel border onto skeleton image

def draw_points(pts, imshape, radius):
476def draw_points(pts, imshape, radius):
477    """ Draw circle (2D) around the given points in 2D image """  
478    image = np.zeros(imshape, dtype=bool)
479    y, x = np.ogrid[:imshape[0], :imshape[1]]
480    for pt in pts:
481        # Calculate distance from pt, scaled to compare to radius
482        distances_sq = ((y - pt[0]))**2 + ((x - pt[1]))**2 
483        image |= distances_sq <= radius**2
484    return image

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

def get_vertices(seg, viewer=None, verbose=0, parallel=0):
486def get_vertices(seg, viewer=None, verbose=0, parallel=0):
487    """ Get the vertices of the segmentation """
488    skeleton = get_skeleton(seg, viewer, verbose, parallel)
489    convfilter = np.array([[-1,-1,-1], [-1,3,-1],[-1,-1,-1]])
490    novert = np.zeros(skeleton.shape, dtype=np.int8)
491    ## pure skeleton
492    for ind, skel in enumerate(skeleton):
493        skeleton[ind] = medial_axis(skel)
494        novert[ind] = signal.convolve2d(skeleton[ind], convfilter, mode="same")
495    novert[novert<=0] = 0
496    nodeimg = skeleton - novert
497    nodeimg[nodeimg<=0] = 0
498    nodeimg[nodeimg>0] = 1 
499    return nodeimg 

Get the vertices of the segmentation

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

convert labels movie to skeleton (thin boundaries)

def setLabelValue( layer, label_layer, event, newvalue, layer_frame=None, label_frame=None):
532def setLabelValue(layer, label_layer, event, newvalue, layer_frame=None, label_frame=None):
533    """ Change the label value under event position and returns its old value """
534    ## get concerned label (under the cursor), layer has to be visible for this
535    vis = label_layer.visible
536    if vis == False:
537        label_layer.visible = True
538    label = label_layer.get_value(position=event.position, view_direction = event.view_direction, dims_displayed=event.dims_displayed, world=True)
539    label_layer.visible = vis
540    
541    if label > 0:
542        inds = getLabelIndexes( label_layer.data, label, label_frame )
543        setNewLabel(layer, inds, newvalue, add_frame=layer_frame)
544        layer.refresh()
545        return label
546    return None

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

def getLabelIndexes(label_data, label, frame):
548def getLabelIndexes(label_data, label, frame):
549    """ Get the indixes at which label_layer is label for given frame """
550    # if the seg image is 2D (single frame), frame will be None
551    if frame is not None and frame >= 0:
552        ldata = label_data[frame,:,:]
553    else:
554        ldata = label_data
555    return np.argwhere( ldata==label ).tolist()

Get the indixes at which label_layer is label for given frame

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

Get the indexes at which frame data is label

def changeLabel(label_layer, old_value, new_value):
562def changeLabel( label_layer, old_value, new_value ):
563    """ replace the value of label old-value by new_value """
564    index = np.argwhere( label_layer.data==old_value ).tolist()
565    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):
567def setNewLabel(label_layer, indices, newvalue, add_frame=None, return_old=True):
568    """ Change the label of all the pixels indicated by indices """
569    indexs = np.array(indices).T
570    if add_frame is not None:
571        indexs = np.vstack((np.repeat(add_frame, indexs.shape[1]), indexs))
572    changed_indices = label_layer.data[tuple(indexs)] != newvalue
573    inds = tuple(x[changed_indices] for x in indexs)
574    oldvalues = None
575    if return_old:
576        oldvalues = label_layer.data[inds]
577    if isinstance(newvalue, list):
578        newvalue = np.array(newvalue)[np.where(changed_indices)[0]]
579    label_layer.data_setitem( inds, newvalue )
580    return inds, newvalue, oldvalues 

Change the label of all the pixels indicated by indices

def convert_coords(coord):
582def convert_coords( coord ):
583    """ Get the time frame, and the 2D coordinates as int """
584    int_coord = tuple(np.round(coord).astype(int))
585    tframe = int(coord[0])
586    int_coord = int_coord[1:3]
587    return tframe, int_coord

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

def outerBBox2D(bbox, imshape, margin=0):
589def outerBBox2D(bbox, imshape, margin=0):
590    if (bbox[0]-margin) <= 0:
591        return True
592    if (bbox[2]+margin) >= imshape[0]:
593        return True
594    if (bbox[1]-margin) <= 0:
595        return True
596    if (bbox[3]+margin) >= imshape[1]:
597        return True
598    return False
def isInsideBBox(bbox, obbox):
600def isInsideBBox( bbox, obbox ):
601    """ Check if bbox is included in obbox """
602    if (bbox[0] >= obbox[0]) and (bbox[1] >= obbox[1]):
603        return (bbox[2] <= obbox[2]) and (bbox[3] <= obbox[3])
604    return False

Check if bbox is included in obbox

def setBBox(position, extend, imshape):
606def setBBox(position, extend, imshape):
607    bbox = [
608        max(int(position[0] - extend), 0),
609        max(int(position[1] - extend), 0),
610        max(int(position[2] - extend), 0),
611        min(int(position[0] + extend), imshape[0]),
612        min(int(position[1] + extend), imshape[1]),
613        min(int(position[2] + extend), imshape[2])
614    ]
615    return bbox
def setBBoxXY(position, extend, imshape):
617def setBBoxXY(position, extend, imshape):
618    bbox = [
619        max(int(position[0]), 0),
620        max(int(position[1] - extend), 0),
621        max(int(position[2] - extend), 0),
622        min(int(position[0] + 1), imshape[0]),
623        min(int(position[1] + extend), imshape[1]),
624        min(int(position[2] + extend), imshape[2])
625    ]
626    return bbox
def getBBox2DFromPts(pts, extend, imshape):
628def getBBox2DFromPts(pts, extend, imshape):
629    """ Get the bounding box surrounding all the points, plus a margin """
630    arr = np.array(pts)
631    ptsdim = arr.shape[1]
632    if ptsdim == 2:
633        bbox = [
634            max( int(np.min(arr[:,0])) - extend, 0), 
635            max( int(np.min(arr[:,1])) - extend, 0), 
636            min( int(np.max(arr[:,0]))+1+extend, imshape[0]), 
637            min( int(np.max(arr[:,1]))+1+extend, imshape[1] )
638            ]
639    if ptsdim == 3:
640        bbox = [
641            max( int(np.min(arr[:,1])) -extend, 0), 
642            max( int(np.min(arr[:,2])) - extend, 0),
643            min( int(np.max(arr[:,1]))+1 + extend, imshape[0]), 
644            min( int(np.max(arr[:,2]))+1 + extend, imshape[1] )
645            ]
646
647    return bbox

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

def getBBoxFromPts(pts, extend, imshape, outdim=None, frame=None):
649def getBBoxFromPts(pts, extend, imshape, outdim=None, frame=None):
650    arr = np.array(pts)
651    ## get if points are 2D or 3D
652    ptsdim = arr.shape[1]
653    ## if not imposed, output the same dimension as input points
654    if outdim is None:
655        outdim = ptsdim
656    ## Get bounding box from points according to dimensions
657    if ptsdim == 2:
658        if outdim == 2:
659            bbox = [int(np.min(arr[:,0])), int(np.min(arr[:,1])), int(np.max(arr[:,0]))+1, int(np.max(arr[:,1]))+1]
660        else:
661            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]
662    if ptsdim == 3:
663        if outdim == 2:
664            bbox = [int(np.min(arr[:,1])), int(np.min(arr[:,2])), int(np.max(arr[:,1]))+1, int(np.max(arr[:,2]))+1]
665        else:
666            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]
667    if extend > 0:
668        for i in range(outdim):
669            if i < 2:
670                bbox[(outdim==3)+i] = max( bbox[(outdim==3)+i] - extend, 0)
671                bbox[(outdim==3)+i+outdim] = min(bbox[(outdim==3)+i+outdim] + extend, imshape[(outdim==3)+i] )
672    return bbox
def inside_bounds(pt, imshape):
674def inside_bounds( pt, imshape ):
675    """ Check if given point is inside image limits """
676    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):
678def extendBBox2D( bbox, extend_factor, imshape ):
679    """ Extend bounding box with given margin """
680    extend = max(bbox[2] - bbox[0], bbox[3] - bbox[1]) * extend_factor
681    bbox = np.array(bbox)
682    bbox[:2] = np.maximum(bbox[:2] - extend, 0)
683    bbox[2:] = np.minimum(bbox[2:] + extend, imshape[:2])
684    return bbox

Extend bounding box with given margin

def getBBox2D(img, label):
686def getBBox2D(img, label):
687    """ Get bounding box of label """
688    mask = (img==label)*1
689    props = regionprops(mask)
690    for prop in props:
691        bbox = prop.bbox
692        return bbox

Get bounding box of label

def getPropLabel(img, label):
694def getPropLabel(img, label):
695    """ Get the properties of label """
696    mask = np.uint8(img == label)
697    props = regionprops(mask)
698    return props[0]

Get the properties of label

def getBBoxLabel(img, label):
700def getBBoxLabel(img, label):
701    """ Get bounding box of label """
702    mask_ind = np.where(img==label)
703    if len(mask_ind) <= 0:
704        return None
705    dim = len(img.shape)
706    bbox = np.zeros(dim*2, int)
707    for i in range(dim):
708        bbox[i] = int(np.min(mask_ind[i]))
709        bbox[i+dim] = int(np.max(mask_ind[i]))+1
710    return bbox

Get bounding box of label

def getBBox2DMerge(img, label, second_label):
712def getBBox2DMerge(img, label, second_label): #, checkTouching=False):
713    """ Get bounding box of two labels and check if they are in contact """
714    mask = np.isin( img, [label, second_label] )
715    props = regionprops(mask*1)
716    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):
719def frame_to_skeleton(frame, connectivity=1):
720    """ convert labels frame to skeleton (thin boundaries) """
721    return skeletonize( find_boundaries(frame, connectivity=connectivity, mode="outer") )

convert labels frame to skeleton (thin boundaries)

def remove_boundaries(img):
723def remove_boundaries(img):
724    """ Put the boundaries pixels between labels as 0 """
725    bound = frame_to_skeleton( img, connectivity=1 )
726    img[bound>0] = 0
727    return img

Put the boundaries pixels between labels as 0

def ind_boundaries(img):
729def ind_boundaries(img):
730    """ Get indices of the boundaries pixels between two labels """
731    bound = frame_to_skeleton( img, connectivity=1 )
732    return np.argwhere(bound>0)

Get indices of the boundaries pixels between two labels

def checkTouchingLabels(img, label, second_label):
734def checkTouchingLabels(img, label, second_label):
735    """ Returns if labels are in contact (1-2 pixel away) """
736    disk_one = disk(radius=1)
737    maska = binary_dilation(img==label, footprint=disk_one)
738    maskb = binary_dilation(img==second_label, footprint=disk_one)
739    return np.any(maska & maskb)

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

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

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

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

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

def positionIn2DBBox(position, bbox):
749def positionIn2DBBox(position, bbox):
750    """ Returns the position shifted to its position inside the 2D bounding box """
751    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):
753def position2DIn2DBBox(position, bbox):
754    """ Returns the position shifted to its position inside the 2D bounding box """
755    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):
757def toFullImagePos(indices, bbox):
758    indices = np.array(indices)
759    return np.column_stack((indices[:, 0] + bbox[0], indices[:, 1] + bbox[1])).tolist()
def addFrameIndices(indices, frame):
761def addFrameIndices( indices, frame ):
762    return [ [frame, ind[0], ind[1]] for ind in indices ]
def shiftFrameIndices(indices, add_frame):
764def shiftFrameIndices( indices, add_frame ):
765    if isinstance( indices, list ):
766        indices = np.array(indices)
767    indices[:, 0] += add_frame
768    return indices.tolist()
def shiftFrames(indices, frames):
770def shiftFrames( indices, frames ):
771    if isinstance( indices, list ):
772        indices = np.array(indices)
773    indices[:, 0] = frames[indices[:, 0]]
774    return indices.tolist()
def toFullMoviePos(indices, bbox, frame=None):
776def toFullMoviePos( indices, bbox, frame=None ):
777    """ Replace indexes inside bounding box to full movie indexes """
778    indices = np.array(indices)
779    if frame is not None:
780        frame_arr = np.full(len(indices), frame)
781        return np.column_stack((frame_arr, indices[:, 0] + bbox[0], indices[:, 1] + bbox[1]))
782    if len(bbox) == 6:
783        return np.column_stack((indices[:, 0] + bbox[0], indices[:, 1] + bbox[1], indices[:, 2] + bbox[2]))
784    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):
786def cropBBox(img, bbox):
787    slices = tuple(slice(bbox[i], bbox[i + len(bbox) // 2]) for i in range(len(bbox) // 2))
788    return img[slices]
def crop_twoframes(img, bbox, frame):
790def crop_twoframes( img, bbox, frame ):
791    """ Crop bounding box with two frames """
792    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):
794def cropBBox2D(img, bbox):
795    return img[bbox[0]:bbox[2], bbox[1]:bbox[3]]
def setValueInBBox2D(img, setimg, bbox):
797def setValueInBBox2D(img, setimg, bbox):
798    bbimg = img[bbox[0]:bbox[2], bbox[1]:bbox[3]] 
799    bbimg[setimg>0]= setimg[setimg>0]
def addValueInBBox(img, addimg, bbox):
801def addValueInBBox(img, addimg, bbox):
802    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):
804def set_maxlabel(layer):
805    layer.mode = "PAINT"
806    layer.selected_label = np.max(layer.data)+1
807    layer.refresh()
def set_label(layer, lab):
809def set_label(layer, lab):
810    layer.mode = "PAINT"
811    layer.selected_label = lab
812    layer.refresh()
def get_free_labels(used, nlab):
814def get_free_labels( used, nlab ):
815    """ Get n-th unused label (not in used list) """
816    maxlab = max(used)+1
817    unused = list(set(range(1, maxlab)) - set(used))
818    if nlab < len(unused):
819        return unused[0:nlab]
820    else:
821        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):
823def get_next_label(layer, label):
824    """ Get the next unused label starting from label """
825    used = np.unique(layer.data)
826    i = label+1
827    while i < np.max(used):
828        if i>0 and (i not in used):
829            return i
830        i = i + 1
831    return i+1

Get the next unused label starting from label

def relabel_layer(layer):
833def relabel_layer(layer):
834    maxlab = np.max(layer.data)
835    used = np.unique(layer.data)
836    nlabs = len(used)
837    if nlabs == maxlab:
838        #print("already relabelled")
839        return
840    for j in range(1, nlabs+1):
841        if j not in used:
842            layer.data[layer.data==maxlab] = j
843            maxlab = np.max(layer.data)
844    show_info("Labels reordered")
845    layer.refresh()
def inv_visibility(viewer, layername):
847def inv_visibility(viewer, layername):
848    """ Switch the visibility of a layer """
849    if layername in viewer.layers:
850        layer = viewer.layers[layername]
851        layer.visible = not layer.visible

Switch the visibility of a layer

def average_area(seg):
854def average_area( seg ):
855    """ Average area of labels (cells) """
856    # Label the input image
857    labeled_array, num_features = ndlabel(seg)
858    
859    if num_features == 0:
860        return 0.0
861    
862    # Calculate the area of each label
863    areas = ndsum(seg > 0, labeled_array, index=np.arange(1, num_features + 1))
864    # Calculate the average area
865    avg_area = np.mean(areas)   
866    return avg_area

Average area of labels (cells)

def summary_labels(seg):
869def summary_labels( seg ):
870    """ Summary of labels (cells) measurements """
871    props = regionprops(seg)
872    avg_duration = 0
873    avg_area = 0.0
874    for prop in props:
875        bbox = prop.bbox
876        nz = 1
877        if len(bbox)>4:
878            nz = bbox[3]-bbox[0]
879        avg_duration += nz
880        avg_area += prop.area/nz
881    return len(props), avg_duration/len(props), avg_area/len(props) 

Summary of labels (cells) measurements

def labels_in_cell(sega, segb, label):
883def labels_in_cell( sega, segb, label ):
884    """ Look at the labels of segb inside label from sega """
885    cell = np.isin( sega, [label] )
886    labelb = segb[ cell ]
887    cell_area = np.sum( cell*1, axis=None )
888    filled_area = np.sum( labelb>0 )
889    nobj = len(np.unique( labelb ))
890    if 0 in labelb:
891        nobj = nobj - 1
892    return nobj, (filled_area/cell_area), np.unique(labelb)

Look at the labels of segb inside label from sega

def match_labels(sega, segb):
895def match_labels( sega, segb ):
896    """ Match the labels of the two segmentation images """
897    region_properties = ["label", "centroid"]
898
899    df0 = pd.DataFrame( regionprops_table( sega, properties=region_properties ) )
900    df0["frame"] = 0
901    df1 = pd.DataFrame( regionprops_table( segb, properties=region_properties ) )
902    df1["frame"] = 1
903    df = pd.concat([df0, df1])
904
905    ## Link the two frames with LapTrack tracking
906    laptrack = LaptrackCentroids(None, None)
907    laptrack.max_distance = 10 
908    laptrack.set_region_properties(with_extra=False)
909    laptrack.splitting_cost = False ## disable splitting option
910    laptrack.merging_cost = False ## disable merging option
911    labels = list(np.unique(segb))
912    if 0 in labels:
913        labels.remove(0)
914    parent_labels = laptrack.twoframes_track(df, labels)
915    return parent_labels, labels

Match the labels of the two segmentation images

def labels_table(labimg, intensity_image=None, properties=None, extra_properties=None):
917def labels_table( labimg, intensity_image=None, properties=None, extra_properties=None ):
918    """ Returns the regionprops_table of the labels """
919    if properties is None:
920        properties = ['label', 'centroid']
921    if intensity_image is not None:
922        return regionprops_table( labimg, intensity_image=intensity_image, properties=properties, extra_properties=extra_properties )
923    return regionprops_table( labimg, properties=properties, extra_properties=extra_properties )

Returns the regionprops_table of the labels

def labels_to_table(labimg, frame):
925def labels_to_table( labimg, frame ):
926    """ Get label and centroid """
927    labels = np.unique(labimg.ravel())
928    labels = labels[labels != 0]
929    centroids = center_of_mass(labimg, labels=labimg, index=labels)
930    table = np.column_stack((labels, np.full(len(labels), frame), centroids))
931    return table.astype(int)

Get label and centroid

def labels_to_table_v1(labimg, frame):
933def labels_to_table_v1( labimg, frame ):
934    """ Get label and centroid """
935    props = regionprops( labimg )
936    n = len(props)
937    if n == 0:
938        return np.empty( (0, 2+labimg.ndim) )
939    res = np.zeros( (n, 2+labimg.ndim), dtype=int )
940    for i, prop in enumerate(props):
941        res[i, 0] = prop.label
942        res[i, 1] = frame
943        res[i,:2] = np.array(prop.centroid).astype(int)
944    return res

Get label and centroid

def non_unique_labels(labimg):
946def non_unique_labels( labimg ):
947    """ Check if contains only unique labels """
948    relab, nlabels = ndlabel( labimg )
949    return nlabels > (len( np.unique(labimg) )-1)

Check if contains only unique labels

def reset_labels(labimg, closing=True):
951def reset_labels( labimg, closing=True ):
952    """ Relabel in 3D all labels (unique labels) """
953    s = ndi_structure(3,1)
954    ## ignore 3D connectivity (unique labels in all frames)
955    s[0,:,:] = 0
956    s[2,:,:] = 0
957    if closing:
958        labimg = ndbinary_opening( labimg, iterations=1, structure=s )
959    lab = ndlabel( labimg, structure=s )[0]
960    return lab

Relabel in 3D all labels (unique labels)

def skeleton_to_label(skel, labelled):
963def skeleton_to_label( skel, labelled ):
964    """ Transform a skeleton to label image with numbers from labelled image """
965    labels = ndlabel( np.invert(skel) )[0]
966    new_labels = find_objects( labels )
967    newlab = np.zeros( skel.shape, np.uint32 )   
968    for i, obj_slice in enumerate(new_labels):
969        if (obj_slice is not None):
970            if ((obj_slice[1].stop-obj_slice[1].start) <= 2) and ((obj_slice[0].stop-obj_slice[0].start) <= 2):
971                continue
972            label_mask = labels[obj_slice] == (i+1)
973            label_values = labelled[obj_slice][label_mask]
974            labvals, counts = np.unique(label_values, return_counts=True )
975            labval = labvals[ np.argmax(counts) ]
976            newlab[obj_slice][label_mask] = labval
977    return newlab

Transform a skeleton to label image with numbers from labelled image

def get_most_frequent(labimg, img, label):
979def get_most_frequent( labimg, img, label ):
980    """ Returns which label is the most frequent in mask """
981    mask = labimg == label
982    vals, counts = np.unique( img[mask], return_counts=True )
983    return vals[ np.argmax(counts) ]

Returns which label is the most frequent in mask

def binary_properties(labimg):
985def binary_properties( labimg ):
986    """ Returns basic label properties """
987    return regionprops( label(labimg) )

Returns basic label properties

def labels_properties(labimg):
989def labels_properties( labimg ):
990    """ Returns basic label properties """
991    return regionprops( labimg )

Returns basic label properties

def labels_bbox(labimg):
993def labels_bbox( labimg ):
994    """ Returns for each label its bounding box """
995    return regionprops_table( labimg, properties=('label', 'bbox') )

Returns for each label its bounding box

def tuple_int(pos):
 997def tuple_int(pos):
 998    if len(pos) == 3:
 999        return ( (int(pos[0]), int(pos[1]), int(pos[2])) )
1000    if len(pos) == 2:
1001        return ( (int(pos[0]), int(pos[1])) )
def get_consecutives(ordered):
1003def get_consecutives( ordered ):
1004    """ Returns the list of consecutives integers (already sorted) """
1005    gaps = [ [start, end] for start, end in zip( ordered, ordered[1:] ) if start+1 < end ]
1006    edges = iter( ordered[:1] + sum(gaps, []) + ordered[-1:] )
1007    return list( zip(edges, edges) )

Returns the list of consecutives integers (already sorted)

def prop_to_pos(prop, frame):
1010def prop_to_pos(prop, frame):
1011    return np.array( (frame, int(prop.centroid[0]), int(prop.centroid[1])) )
def current_frame(viewer):
1013def current_frame(viewer):
1014    return int(viewer.cursor.position[0])
def distance(x, y):
1016def distance( x, y ):
1017    """ 2d distance """
1018    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):
1020def interm_position( prop, a, b ):
1021    res = [0,0]
1022    res[0] = a[0] + prop*(b[0]-a[0])
1023    res[1] = a[1] + prop*(b[1]-a[1])
1024    return res
def nb_frames(seg, lab):
1026def nb_frames( seg, lab ):
1027    """ Return nb frames with label lab """
1028    labseg = seg==lab
1029    return np.sum( np.any(labseg, axis=(1,2)) )

Return nb frames with label lab

def keep_orphans(img, comp_img, klabels):
1031def keep_orphans( img, comp_img, klabels ):
1032    """ Keep only labels that doesn't have a follower """
1033    valid_labels = np.setdiff1d(img[0], klabels)
1034    if (len(valid_labels)==1) and (valid_labels[0]==0):
1035        return
1036    labels = [val for val in valid_labels if (val!=0) and np.any(comp_img==val)]
1037    mask = np.isin(img, labels)
1038    img[mask] = 0

Keep only labels that doesn't have a follower

def keep_orphans_3d(img, klabels):
1040def keep_orphans_3d( img, klabels ):
1041    """ Keep only orphans labels or lab and olab """
1042    for label in np.unique(img[1]):
1043        if label not in klabels:
1044            if nb_frames( img, label ) == 2:
1045                img[img==label] = 0
1046    return img

Keep only orphans labels or lab and olab

def mean_nonzero(array):
1048def mean_nonzero( array ):
1049    nonzero = np.count_nonzero(array)
1050    if nonzero > 0:
1051        return np.sum(array)/nonzero
1052    return 0
def get_contours(binimg):
1054def get_contours( binimg ):
1055    """ Return the contour of a binary shape """
1056    return find_contours( binimg )

Return the contour of a binary shape

def touching_labels(img, expand=3):
1059def touching_labels( img, expand=3 ):
1060    """ Extends the labels to make them touch """
1061    return expand_labels( img, distance=expand )

Extends the labels to make them touch

def connectivity_graph(img, distance):
1063def connectivity_graph( img, distance ):
1064    """ Returns the region adjancy graph of labels """
1065    touchlab = touching_labels( img, expand=distance )
1066    return RAG( touchlab, connectivity=2 )

Returns the region adjancy graph of labels

def get_neighbor_graph(img, distance):
1068def get_neighbor_graph( img, distance ):
1069    """ Returns the adjancy graph without bg, so only neigbor cells """
1070    graph = connectivity_graph( img, distance=distance ) # be sure that labels touch and get the graph
1071    graph.remove_node(0) if 0 in graph.nodes else None
1072    return graph

Returns the adjancy graph without bg, so only neigbor cells

def get_neighbors(label, graph):
1074def get_neighbors( label, graph ):
1075    """ Get the list of neighbors of cell 'label' from the graph """
1076    if label in graph.nodes:
1077        return list(graph.adj[label])
1078    return []

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

def get_boundary_cells(img):
1080def get_boundary_cells( img ):
1081    """ Return cells on tissu boundary in current image """ 
1082    dilated = binary_dilation( img > 0, disk(3) )
1083    zero = np.invert( dilated )
1084    zero = binary_dilation( zero, disk(5) )
1085    touching = np.unique( img[ zero ] ).tolist()
1086    if 0 in touching:
1087        touching.remove(0)
1088    return touching

Return cells on tissu boundary in current image

def get_border_cells(img):
1090def get_border_cells( img ):
1091    """ Return cells on border in current image """ 
1092    height = img.shape[1]
1093    width = img.shape[0]
1094    labels = list( np.unique( img[ :, 0:2 ] ) )   ## top border
1095    labels += list( np.unique( img[ :, (height-2): ] ) )   ## bottom border
1096    labels += list( np.unique( img[ 0:2,] ) )   ## left border
1097    labels += list( np.unique( img[ (width-2):,] ) )   ## right border
1098    labels = list( np.unique(labels) )
1099    return labels

Return cells on border in current image

def count_neighbors(label_img, label):
1101def count_neighbors( label_img, label ):
1102    """ Get the number of neighboring labels of given label """
1103    ## much slower than using the RAG graph
1104    # Dilate the labeled image
1105    dilated_mask = binary_dilation( label_img==label, disk(1) )
1106    nonzero = np.nonzero( dilated_mask)
1107        
1108    # Find the unique labels in the dilated region, excluding the current label and background
1109    neighboring_labels = np.unique( label_img[nonzero] ).tolist()
1110        
1111    # Add the number of unique neighboring labels
1112    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):
1114def get_cell_radius( label, labimg ):
1115    """ Get the radius of the cell label in labimg (2D) """
1116    area = np.sum( labimg == label )
1117    return math.sqrt( area / math.pi )

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

def consecutive_distances(pts_pos):
1122def consecutive_distances( pts_pos ):
1123    """ Distance travelled by the cell between each frame """
1124    diff = np.diff( pts_pos, axis=0 )
1125    disp = np.linalg.norm(diff, axis=1)
1126    return disp

Distance travelled by the cell between each frame

def velocities(pts_pos):
1128def velocities( pts_pos ):
1129    """ Velocity of the cell between each frame (average between previous and next) """
1130    diff = np.diff( pts_pos, axis=0 ).astype(float)
1131    diff = np.vstack( (diff[0], diff) )
1132    diff = np.vstack( (diff, diff[-1]) )
1133    kernel=np.array([0.5,0.5])
1134    adiff = np.zeros( (len(diff)+1, 3) )
1135    for i in range(3):
1136        adiff[:,i] = np.convolve( diff[:,i], kernel )
1137    adiff = adiff[1:-1]
1138    disp = np.linalg.norm(adiff[:,1:3], axis=1)
1139    dt = adiff[:,0] 
1140    return disp/dt

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

def total_distance(pts_pos):
1142def total_distance( pts_pos ):
1143    """ Total distance travelled by point with coordinates xpos and ypos """
1144    diff = np.diff( pts_pos, axis=0 )
1145    disp = np.linalg.norm(diff, axis=1)
1146    return np.sum(disp)

Total distance travelled by point with coordinates xpos and ypos

def net_distance(pts_pos):
1148def net_distance( pts_pos ):
1149    """ Net distance travelled by point with coordinates xpos and ypos """
1150    disp = pts_pos[len(pts_pos)-1] - pts_pos[0]
1151    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():
1155def start_time():
1156    return time.time()
def show_duration(start_time, header=None):
1158def show_duration(start_time, header=None):
1159    if header is None:
1160        header = "Processed in "
1161    #show_info(header+"{:.3f}".format((time.time()-start_time)/60)+" min")
1162    print(header+"{:.3f}".format((time.time()-start_time)/60)+" min")
def shortcut_click_match(shortcut, event):
1166def shortcut_click_match( shortcut, event ):
1167    """ Test if the click event corresponds to the shortcut """
1168    button = 1
1169    if shortcut["button"] == "Right":
1170        button = 2
1171    if event.button != button:
1172        return False
1173    if "modifiers" in shortcut.keys():
1174        return set(list(event.modifiers)) == set(shortcut["modifiers"])
1175    else:
1176        if len(event.modifiers) > 0:
1177            return False
1178        return True

Test if the click event corresponds to the shortcut

def is_windows():
1180def is_windows():
1181    """ Is running on windows or not """
1182    try:
1183        return platform.lower().startswith("win")
1184    except:
1185        return False

Is running on windows or not

def is_darwin():
1187def is_darwin():
1188    """ Test if OS is MacOS or not """
1189    try:
1190        return platform.lower() == "darwin"
1191    except:
1192        return False

Test if OS is MacOS or not