pythonopencvcontourconvex-optimization

OpenCV Python: Closed Contour Approximation For A Speech Bubble Shape


I've got a shape like a speech bubble. And I only want to detect the ellipse of this shape like in the image with the green encircled one.

enter image description here enter image description here

I tried with closed morphology, but certain parts of the bubbles are also removed. I used a Kernel with a matrix of 20, 20. The shape becomes more rectangular. Maybe I have to change the kernel matrix more like this:

0 1 0
1 1 1
0 1 0

I also tried to draw a convex hull, but it also has no effect. And a inner convex hull is not possible. Here is my code for drawing a convex hull:

for i in range (max_index):
    hull = cv2.convexHull(contours[i])
    cv2.drawContours(image, [hull], 0, (0, 255, 0), 2)

I retrieved the contours with the parameters cv2.RETR_EXTERNAL and cv2.CHAIN_APPROX_NONE


Solution

  • This is the best I was able to get: Ballon ellipse

    It is not the most clever way to do it. What I am doing here is actually simple, despite the verbose code.

    First, I get the gray image and add a lot of blur and in the same way you tried, apply threshold and find contours. I then take the biggest contour and find the ellipse that fits this contour with fitEllipse. This is all in getEllipse function.

    In this first round, the ellipse will be skewed because the tail is getting in the way. So, I use this not so good ellipse to process the original image and give another try.

    The function grayEllipse filters an image by an ellipse. So, I use the ellipse from the first try to get process the original image and highlight the points the are inside the first ellipse. I use this image as input in this second round.

    By repeating the process, the final ellipse I get in the second time is much less skewed.

    Here is the code:

    import cv2
    import numpy as np
    
    
    def getEllipse(imgray):
    
    
        ret, thresh = cv2.threshold(imgray, 20, 255, cv2.THRESH_BINARY|cv2.THRESH_OTSU)
        _, contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
    
        maxArea = 0
        best = None
        for contour in contours:
            area = cv2.contourArea(contour)
            if area > maxArea :
                maxArea = area
                best = contour
    
        ellipse = cv2.fitEllipse(best)
        el = np.zeros(imgray.shape)
        cv2.ellipse(el, ellipse,(255,255,255),-1)
    
        return el
    
    def grayEllipse(el, img):
        el = np.dstack((el,el,el))
        el = el*img
        el = el/(255)
        el = el.astype('uint8')
        imgray = cv2.cvtColor(el, cv2.COLOR_BGR2LAB)[...,0]
        return imgray
    
    
    image = cv2.imread("./baloon.png", cv2.IMREAD_COLOR)
    img = image.copy()
    imgray = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)[...,0]
    imgray = cv2.GaussianBlur(imgray, (79,79), 0)
    el = getEllipse(imgray)
    imgray = grayEllipse(el, img.copy())
    imgray = cv2.GaussianBlur(imgray, (11,11), 0)
    el = getEllipse(imgray)
    imgray = grayEllipse(el, img.copy())
    
    ret, thresh = cv2.threshold(imgray, 20, 255, cv2.THRESH_BINARY|cv2.THRESH_OTSU)
    _, contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
    
    maxArea = 0
    best = None
    for contour in contours:
        area = cv2.contourArea(contour)
        if area > maxArea :
            maxArea = area
            best = contour
    
    ellipse = cv2.fitEllipse(best)
    cv2.ellipse(image, ellipse, (0,255,0),3)
    
    while True:
      cv2.imshow("result", image)
      k = cv2.waitKey(30) & 0xff
      if k == 27:
          break