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