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
Display info in napari
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)
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)
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)
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
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)
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
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
97def close_progress( viewer, progress_bar ): 98 """ Close the progress bar """ 99 progress_bar.close() 100 show_progress( viewer, False)
Close the progress bar
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
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
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
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
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
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
156def found_segfile( filepath ): 157 """ Check if the segmentation file exists """ 158 return os.path.exists( filepath )
Check if the segmentation file exists
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
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
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
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
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
204def getOverlayText(viewer): 205 """ Returns the current overlay text """ 206 return viewer.text_overlay.text
Returns the current overlay text
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
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
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
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
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)
253def set_frame(viewer, frame, scale=1): 254 """ Set current frame """ 255 viewer.dims.set_point(0, frame*scale)
Set current frame
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
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
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
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
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
293def remove_all_widgets( viewer ): 294 """ Remove all widgets """ 295 viewer.window.remove_dock_widget("all")
Remove all widgets
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
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
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
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
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
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
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
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
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
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
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
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
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
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
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)
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
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
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
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
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
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
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)
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
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()
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
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)
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
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
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
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
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
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
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
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)
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
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
985def binary_properties( labimg ): 986 """ Returns basic label properties """ 987 return regionprops( label(labimg) )
Returns basic label properties
989def labels_properties( labimg ): 990 """ Returns basic label properties """ 991 return regionprops( labimg )
Returns basic label properties
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
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)
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
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
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
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
1054def get_contours( binimg ): 1055 """ Return the contour of a binary shape """ 1056 return find_contours( binimg )
Return the contour of a binary shape
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
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
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
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
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
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
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
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)
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
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)
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
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
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
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
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
1194def print_shortcuts( shortcut_group ): 1195 """ Put to text the subset of shortcuts """ 1196 text = "" 1197 for short_name, vals in shortcut_group.items(): 1198 if vals["type"] == "key": 1199 text += " <"+vals["key"]+"> "+vals["text"]+"\n" 1200 if vals["type"] == "click": 1201 modif = "" 1202 if "modifiers" in vals.keys(): 1203 modifiers = vals["modifiers"] 1204 for mod in modifiers: 1205 if mod == "Control": 1206 if is_darwin(): 1207 modif += "Command"+"-" 1208 else: 1209 modif += mod+"-" 1210 else: 1211 if mod == "Alt": 1212 if is_darwin(): 1213 modif += "Option"+"-" 1214 else: 1215 modif += mod+"-" 1216 else: 1217 modif += mod+"-" 1218 text += " <"+modif+vals["button"]+"-click> "+vals["text"]+"\n" 1219 return text
Put to text the subset of shortcuts