pythonopencvcomputer-vision

Python - OpenCV - Is there a way to "Fill" an expected contour?


I want to detect the contour of a ball and the object will always be a ball, but i have an object in the middle that can obstruct part of the ball, here's my current situation : (https://i.sstatic.net/gwxnsVTI.png)

I made it so it works just fine in this case where theres a good amount of the ball on both sides, but when it reduces its not as good: (https://i.sstatic.net/gYiZ9bCI.png)

I know I can change the parameters of my code where I just concatenate both parts when they are of a good enough size, but it gets very situational when it works because of the noise, so I can't reduce the parameters that much:

import cv2
import numpy as np
from pyfirmata2 import Arduino, SERVO
import time
import serial



# Camera Connection/Setup
cap = cv2.VideoCapture(0)


while True:
    _, frame = cap.read()
    hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    
    #Blue color
    low_blue = np.array([100, 100, 20])
    high_blue = np.array([200, 255, 255])
    blue_mask = cv2.inRange(hsv_frame, low_blue, high_blue)
    #blue_mask = cv2.GaussianBlur(blue_mask,(5,5),0)
    contours, hierarchy = cv2.findContours(blue_mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    contours = sorted(contours, key=lambda x:cv2.contourArea(x), reverse=True)

    sum = 0
    emptymask = []
    for cnt in contours:
        if cv2.contourArea(cnt) > 300: #If the parts are big enough it assumes its a part of the ball
            sum += cv2.contourArea(cnt)
            emptymask.append(cnt)

    #print(sum)
    emptymask = cv2.vconcat(emptymask)

    (x, y, w, h) = cv2.boundingRect(emptymask)
   
    cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)

    
    cv2.imshow("Frame", frame)
    cv2.imshow("Mask", blue_mask)

    
    key = cv2.waitKey(1)
    if key == 27:
        break
        

cap.release()
cv2.destroyAllWindows()

So I was thinking wether I can sort of deduce the ball based on its size or something like that, since it's always the same ball. I did do something like that in another code where if it's not the size expected, it "guesses" the whole ball and makes a square, but it became incredibly hard to calibrate the exact x and y positions:

if reference > 6000: #See if the object is cropped or not
        cv2.rectangle(frame, (x, y), (x + w, y + w), (0, 255, 0), 2)
    elif ((x>180 and x<230) and (y>170 and y<240) or (y<5 and x<225)):                #If it is, it will calculate the rest of the area
        cv2.rectangle(frame, (x+ball_diameter, y+h), (x, y+h - ball_diameter), (0, 255, 0), 2)
    elif (x>230 and x<290) and (y>140 and y<170):                #If it is, it will calculate the rest of the area
        cv2.rectangle(frame, (x+w, y+ball_diameter), (x+w - ball_diameter, y), (0, 255, 0), 2)
    elif (x>270 and x<300 and (y<140 or y>230)) or (y>220 and y<250) or (y<5 and x>225):                #If it is, it will calculate the rest of the area
        cv2.rectangle(frame, (x+w, y+h), (x+w - ball_diameter, y+h - ball_diameter), (0, 255, 0), 2)
    else:
        cv2.rectangle(frame, (x, y), (x + ball_diameter, y + ball_diameter), (0, 255, 0), 2)    


Finally, my question is : Is there a way to detect the arch from either side and just fill the missing circle gap ? Or an even better way ?


Solution

  • Here is one way to do that in Python/OpenCV.

    Since you did not post actual input files and not screen snaps, I took your image and cropped it to get the black/white mask image as my input.

    Input:

    enter image description here

    import cv2
    import numpy as np
    
    # Read image
    img = cv2.imread('input_mask.png')
    hh, ww = img.shape[:2]
    
    # Convert to grayscale
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # threshold
    thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1]
    
    # apply morphology to clean up small spots leaving only two largest ones
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
    morph = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
    morph = cv2.morphologyEx(morph, cv2.MORPH_CLOSE, kernel)
    
    # get convex hull
    points = np.column_stack(np.where(morph.transpose() > 0))
    hull = cv2.convexHull(points)
    
    # draw convex hull in white filled on black 
    hull_img = np.zeros_like(morph)
    cv2.fillPoly(hull_img, [hull], 255)
    
    # get Hough circles
    circles = cv2.HoughCircles(hull_img, cv2.HOUGH_GRADIENT, 1, minDist=210, param1=150, param2=10, minRadius=40, maxRadius=100)
    print(circles)
    
    # draw circles
    result = img.copy()
    for circle in circles[0]:
        # draw the circle in the output image, then draw a rectangle
        # corresponding to the center of the circle
        (x,y,r) = circle
        x = int(x)
        y = int(y)
        r = int(r)
        cv2.circle(result, (x, y), r, (0, 0, 255), 1)
    
    # ALTERNATELY: get minEnclosingCircle
    center, radius = cv2.minEnclosingCircle(hull)
    cx = int(round(center[0]))
    cy = int(round(center[1]))
    rr = int(round(radius))
    
    # draw minEnclosingCircle over copy of input
    result2 = img.copy()
    cv2.circle(result2, (cx,cy), rr, (0, 0, 255), 1)
    
    # save results
    cv2.imwrite('input_mask_morph.png', morph)
    cv2.imwrite('input_mask_convex_hull.png', hull_img)
    cv2.imwrite('input_mask_circle3.png', result)
    cv2.imwrite('input_mask_circle3b.png', result2)
    
    # show images
    cv2.imshow('thresh', thresh)
    cv2.imshow('morph', morph)
    cv2.imshow('convex hull', hull_img)
    cv2.imshow('result', result)
    cv2.imshow('result2', result2)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    

    Morphology cleaned image:

    enter image description here

    Convex hull image:

    enter image description here

    Resulting circle on copy of input:

    enter image description here

    Resulting circle from minEnclosingCircle on input:

    enter image description here