Shaun314
Shaun314

Reputation: 3461

OpenCV - Bridge Components without Altering Region Size

Goal:

I'd like to dilate a binary mask with a kernel size of N, then erode it with effectively the same kernel, but leave any connections intact that were formed by the dilation. When I do connectedComponentsWithStats, I want anything close together to have been merged into one component.

This has been a surprisingly challenging endeavor.

Example Image: enter image description here In this case, the goal is to have this stray pixel join the object on the left, but not change the size of the object on the left.

Effort put in:

At first glance, a "closing" operation seemed perfect for this, but I noticed that it was eroding the connection formed during the dilation, essentially not working at all for this use-case.

I've been thinking through other options of dilation, erosion, and potentially some creative use of contours to help.

I thought I was close if I were to do a close operation on the inversion of the mask as documented here, but that had the effect of eliminating significant parts of the source mask, not just closing the gaps.

One idea I had was to do a dilation, find the skeleton, erode, and then "or" the skeleton and original image together. I ran into two issues: 1) At least the implementations of finding mask skeletons I found online were extremely slow, and 2) I'm not sure this is the best idea in the first place (would the skeletal line go through my single pixel I'm trying to capture?)

I'm hoping I am just being silly and missing something obvious?

Upvotes: 2

Views: 493

Answers (2)

Shaun314
Shaun314

Reputation: 3461

Thanks to Knight Forked, I had an idea that I think was similar in method that seemed to work reasonably elegantly.

The goal was to consume stray pixels close to larger regions, but leave as much of the image, and stray pixels not close to a larger region alone.

I ended up going through each of the connected components and if a component was smaller than X, I'd dilate it by Y and see if that resulted in an overlap with another component. If there was overlap, one could leave the dilation in place in the final image, or in my case, just note that it wasn't actually stray.

Here was the code I ended up with! My goal was to return a JSON blob with a raw list of connected component areas, and then a withinPixels list as a subset of the raw list, where anything that was stray but close to another component would be filtered out.

    PIXEL_RANGE = 3 # Allowed distance from other connected components
    PIXEL_CONNECTIVITY = 8 # , or 4 | For Connected Component Analysis 
    MIN_PIXEL_COUNT = 5 # Components with < than MIN_PIXEL_COUNT area = stray

    label = np.uint8(gray == index) # Your boolean mask
    base_mask = np.copy(label)

    kSize = PIXEL_RANGE * 2 + 1
    kernel = np.ones((kSize,kSize),np.uint8)

    num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(label , PIXEL_CONNECTIVITY , cv2.CV_32S)

    base_mask = np.copy(label)

    idx_res = {}
    idx_res["raw"] = []
    idx_res["withinPixels"] = []

    for i in np.arange(1, len(stats)):
        idx_res["raw"].append(int(stats[i][cv2.CC_STAT_AREA]))

        # Check to see if it's flagged
        if (stats[i][cv2.CC_STAT_AREA] <= MIN_PIXEL_COUNT):
            # Dilate this specifc label by KERNEL
            dilated = cv2.dilate(np.uint8(labels == i),kernel,iterations = 1)

            # See if it overlaps with other regions
            temp = base_mask + dilated - np.uint8(labels == i)

            if (np.max(np.max(temp)) == 1):
                # If it doesn't connect, add it to the withinPixels as is
                print("Still not connected")
                idx_res["withinPixels"].append(int(stats[i][cv2.CC_STAT_AREA]))
            else:
                # If it does, maybe consider add the dilated mask to base image
                # for now, just note that it was connected and do nothing?
                # possible edge-case as-is is when the dilated mask overlaps with another stray pixel.. 
                print("Connected a label successfully")

        else:
            # Already passed filter, add to withinPixels
            idx_res["withinPixels"].append(int(stats[i][cv2.CC_STAT_AREA]))

An edge-case that's not handled with this as-is is if there was a clump of stray pixels together. This code would note the overlap and incorrectly not flag that that clump still doesn't connect to a larger component. It should be easy to solve if someone needed by re-running the component analysis.

Upvotes: 1

Knight Forked
Knight Forked

Reputation: 1619

This is using the concept I mentioned in the comments. Of course this is kind of brute force method but can be tweaked to give desired results, I think.

img = cv2.imread('/your/binarized/image', \
                  cv2.IMREAD_GRAYSCALE)
h, w = gray.shape
ret, markers = cv2.connectedComponents(img)

out = np.copy(img)
for i in range(1, w-1):
    for j in range(1, h-1):
        if markers[j][i] == 0:
            lset = set()
            for m in range(-1, 2):
                for n in range(-1, 2):
                    if markers[j+m][i+n] != 0 and \
                    (markers[j+m][i+n] not in lset):
                        lset.add(markers[j+m][i+n])
                        if(len(lset) >= 2):
                            out[j][i] = 255
                            break

Upvotes: 2

Related Questions