Home > Blockchain >  How can I recreate original image from cv2.findContours result with holes?
How can I recreate original image from cv2.findContours result with holes?

Time:12-03

I am trying to detect contours within a binary image using OpenCV, and then plotting the resulting contour polygons to recreate the input image. However, the representation in which OpenCV contour polygons are returned do not make that easy.

First, let's setup up the data:

import cv2
import numpy as np


def a_small_hole_with_diagonal_border() -> np.ndarray:
    bitmask = np.zeros((10, 10), dtype=np.uint8)   255
    indices = [(2, 2), (2, 3), (2, 4), (2, 5), (2, 6),
               (3, 4), (3, 5), (3, 6),
               (4, 4), (4, 5), (4, 6)]
    row_indices, col_indices = zip(*indices)
    bitmask[row_indices, col_indices] = 0
    bitmask[5:, 7:] = 0
    return bitmask


bitmask = a_small_hole_with_diagonal_border()
padded_bitmask = np.zeros((bitmask.shape[0]   2, bitmask.shape[1]   2), dtype=bitmask.dtype)
padded_bitmask[1:-1, 1:-1] = bitmask

This image looks like this (These are just screenshots of matplotlib):

The image I am running findContours on

Now I am running findContours on this and plotting the resulting contours:

contours, hierarchy = cv2.findContours(image=padded_bitmask,
                                       mode=cv2.RETR_TREE,
                                       method=cv2.CHAIN_APPROX_SIMPLE)

def get_bitmask(*polygons: np.ndarray, width: int, height: int) -> np.ndarray:
    image = np.zeros((width, height, 3), dtype=np.uint8)
    for polygon in polygons:
        polygon = polygon.reshape((-1, 1, 2)).astype(np.int32)
        cv2.fillPoly(image, pts=[polygon], color=(0, 0, 255))

    return image[..., -1]

get_bitmask(contours[0], 10, 10)
get_bitmask(contours[1], 10, 10)

And am getting the following output:

First level contour Second level contour

The outline of the outer area is exactly what I want, but for the hole in the middle I would like to contour coordinates to be in a way that does not represent the inner-most border of the contour, but rather the outer-most border of the hole. This is what I mean:

(I overlayed the second contour over the original image using gimp manually)

Border of contour

What I would like to have is a contour that describes these pixels for the hole (forgive my poor drawing skills):

border of hole

So in essence if I draw the second contour over the first contour (I am parsing the tree hierarchy for that), I would like to recreate the input image. How can I do this? If I just draw the polygon of the hole as suggested wrong recreation of the hole Without approximation

I tried to run findContours both on the image and its inverse and then merging the results, but there must be an easier way, since I couldn't get it to work in all cases, and simply getting the right representation would be a lot easier.

CodePudding user response:

Here is a solution, but there might be something better. Inverting the whole image is tricky, but you can invert the mask for each hole and then run findContours again. Then replace the contour of the hole with that output. Here is a function that computes the hole boundary from the blob boundary. Running it on the data from the question you will get the polygon that describes the data you drew if you put in the second contour.

def get_hole_contour_from_outer_boundary_contour(boundary_contour: np.ndarray,
                                                  padded_bitmask: np.ndarray) -> np.ndarray:
    # We have the outer boundary of the toplevel blob, but we need the outer boundary of the hole
    # which is the pixels that are enclosed by the boundary_contour
    col, row, width, height = cv2.boundingRect(boundary_contour)
    cutout = padded_bitmask[row:row   height, col:col   width]

    # we invert the pixels so that the black hole now becomes a white blob
    # this way we get the representation we want when we extract contours again
    inverse_cutout = np.abs(255 - cutout)

    # Now we can use RETR_EXTERNAL, which only gives us the outer boundaries and does not care about holes
    contours_hole, _ = cv2.findContours(image=inverse_cutout,
                                        mode=cv2.RETR_EXTERNAL,
                                        method=cv2.CHAIN_APPROX_SIMPLE,
                                        offset=(col, row)
                                        )
    # Since we cut out the area exactly around the hole, the first contour is always the one we want
    return contours_hole[0]

CodePudding user response:

It's not smart, but how about this way?

import cv2
import numpy as np
import matplotlib.pyplot as plt

def a_small_hole_with_diagonal_border() -> np.ndarray:
    bitmask = np.zeros((10, 10), dtype=np.uint8)   255
    indices = [(2, 2), (2, 3), (2, 4), (2, 5), (2, 6),
               (3, 4), (3, 5), (3, 6),
               (4, 4), (4, 5), (4, 6)]
    row_indices, col_indices = zip(*indices)
    bitmask[row_indices, col_indices] = 0
    bitmask[5:, 7:] = 0
    return bitmask


bitmask = a_small_hole_with_diagonal_border()
padded_bitmask = np.zeros((bitmask.shape[0]   2, bitmask.shape[1]   2), dtype=bitmask.dtype)
padded_bitmask[1:-1, 1:-1] = bitmask

Creating the inner contour

invgray = cv2.bitwise_not(padded_bitmask)
plt.imshow(invgray, cmap="gray")

enter image description here

kernel_size=3
kernel  = np.ones((kernel_size,kernel_size), np.uint8)
dilation = cv2.erode(invgray, kernel, iterations=1)
plt.imshow(dilation, cmap="gray")

enter image description here

diff = padded_bitmask   dilation
diff_inv = cv2.bitwise_not(diff)
plt.imshow(diff_inv, cmap="gray")

enter image description here

Creating mask

contours, hierarchy = cv2.findContours(image=padded_bitmask,
                                       mode=cv2.RETR_TREE,
                                       method=cv2.CHAIN_APPROX_NONE)

mask = np.zeros((12, 12), dtype=np.uint8)
cv2.drawContours(mask, contours, 1, color=(255, 255, 255), thickness=cv2.FILLED)
plt.imshow(mask, cmap="gray")

enter image description here

integrate

masked = cv2.bitwise_and(diff_inv, mask)
plt.imshow(masked, cmap="gray")

enter image description here

  • Related