packoman
packoman

Reputation: 1282

Merge image-segments depending on length of the watershed-line in-between using Python, Numpy and Scikit-Image/OpenCV

I am working on a watershedding-based segmentation algorithm to segment fluorescence images such as this one:

enter image description here

As result I obtain a Numpy array with labels for each segment. These are separated by a watershed lines, if the corresponding regions in the fluorescence image have a sufficiently large intensity-drop-off between them. For very large intensity-drop-offs they are completely separated through simple thresholding. The result for the image above is this:

enter image description here

My algorithm performs well for the vast majority of cases. However, it sometimes it has a slight tendency to oversegment. Such as in this case from the image above:

enter image description here

Since these cases will be difficult to improve by working further on the intensity-based segmentation itself (and I run the risk of breaking other things), I want to instead selectively merge adjacent segments based on the length of the watershed-line between them and the averaged maximum width of the two segments above and below. I know what I have to do on a pixel-for-pixel basis:

  1. Find pixels that have two different label-values in their direct neighborhood. Store these pixels separately for each segment-pair (with corresponding segment-labels).
  2. Calculate the number of these pixels for each pair of adjacent segments to obtain the length of the watershed-line.
  3. Calculate the maximum width (horizontally for simplicity) of the adjacent segments.
  4. Merge the adjacent segments, if the watershed-line is longer than a given threshold-fraction (user-defined) of the averaged width of the two segments. I could do this by converting the labels to a binary mask, filling the watershed line using the stored pixels where applicable, and relabelling the binary mask.

Since in Python iterating over individual pixels is generally slow, I am unsure how to write performant code for this. Therefore I am looking for suggestions on how to implement this with Numpy and Skimage (OpenCV is also an option).

Upvotes: 2

Views: 1046

Answers (1)

JoOkuma
JoOkuma

Reputation: 492

You didn't provide how you got your initial segments. Despite this, I think improving the watershed lines could solve your problem and this can be done in the watershed hierarchy framework, with the Higra package.

I specify an initial ordering of the watershed by the image complement and recompute its watershed lines with another attribute (volume).

The intensity drop and area that you describe are the volume attribute, and you can control the segmentation by its threshold in the hierarchy.

Here it is a working example:

import cv2
import numpy as np
import higra as hg
from skimage.morphology import remove_small_objects, label
import matplotlib.pyplot as plt

def main():
    img_path = "fig.png"
    img = cv2.imread(img_path)
    img = img[:,:,0].copy()
    img = img.max() - img

    size = img.shape[:2]
    graph = hg.get_4_adjacency_graph(size)
    
    edge_weights = hg.weight_graph(graph, img, hg.WeightFunction.mean)
    tree, altitudes = hg.quasi_flat_zone_hierarchy(graph, edge_weights)

    attr = hg.attribute_volume(tree, altitudes)

    saliency = hg.saliency(tree, attr) 
    # Take a look at this :)
    # grid = hg.graph_4_adjacency_2_khalimsky(graph, saliency)
    # plt.imshow(grid)
    # plt.show()
    
    attr_thold = np.mean(saliency) / 4  # arbitrary
    area_thold = 500  # arbitrary 

    segments = hg.labelisation_horizontal_cut_from_threshold(tree, attr, attr_thold)
    segments = label(remove_small_objects(segments, area_thold))

    plt.imshow(segments)
    plt.show()

if __name__ == "__main__":
    main()

Here it is the result.

Result

Upvotes: 0

Related Questions