Search code examples
pythonnumpyopencvmaskcontour

Rectify edges of a shape in mask with OpenCV


I need to rectify the edges of a shape (polygon) like the one below.

enter image description here

It is the result of cv2.approxPolyDPm, that approximates cv2.findContours results:

for (i, c) in enumerate(cnts):
    peri = cv2.arcLength(c, closed=True)
    approx = cv2.approxPolyDP(c, epsilon=0.01 * peri, closed=True)

Some borders are not straight. I need them to be perfectly vertical or horizontal. I tried modifying the epsilon value without success.


Solution

  • You need to add another stage that forces contour vertices to form only horizontal and vertical straight lines.

    If two vertices p1, p2 has very close y coordinates (say below 10 pixels), you need to fix y coordinate of p1 to be equal to y coordinate of p2 or vice versa.

    Here is a working code sample (please read the comments):

    import cv2
    import numpy as np
    
    font = cv2.FONT_HERSHEY_COMPLEX
    
    img = cv2.imread('img.png', cv2.IMREAD_COLOR)
    
    # Remove image borders
    img = img[20:-20, 20:-20, :]
    
    imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # https://pysource.com/2018/09/25/simple-shape-detection-opencv-with-python-3/
    # From the black and white image we find the contours, so the boundaries of all the shapes.
    _, threshold = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY)
    _, contours, _ = cv2.findContours(threshold, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    
    c = contours[0]
    
    peri = cv2.arcLength(c, closed=True)
    approx = cv2.approxPolyDP(c, epsilon=0.01 * peri, closed=True)
    
    # Delat threshold
    t = 10
    
    # n - Number of vertices
    n = approx.shape[0]
    
    for i in range(n):
        #      p1              p2
        #       *--------------*
        #       |
        #       |
        #       |
        #       *
        #      p0
    
        p0 = approx[(i+n-1) % n][0]    # Previous vertex
        p1 = approx[i][0]              # Current vertex
        p2 = approx[(i + 1) % n][0]    # Next vertex
        dx = p2[0] - p1[0]             # Delta pixels in horizontal direction
        dy = p2[1] - p1[1]             # Delta pixels in vertical direction
    
        # Fix x index of vertices p1 and p2 to be with same x coordinate ([<p1>, <p2>] form horizontal line).
        if abs(dx) < t:
            if ((dx < 0) and (p0[0] > p1[0])) or ((dx > 0) and (p0[0] < p1[0])):
                p2[0] = p1[0]
            else:
                p1[0] = p2[0]
    
        # Fix y index of vertices p1 and p2 to be with same y coordinate ([<p1>, <p2>] form vertical line).
        if abs(dy) < t:
            if ((dy < 0) and (p0[1] > p1[1])) or ((dy > 0) and (p0[1] < p1[1])):
                p2[1] = p1[1]
            else:
                p1[1] = p2[1]
    
        approx[i][0] = p1
        approx[(i + 1) % n][0] = p2
    
    cv2.drawContours(img, [approx], 0, (0, 255, 0), 1)
    
    # Finally we display everything on the screen:
    cv2.imshow("img", img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    

    Result:
    enter image description here

    Note: the solution needs some polishing (my intention was to get the contour with the minimal area).