Esempio n. 1
0
def optic_disk_detect_3(img):
    '''
    Method that seems to work well with DIARETDB1 database.
    '''

    hsi = rgb_to_hsi(img)
    intensity = hsi[:, :, 2].copy()
    #plt.axis('off'); show_image(intensity)
    #i_sp = add_salt_and_pepper(intensity, 0.005)
    i_med = mh.median_filter(intensity)    # use Wiener filter instead?
    i_clahe = skimage.exposure.equalize_adapthist(i_med)
    #plt.axis('off'); show_image(i_clahe)
    seeds = (i_clahe > 0.85)
    seeds = skimage.morphology.remove_small_objects(seeds, min_size=300, connectivity=2)
    #plt.axis('off'); show_image(seeds)
    optic_disk_map = region_growing_1(i_clahe, radius=3, tol1=0.1, tol2=0.2, tol3=0.2, seeds=seeds)
    optic_disk_map += 1
    #plt.axis('off'); show_image(optic_disk_map)

    _cl_areas = cl_areas(optic_disk_map)
    print _cl_areas
    optic_disk_map = leave_segments_by_mask(optic_disk_map,
                                            (8000 < _cl_areas) & (_cl_areas < 30000))

    #optic_disk_map = skimage.morphology.remove_small_objects(optic_disk_map, min_size=500, connectivity=2)
    optic_disk_map = mh.close_holes(mh.close(optic_disk_map, Bc=np.ones((10, 10))))
    #optic_disk_map = skimage.morphology.remove_small_objects(optic_disk_map, min_size=5000, connectivity=2)
    if np.all(optic_disk_map == 0):
        print 'Disk not found'
    return optic_disk_map
Esempio n. 2
0
    def predict(self, filenames_list, scale=0.4, n_clusters=4):
        if not isinstance(filenames_list, list):
            raise Exception('Input list of files is not a list actually')
        rectangles = []
        for filename in filenames_list:
            img_orig = gaussian(rgb2lab(imread(filename))[:, :, 1], 3)
            h_orig, w_orig = img_orig.shape
            h_small, w_small = int(h_orig * scale), int(w_orig * scale)
            thumb = resize(img_orig, (h_small, w_small))

            model = KMeans(n_clusters)
            segments_flatten = model.fit_predict(thumb.reshape(-1, 1))
            segments = segments_flatten.reshape(h_small, w_small)
            seg_means = [
                np.mean(thumb[segments == n])
                for n in range(np.max(segments) + 1)
            ]
            brightest_seg = np.argmax(seg_means)
            mask = segments == brightest_seg
            thumb_closed = mahotas.close(
                mask, np.ones((int(30 * scale), int(30 * scale))))

            label_objects, nb_labels = ndi.label(thumb_closed)
            sizes = np.bincount(label_objects.ravel())
            mask_sizes = sizes == np.sort(sizes)[-2]
            img_cleaned = mask_sizes[label_objects]

            img_final = resize(img_cleaned, (h_orig, w_orig)).astype(int)
            rectangles.append(blob.bound_rect(img_final))
        return rectangles
def segment_layer(filename, params):
	'''
	Segment one layer in a stack
	'''
	#extract pixel size in xy and z
	xsize, zsize = extract_zoom(params.folder)

	#load image
	img = tifffile.imread(params.inputfolder + params.folder + filename)

	#normalize image
	img = ndimage.median_filter(img, 3)
	per_low = np.percentile(img, 5)
	img[img < per_low] = per_low
	img = img - img.min()

	per_high = np.percentile(img, 99)
	img[img > per_high] = per_high
	img = img*255./img.max()


	imgf = ndimage.gaussian_filter(img*1., 30./xsize).astype(np.uint8)

	kmask = (imgf > mahotas.otsu(imgf.astype(np.uint8)))*255.

	sizefactor = 10
	small = ndimage.interpolation.zoom(kmask, 1./sizefactor)	#scale the image to a smaller size

	rad = int(300./xsize)

	small_ext = np.zeros([small.shape[0] + 4*rad, small.shape[1] + 4*rad])
	small_ext[2*rad : 2*rad + small.shape[0], 2*rad : 2*rad + small.shape[1]] = small

	small_ext = mahotas.close(small_ext.astype(np.uint8), mahotas.disk(rad))
	small = small_ext[2*rad : 2*rad + small.shape[0], 2*rad : 2*rad + small.shape[1]]
	small = mahotas.close_holes(small)*1.			
	small = small*255./small.max()

	kmask = ndimage.interpolation.zoom(small, sizefactor)	#scale back to normal size
	kmask = normalize(kmask)
	kmask = (kmask > mahotas.otsu(kmask.astype(np.uint8)))*255.	#remove artifacts of interpolation

	if np.median(imgf[np.where(kmask > 0)]) < (np.median(imgf[np.where(kmask == 0)]) + 1)*3:
		kmask = np.zeros_like(kmask)


	#save indices of the kidney mask
#	ind = np.where(kmask > 0)
#	ind = np.array(ind)
#	np.save(params.inputfolder + '../segmented/masks/' + params.folder + filename[:-4] + '.npy', ind)

	#save outlines
	im = np.zeros([img.shape[0], img.shape[1], 3])
	img = tifffile.imread(params.inputfolder + params.folder + filename)
	im[:,:,0] = im[:,:,1] = im[:,:,2] = np.array(img)
	output = overlay(kmask, im, (255,0,0), borders = True)
	tifffile.imsave(params.inputfolder + '../segmented/outlines/' + params.folder + filename[:-4] + '.tif', (output).astype(np.uint8))
Esempio n. 4
0
def optic_disk_detect_2(img):
    hsi = rgb_to_hsi(img)
    intensity = hsi[:, :, 2].copy()
    i_sp = add_salt_and_pepper(intensity, 0.005)
    i_med = mh.median_filter(i_sp)
    i_clahe = skimage.exposure.equalize_adapthist(i_med)
    optic_disk_map = (i_clahe > 0.6) & (hsi[:, :, 1] < 0.3)
    #show_image(optic_disk_map)
    optic_disk_map = skimage.morphology.remove_small_objects(optic_disk_map, min_size=500, connectivity=2)
    optic_disk_map = mh.close_holes(mh.close(optic_disk_map, Bc=np.ones((30, 30))))
    optic_disk_map = skimage.morphology.remove_small_objects(optic_disk_map, min_size=10000, connectivity=2)
    if np.all(optic_disk_map == 0):
        print 'Disk not found'
    return optic_disk_map
Esempio n. 5
0
 def predict(self, filenames_list, threshold=0.75):        
     if not isinstance(filenames_list, list):
         raise Exception('Input list of files is not a list actually')
     
     rectangles = []
     for filename in filenames_list:
         img = imread(filename)
         img_a = color.scaler(gaussian(rgb2lab(img)[:,:,1], 3))
         h,w = img_a.shape
         mask = img_a > threshold
         img_closed = mahotas.close(mask, disk(h/15))        
         
         label_objects, nb_labels = ndi.label(img_closed)
         sizes = np.bincount(label_objects.ravel())
         mask_sizes = sizes == np.sort(sizes)[-2]
         img_cleaned = mask_sizes[label_objects]
         rectangles.append(blob.bound_rect(img_cleaned))
     return rectangles
Esempio n. 6
0
if len(img.shape) == 3: #color image
    #convert to grayscale
    img = mh.colors.rgb2gray(img, dtype=np.uint8)

# thresholding
T_otsu = mh.otsu(img) # finds a numeric threshold
img = (img > T_otsu)  # make image binary

# invert the image (just because the test image is stored the other way)
img = ~img 

# close single-pixel holes. Also makes the skeletonization much more well-behaved,
# with less tiny branches close to terminals.
#
# This can create loops if two branches are separated by < 3 px of background
img = mh.close(img)

print('Thinning...')
# skeletonization from scikit image.
# Zhang-Suen algorithm (apparently with staircase removal)
skel = morphology.skeletonize(img)

# Try mahotas skeletonization. Makes many spikes.
# works better after mh.close(), but still splits many tips in two.
# Also gives staircases in the skeleton.
#skel = mh.thin(img)  
#skel = morphology.skeletonize(skel)  # one pass of the other skeletonization to remove staircases


################
Esempio n. 7
0
import numpy as np
import mahotas as mh
image = mh.imread('../1400OS_10_01.jpeg')
image = mh.colors.rgb2gray(image, dtype=np.uint8)
thresh = mh.thresholding.otsu(image)
print(thresh)
otsubin = (image > thresh)
mh.imsave('otsu-threshold.jpeg', otsubin.astype(np.uint8) * 255)
otsubin = ~mh.close(~otsubin, np.ones((15, 15)))
mh.imsave('otsu-closed.jpeg', otsubin.astype(np.uint8) * 255)

thresh = mh.thresholding.rc(image)
print(thresh)
mh.imsave('rc-threshold.jpeg', (image > thresh).astype(np.uint8) * 255)
Esempio n. 8
0
if len(img.shape) == 3:  #color image
    #convert to grayscale
    img = mh.colors.rgb2gray(img, dtype=np.uint8)

# thresholding
T_otsu = mh.otsu(img)  # finds a numeric threshold
img = (img > T_otsu)  # make image binary

# invert the image (just because the test image is stored the other way)
img = ~img

# close single-pixel holes. Also makes the skeletonization much more well-behaved,
# with less tiny branches close to terminals.
#
# This can create loops if two branches are separated by < 3 px of background
img = mh.close(img)

print('Thinning...')
# skeletonization from scikit image.
# Zhang-Suen algorithm (apparently with staircase removal)
skel = morphology.skeletonize(img)

# Try mahotas skeletonization. Makes many spikes.
# works better after mh.close(), but still splits many tips in two.
# Also gives staircases in the skeleton.
#skel = mh.thin(img)
#skel = morphology.skeletonize(skel)  # one pass of the other skeletonization to remove staircases

################

# find terminals and junctions. t and j are in the format [(x1, y1), (x2, y2), ...]
# This code is supporting material for the book
# Building Machine Learning Systems with Python
# by Willi Richert and Luis Pedro Coelho
# published by PACKT Publishing
#
# It is made available under the MIT License

import numpy as np
import mahotas as mh
image = mh.imread('../SimpleImageDataset/building05.jpg')
image = mh.colors.rgb2gray(image, dtype=np.uint8)
thresh = mh.thresholding.otsu(image)
print(thresh)
otsubin = (image > thresh)
mh.imsave('otsu-threshold.jpeg', otsubin.astype(np.uint8) * 255)
otsubin = ~ mh.close(~otsubin, np.ones((15, 15)))
mh.imsave('otsu-closed.jpeg', otsubin.astype(np.uint8) * 255)

thresh = mh.thresholding.rc(image)
print(thresh)
mh.imsave('rc-threshold.jpeg', (image > thresh).astype(np.uint8) * 255)
Esempio n. 10
0
def findClosestEdge(G, p):
    emin = None
    dmin = 1e100
    for e in G.edges():
        d = distPointLine(e[0], e[1], p)
        if d < dmin:
            dmin = d
            emin = e
    return emin
    #@profile
    #def my_func():
    ###################################################################################
    # Main code starts here
    #

    # take the file name from the command line, if given
    """    
    if len(sys.argv) > 1:
        filename = sys.argv[1]
    else:
        filename = 'img/f.png'
    """
    filename = '1-1.png'

    # =============================================================================
    #     # read database of leaves
    #     leaves = {}
    #
    #     fileHandle = None
    #     try:
    #         leavesFile = getScriptPath() + '/leaves.json'
    #         print('Database file: ' + leavesFile)
    #         fileHandle = open(leavesFile, 'r')
    #     except:
    #         print('Could not load the leaf data base')
    #
    #     if fileHandle != None:
    #         # if the file exists, but the JSON is not correct, this will fail
    #         # This is on purpose, since if we continue and save the database at the end,
    #         # the data in it will be overwritten
    #         leaves = json.load(fileHandle)
    #
    #
    #     path_basename = os.path.splitext(filename)[0]
    #     print('path_basename', path_basename)
    #     basename = os.path.splitext(os.path.basename(filename))[0]
    #
    #     ## Read from .json
    #     leafData = {}
    #     try:
    #         #find the current leaf, based on file name
    #         leafData = leaves[basename]
    #     except:
    #         print('%s was not found in the leaf data base'%basename)
    #         # add an entry for this leaf to the ditionary
    #         leaves[basename] = leafData
    #
    #     print('Data for this leaf: ' + str(leafData))
    #
    #     px_mm = None
    #     root = None
    #
    #     try:
    #         px_mm = leafData['px_mm']
    #     except:
    #         print('No resolution found for leaf %s'%basename)
    #
    #     try:
    #         root = tuple(leafData['root'])
    #     except:
    #         print('No root found for leaf %s'%basename)
    #
    #     print('Resolution: ' + str(px_mm) + ' px/mm')
    #     print('Root:       ' + str(root))
    #     print()
    #
    #     print('Reading image %s'%filename)
    # =============================================================================
    root = None
    img = mh.imread(filename)
    # color image handling - works if the background is brighter than the object
    if len(img.shape) == 3:  #color image
        #convert to grayscale
        img = mh.colors.rgb2gray(img, dtype=np.uint8)

    # thresholding
    T_otsu = mh.otsu(img)  # finds a numeric threshold
    img = (img > T_otsu)  # make image binary

    # invert the image (just because the test image is stored the other way)
    img = ~img

    # close single-pixel holes. Also makes the skeletonization much more well-behaved,
    # with less tiny branches close to terminals.
    #
    # This can create loops if two branches are separated by < 3 px of background
    img = mh.close(img)

    #    print('Thinning...')
    # skeletonization from scikit image.
    # Zhang-Suen algorithm (apparently with staircase removal)
    skel = morphology.skeletonize(img)

    # Try mahotas skeletonization. Makes many spikes.
    # works better after mh.close(), but still splits many tips in two.
    # Also gives staircases in the skeleton.
    #skel = mh.thin(img)
    #skel = morphology.skeletonize(skel)  # one pass of the other skeletonization to remove staircases

    ################

    # find terminals and junctions. t and j are in the format [(x1, y1), (x2, y2), ...]
    #    print('Features...')
    t, j = terminals(skel)

    if root == None:
        # unpack the tuples to separate lists of x:s and y:s, for plotting and root selection
        #tx = [x[0] for x in t]
        #ty = [x[1] for x in t]

        dist_terminal = []
        for x in t:
            _dist = distPointLine([0, 316], [img.shape[1], 316], list(x))
            dist_terminal.append(_dist)
        # find the index of the lowest terminal. Use that as the root, for now.
        iroot = dist_terminal.index(min(dist_terminal))

        # find the lowest node, to use as root
        root = t[iroot]
    else:
        if root not in t + j:
            newroot = findClosest(t + j, root)
            print('Moving the root to a node in the tree. New root is',
                  str(newroot), 'old root was', str(root), 'distance',
                  dist(newroot, root))
            root = newroot

    #    print('Plotting')
    # make the skeleton image's background transparent
    skel = np.ma.masked_where(skel == 0, skel)
    #visited = np.ma.masked_where (visited==0, visited)

    #    fig = plt.figure()

    #     Plot images. Inversion here is just for nicer coloring
    #     interpolation=nearest is to turn smoothing off.

    width = skel.shape[1]
    height = skel.shape[0]

    #    plt.axis((0,width,height,0))

    #    plt.imshow(~img, cmap=leaf_colors, interpolation="nearest")
    #plt.imshow(np.sqrt(dmap), cmap=mpl.cm.jet_r, interpolation="nearest")

    #    plt.imshow(skel, cmap=skel_colors,  interpolation="nearest")
    #plt.imshow(visited, cmap=mpl.cm.cool,  interpolation="nearest")

    # update measures that depend on graph structure or root placement
    def updateMeasures(G, root):
        #print('  Strahler order...')
        StrahlerOrder(G, root)
        #print('  apical distances...')
        measureApicalDist(G)
        #print('  branch angles...')
        measureAngles(G)

    def buildGraph(img, skel):
        #print('Building tree...')
        G = nx.Graph()
        visited = np.zeros_like(
            skel)  # a new array of zeros, same dimensions as skel

        #print('  distance transform...')
        dmap = mh.distance(img)

        #print('  constructing tree...')
        buildTree(skel, visited, dmap, root, j, t, G)

        # measure node diameters
        measureDia(G, dmap)

        # automatically remove bad nodes and branches, e.g. too small ones
        removed = cleanup(G, root)
        # show (automatically) removed nodes
        for x, y in removed:
            plt.gca().add_patch(plt.Circle((x, y), radius=4, alpha=.4))

        updateMeasures(G, root)

        #print('Done.')
        return G

    # read in the graph from a previous run, if it exists
    path_basename = os.path.splitext(filename)[0]
    try:
        G = nx.read_gpickle(path_basename + '_graph.pkl')
        print('Loaded graph from ' + path_basename + '_graph.pkl')
    except:
        # could not read the graph. Constructing it now
        G = buildGraph(img, skel)

    # handles to plot elements
    nodes = None
    edges = None
    node_labels = None
    edge_labels = None

    rad = []

    #    plot_graph(G)

    # semi-transparent circles on the nodes
    #    for p,r in zip(G.nodes(), rad):
    #        plt.gca().add_patch(plt.Circle(p, radius=r, alpha=terminal_disk_alpha, color=terminal_disk_color))

    #    root_patch = plt.Circle(root, radius=40, alpha=root_disk_alpha, color=root_disk_color)
    #    plt.gca().add_patch(root_patch)

    # =============================================================================
    #     def setRoot(root):
    #         root_patch.center = root;
    #
    #         # save the new root in database
    #         # convert to int from numpy type, for JSON to work later
    #         leafData['root'] = (int(root[0]), int(root[1]))
    #
    #
    #     # a function called when the user clicks a node
    # =============================================================================
    def onpick(event):
        global root
        # make the clicked node the new root

        # for some reason it's difficult to get the coordinates of the clicked node
        # so we use mouse coordinates and search for the closest node.
        root = findClosest(G.nodes(),
                           (event.mouseevent.xdata, event.mouseevent.ydata))
        print('New root: ' + str(root))
        #setRoot(root)
        updateMeasures(G, root)
        ##report(G)

#        plot_graph(G)
#        fig.canvas.draw()

    undo_stack = []

    # a function called on keypress events
    def keypress(event):
        global nodes, edges, node_labels, G, root
        #print('press', event.key)
        sys.stdout.flush()
        if event.key == 'd':  # delete closest node
            p = findClosest(G.nodes(), (event.xdata, event.ydata))
            print('closest node is (%5.1f, %5.1f)' % p)
            if p == root:
                print('Cannot remove the root.')
                return
            undo_stack.append((G.copy(), root))
            G.remove_node(p)

            updateMeasures(G, root)
#            plot_graph(G)
#            fig.canvas.draw()

        if event.key == 'x':  # delete closest branch
            e = findClosestEdge(G, (event.xdata, event.ydata))
            undo_stack.append((G.copy(), root))
            G.remove_edge(*e)

            updateMeasures(G, root)
#            plot_graph(G)
#            fig.canvas.draw()

        if event.key == 'u':  # undo
            if len(undo_stack) > 0:
                print('Undo')
                G, root = undo_stack.pop()
                #setRoot(root)
                updateMeasures(G, root)
#                plot_graph(G)
#                fig.canvas.draw()
            else:
                print('No further undo')

        if event.key == 'r':  #re-build the graph from skeleton
            print('Rebuilding tree')
            undo_stack.append((G.copy(), root))
            G = buildGraph(img, skel)
#            plot_graph(G)
#            fig.canvas.draw()

# register the event callback functions
#    fig.canvas.mpl_connect('pick_event', onpick)
#    fig.canvas.mpl_connect('key_press_event', keypress)

# plot disks for the apical distance, just for testing
#for n in G.nodes():
#   if G.node[n]['level'] == 1:
#       r = G.node[n]['apicaldist']
#       plt.gca().add_patch(plt.Circle(n, radius=r, alpha=.4))
#       print(G.node[n])

#    report(G)
#    plt.tight_layout()

# show the plot - program pauses here for as long as the window is open
#    plt.show()
#
#    saveTreeText(G, path_basename+'_branches.txt', path_basename+'_nodes.txt');
#    nx.write_gpickle(G, path_basename+'_graph.pkl')

    def saveReport(reportFile, report_text, header_text):
        # if the file does not exist already, write the header
        present = os.path.isfile(reportFile)

        print('Saving leaf report in file ' + reportFile)
        try:
            f = open(reportFile, 'at')
            if not present:
                f.write(header_text + '\n')
            f.write(report_text + '\n')
        except:
            print('Error when saving the report')
Esempio n. 11
0
def main(image, mask, threshold=150, bead_size=2, superpixel_size=4,
         close_surface=False, close_disc_size=8, plot=False):
    '''Converts an image stack with labelled cell surface to a cell
    `volume` image

    Parameters
    ----------
    image: numpy.ndarray[Union[numpy.uint8, numpy.uint16]]
        grayscale image in which beads should be detected (3D)
    mask: numpy.ndarray[Union[numpy.int32, numpy.bool]]
        binary or labeled image of cell segmentation (2D)
    threshold: int, optional
        intensity of bead (default: ``150``)
    bead_size: int, optional
        minimal size of bead (default: ``2``)
    superpixel_size: int, optional
        size of superpixels for searching the 3D position of a bead
    close_surface: bool, optional
        whether the interpolated surface should be morphologically closed
    close_disc_size: int, optional
        size in pixels of the disc used to morphologically close the
        interpolated surface
    plot: bool, optional
        whether a plot should be generated (default: ``False``)

    Returns
    -------
    jtmodules.generate_volume_image.Output
    '''

    n_slices = image.shape[-1]
    logger.debug('input image has size %d in last dimension', n_slices)

    logger.debug('mask beads inside cell')
    beads_outside_cell = np.copy(image)
    for iz in range(n_slices):
        beads_outside_cell[mask > 0, iz] = 0

    logger.debug('search for 3D position of beads outside cell')
    slide = np.argmax(beads_outside_cell, axis=2)
    slide[slide > np.percentile(slide[mask == 0], 20)] = 0

    logger.debug('determine surface of slide')
    slide_coordinates = array_to_coordinate_list(slide)
    bottom_surface = fit_plane(subsample_coordinate_list(
        slide_coordinates, 2000)
    )

    logger.debug('detect_beads in 2D')
    mip = np.max(image, axis=-1)
    try:
        # TODO: use LOG filter???
        beads, beads_centroids = detect_blobs(
            image=mip, mask=np.invert(mask > 0), threshold=threshold,
            min_area=bead_size
        )
    except:
        logger.warn('detect_blobs failed, returning empty volume image')
        volume_image = np.zeros(shape=mask.shape, dtype=image.dtype)
        figure = str()
        return Output(volume_image, figure)

    n_beads = np.count_nonzero(beads_centroids)
    logger.info('found %d beads on cells', n_beads)

    if n_beads == 0:
        logger.warn('empty volume image')
        volume_image = np.zeros(shape=mask.shape, dtype=image.dtype)
    else:
        logger.debug('locate beads in 3D')
        beads_coords_3D = locate_in_3D(
            image=image, mask=beads_centroids,
            bin_size=superpixel_size
        )

        logger.info('interpolate cell surface')
        volume_image = interpolate_surface(
            coords=beads_coords_3D,
            output_shape=np.shape(image[:, :, 1]),
            method='linear'
        )

        volume_image = volume_image.astype(image.dtype)

        if (close_surface is True):
            import mahotas as mh
            logger.info('morphological closing of cell surface')
            volume_image = mh.close(volume_image,
                                    Bc=mh.disk(close_disc_size))
        volume_image[mask == 0] = 0

    if plot:
        logger.debug('convert bottom surface plane to image for plotting')
        bottom_surface_image = np.zeros(slide.shape, dtype=np.uint8)
        for ix in range(slide.shape[0]):
            for iy in range(slide.shape[1]):
                bottom_surface_image[ix, iy] = plane(
                    ix, iy, bottom_surface.x)

        logger.info('create plot')
        from jtlib import plotting
        plots = [
            plotting.create_intensity_image_plot(
                mip, 'ul', clip=True
            ),
            plotting.create_intensity_image_plot(
                bottom_surface_image, 'll', clip=True
            ),
            plotting.create_intensity_image_plot(
                volume_image, 'ur', clip=True
            )
        ]
        figure = plotting.create_figure(
            plots, title='Convert stack to volume image'
        )
    else:
        figure = str()

    return Output(volume_image, figure)
def main(image,
         mask,
         threshold=150,
         bead_size=2,
         superpixel_size=4,
         close_surface=False,
         close_disc_size=8,
         plot=False):
    '''Converts an image stack with labelled cell surface to a cell
    `volume` image

    Parameters
    ----------
    image: numpy.ndarray[Union[numpy.uint8, numpy.uint16]]
        grayscale image in which beads should be detected (3D)
    mask: numpy.ndarray[Union[numpy.int32, numpy.bool]]
        binary or labeled image of cell segmentation (2D)
    threshold: int, optional
        intensity of bead (default: ``150``)
    bead_size: int, optional
        minimal size of bead (default: ``2``)
    superpixel_size: int, optional
        size of superpixels for searching the 3D position of a bead
    close_surface: bool, optional
        whether the interpolated surface should be morphologically closed
    close_disc_size: int, optional
        size in pixels of the disc used to morphologically close the
        interpolated surface
    plot: bool, optional
        whether a plot should be generated (default: ``False``)

    Returns
    -------
    jtmodules.generate_volume_image.Output
    '''

    n_slices = image.shape[-1]
    logger.debug('input image has size %d in last dimension', n_slices)

    logger.debug('mask beads inside cell')
    beads_outside_cell = np.copy(image)
    for iz in range(n_slices):
        beads_outside_cell[mask > 0, iz] = 0

    logger.debug('search for 3D position of beads outside cell')
    slide = np.argmax(beads_outside_cell, axis=2)
    slide[slide > np.percentile(slide[mask == 0], 20)] = 0

    logger.debug('determine surface of slide')
    slide_coordinates = array_to_coordinate_list(slide)
    bottom_surface = fit_plane(
        subsample_coordinate_list(slide_coordinates, 2000))

    logger.debug('detect_beads in 2D')
    mip = np.max(image, axis=-1)
    try:
        # TODO: use LOG filter???
        beads, beads_centroids = detect_blobs(image=mip,
                                              mask=np.invert(mask > 0),
                                              threshold=threshold,
                                              min_area=bead_size)
    except:
        logger.warn('detect_blobs failed, returning empty volume image')
        volume_image = np.zeros(shape=mask.shape, dtype=image.dtype)
        figure = str()
        return Output(volume_image, figure)

    n_beads = np.count_nonzero(beads_centroids)
    logger.info('found %d beads on cells', n_beads)

    if n_beads == 0:
        logger.warn('empty volume image')
        volume_image = np.zeros(shape=mask.shape, dtype=image.dtype)
    else:
        logger.debug('locate beads in 3D')
        beads_coords_3D = locate_in_3D(image=image,
                                       mask=beads_centroids,
                                       bin_size=superpixel_size)

        logger.info('interpolate cell surface')
        volume_image = interpolate_surface(coords=beads_coords_3D,
                                           output_shape=np.shape(image[:, :,
                                                                       1]),
                                           method='linear')

        volume_image = volume_image.astype(image.dtype)

        if (close_surface is True):
            import mahotas as mh
            logger.info('morphological closing of cell surface')
            volume_image = mh.close(volume_image, Bc=mh.disk(close_disc_size))
        volume_image[mask == 0] = 0

    if plot:
        logger.debug('convert bottom surface plane to image for plotting')
        bottom_surface_image = np.zeros(slide.shape, dtype=np.uint8)
        for ix in range(slide.shape[0]):
            for iy in range(slide.shape[1]):
                bottom_surface_image[ix, iy] = plane(ix, iy, bottom_surface.x)

        logger.info('create plot')
        from jtlib import plotting
        plots = [
            plotting.create_intensity_image_plot(mip, 'ul', clip=True),
            plotting.create_intensity_image_plot(bottom_surface_image,
                                                 'll',
                                                 clip=True),
            plotting.create_intensity_image_plot(volume_image, 'ur', clip=True)
        ]
        figure = plotting.create_figure(plots,
                                        title='Convert stack to volume image')
    else:
        figure = str()

    return Output(volume_image, figure)