2

I am trying to detect the outer boundary of the circular object in the images below:

I tried OpenCV's Hough Circle, but the code is not working for every image. I also tried to adjust parameters such as minRadius and maxRadius in Hough Circle but its not working on every image.

The aim is to detect the object from the image and crop it.

Expected output:

Source code:

import imutils
import cv2
import numpy as np
from matplotlib import pyplot as plt


image = cv2.imread("path to the image i have provided")
r = 600.0 / image.shape[1]
dim = (600, int(image.shape[0] * r))
resized = cv2.resize(image, dim, interpolation = cv2.INTER_AREA)
cv2.imwrite("path to were we want to save downscaled image", resized)


image = cv2.imread('path of downscaled image')
image1 = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
image2 = cv2.GaussianBlur(image1, (5, 5), 0)
edged = cv2.Canny(image2, 30, 150)

img = cv2.medianBlur(image2,5)
cimg = cv2.cvtColor(img,cv2.COLOR_GRAY2BGR)

circles = cv2.HoughCircles(edged,cv2.HOUGH_GRADIENT,1,20,
                            param1=50,param2=30,minRadius=200,maxRadius=280)

circles = np.uint16(np.around(circles))

max_circle = max(circles[0,:], key=lambda x:x[2])
# print(max_circle)

# # Create mask
height,width = image1.shape
mask = np.zeros((height,width), np.uint8)


for i in [max_circle]:
    cv2.circle(mask,(i[0],i[1]),i[2],(255,255,255),thickness=-1)  


masked_data = cv2.bitwise_and(image, image, mask=mask)

_,thresh = cv2.threshold(mask,1,255,cv2.THRESH_BINARY)

# Find Contour
contours = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[0]
x,y,w,h = cv2.boundingRect(contours[0])

# Crop masked_data
crop = masked_data[y:y+h,x:x+w]

#Code to close Window
cv2.imshow('OG',image)
cv2.imshow('Cropped ROI',crop)
cv2.imwrite("path to save roi image", crop)
cv2.waitKey(0)
cv2.destroyAllWindows()

karlphillip
  • 92,053
  • 36
  • 243
  • 426
laura
  • 23
  • 3
  • 1
    houghCircles has some problems with concentric circles (maybe the dark one in the inside and the big one at the outside). Do you know that there is such a circle in each of the images, or do you have to detect whether there is a circle at all? – Micka Feb 05 '20 at 08:22
  • Object is detected using Object Detection method, so yes every image will contain an object, and after this i have to detect the shape of the object which is circle and crop it – laura Feb 05 '20 at 08:48
  • I would suggest a proper edge detection (hopefully mostly only remaining outer and inner circle, not so much of the high frequencies in between) followed by a RANSAC circle detection to detect the 2 best circles in the image: https://stackoverflow.com/a/20734263/2393191 https://stackoverflow.com/questions/23976766/hough-circle-detection-accuracy-very-low/23993030#23993030 https://stackoverflow.com/questions/59428540/how-to-determine-rotation-of-a-shape/59431561#59431561 https://stackoverflow.com/questions/27095483/improving-circle-detection/27100805#27100805 – Micka Feb 05 '20 at 11:39
  • 1
    @Micka Great posts, by the way. – karlphillip Feb 07 '20 at 09:25

2 Answers2

4

Second Answer: an approach based on color segmentation.

While I was editing the question to improve it's readability and was inserting and resizing all the images from the link you shared to make it easier for everyone to visualize what you are trying to do, it occurred to me that this problem might be a better candidate for an approach based on segmentation by color:

This simpler (but clever) approach assumes that the reel appears pretty much in the same location and has more or less the same dimensions every time:

  • To discover the approximate color of the reel in the image, define a list of Regions of Interest (ROIs) to sample pixels from and determine the min and max color of that area in the HSV color space. The location and size of the ROI are values derived from the size of the image. In the images below, you can see the ROIs as draw as blue-ish rectangles:

  • Once the min and max HSV colors have been found, a threshold operation with cv2.inRange() can be executed to segment the reel:

  • Then, iterate though all the contours in the binary image and assume that the largest one represents the reel. Use this contour and draw it in a separate mask to be able to extract the pixels from original image:

  • At this stage, it is also possible to compute a bounding box for the contour and extract it's precise location to be able to perform a crop operation later and completely isolate the reel in the image:

This approach works for EVERY image shared on the question.

Source code:

import cv2
import numpy as np
import sys


# initialize global H, S, V values
min_global_h = 179
min_global_s = 255
min_global_v = 255

max_global_h = 0
max_global_s = 0
max_global_v = 0

# load input image from the cmd-line
filename = sys.argv[1]
img = cv2.imread(sys.argv[1])
if (img is None):
    print('!!! Failed imread')
    sys.exit(-1)

# create an auxiliary image for debugging purposes
dbg_img = img.copy()

# initiailize a list of Regions of Interest that need to be scanned to identify good HSV values to threhsold by color
w = img.shape[1]
h = img.shape[0]
roi_w = int(w * 0.10)
roi_h = int(h * 0.10)
roi_list = []
roi_list.append( (int(w*0.25), int(h*0.15), roi_w, roi_h) )
roi_list.append( (int(w*0.25), int(h*0.60), roi_w, roi_h) )

# convert image to HSV color space
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# iterate through the ROIs to determine the min/max HSV color of the reel
for rect in roi_list:
    x, y, w, h = rect
    x2 = x + w
    y2 = y + h
    print('ROI rect=', rect)

    cropped_hsv_img = hsv_img[y:y+h, x:x+w]

    h, s, v = cv2.split(cropped_hsv_img)
    min_h  = np.min(h)
    min_s  = np.min(s)
    min_v  = np.min(v)

    if (min_h < min_global_h):
        min_global_h = min_h

    if (min_s < min_global_s):
        min_global_s = min_s

    if (min_v < min_global_v):
        min_global_v = min_v

    max_h  = np.max(h)
    max_s  = np.max(s)
    max_v  = np.max(v)

    if (max_h > max_global_h):
        max_global_h = max_h

    if (max_s > max_global_s):
        max_global_s = max_s

    if (max_v > max_global_v):
        max_global_v = max_v

    # debug: draw ROI in original image
    cv2.rectangle(dbg_img, (x, y), (x2, y2), (255,165,0), 4) # red


cv2.imshow('ROIs', cv2.resize(dbg_img, dsize=(0, 0), fx=0.5, fy=0.5))
#cv2.waitKey(0)
cv2.imwrite(filename[:-4] + '_rois.png', dbg_img)

# define min/max color for threshold
low_hsv = np.array([min_h, min_s, min_v])
max_hsv = np.array([max_h, max_s, max_v])
#print('low_hsv=', low_hsv)
#print('max_hsv=', max_hsv)

# threshold image by color
img_bin = cv2.inRange(hsv_img, low_hsv, max_hsv)
cv2.imshow('binary', cv2.resize(img_bin, dsize=(0, 0), fx=0.5, fy=0.5))
cv2.imwrite(filename[:-4] + '_binary.png', img_bin)

#cv2.imshow('img_bin', cv2.resize(img_bin, dsize=(0, 0), fx=0.5, fy=0.5))
#cv2.waitKey(0)

# create a mask to store the contour of the reel (hopefully)
mask = np.zeros((img_bin.shape[0], img_bin.shape[1]), np.uint8)
crop_x, crop_y, crop_w, crop_h = (0, 0, 0, 0)

# iterate throw all the contours in the binary image:
#   assume that the first contour with an area larger than 100k belongs to the reel
contours, hierarchy = cv2.findContours(img_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
for contourIdx, cnt in enumerate(contours):
    area = cv2.contourArea(contours[contourIdx])
    print('contourIdx=', contourIdx, 'area=', area)

    # draw potential reel blob on the mask (in white)
    if (area > 100000):
        crop_x, crop_y, crop_w, crop_h = cv2.boundingRect(cnt)
        centers, radius = cv2.minEnclosingCircle(cnt)

        cv2.circle(mask, (int(centers[0]), int(centers[1])), int(radius), (255), -1) # fill with white
        break

cv2.imshow('mask', cv2.resize(mask, dsize=(0, 0), fx=0.5, fy=0.5))
cv2.imwrite(filename[:-4] + '_mask.png', mask)

# copy just the reel area into its own image
reel_img = cv2.bitwise_and(img, img, mask=mask)
cv2.imshow('reel_img', cv2.resize(reel_img, dsize=(0, 0), fx=0.5, fy=0.5))
cv2.imwrite(filename[:-4] + '_reel.png', reel_img)

# crop the reel to a smaller image
if (crop_w != 0 and crop_h != 0):
    cropped_reel_img = reel_img[crop_y:crop_y+crop_h, crop_x:crop_x+crop_w]
    cv2.imshow('cropped_reel_img', cv2.resize(cropped_reel_img, dsize=(0, 0), fx=0.5, fy=0.5))

    output_filename = filename[:-4] + '_crop.png'
    cv2.imwrite(output_filename, cropped_reel_img)

cv2.waitKey(0)
karlphillip
  • 92,053
  • 36
  • 243
  • 426
  • Well i never thought of that. My ROI is always going to occupy most of the image and will be in middle of the image..This is gonna work for my case.Thanks – laura Feb 07 '20 at 07:15
  • 1
    nice approach, would've tried something similar if I've had a bit more time – Micka Feb 07 '20 at 12:22
2

First answer: an approach based on pre-processing the image and executing an adaptiveThreshold operation.

There might be other ways of solving this problem that are not based on Hough Circles. Here is the result of an approach that is not:

  • Preprocess the image! Decreasing the size of the image and executing a blur helps with segmentation:

  • The segmentation method uses a cv2.adaptiveThreshold() to create a binary image that preserves the most important objects: the center of the reel and the external edge of the reel. This is an important step since we are only interested in what exists between these two objects. However, life is not perfect and neither is this segmentation. The shadow of reel on the table became part of the binary objects detected. Also, the outer edge is not fully connected as you can see on the resulting image on the right (look at the top left of the circumference):

  • To join broken segments, a morphological operation can be executed:

  • Finally, the entire reel area can be exposed by iterating through the contours of the image above and discarding those whose area is larger than what is expected for a reel. The resulting binary image (on the left) can then be used as a mask to identify the reel location on the original image:

Keep in mind that I'm not trying to find an universal solution for your problem. I'm merely showing that there might be other solutions that don't depend on Hough Circles.

Also, this code might need some adjustments to work on a larger number of cases.

Source code:

import cv2
import numpy as np
import sys


img = cv2.imread("test_images/reel.jpg")
if (img is None):
    print('!!! Failed imread')
    sys.exit(-1)

# create output image
output_img = img.copy()

# 1. Preprocess the image: downscale to speed up processing and execute a blur
SCALE_FACTOR = 0.5
smaller_img = cv2.resize(img, dsize=(0, 0), fx=SCALE_FACTOR, fy=SCALE_FACTOR)
blur_img = cv2.medianBlur(smaller_img, 9)
cv2.imwrite('reel1_blur_img.png', blur_img)


# 2. Segment the image to identify the 2 most important contours: the center of the reel and the outter edge
gray_img = cv2.cvtColor(blur_img, cv2.COLOR_BGR2GRAY)
img_bin = cv2.adaptiveThreshold(gray_img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 19, 4)
cv2.imwrite('reel2_img_bin.png', img_bin)

green_mask = np.zeros((img_bin.shape[0], img_bin.shape[1]), np.uint8)
#green_mask = cv2.cvtColor(img_bin, cv2.COLOR_GRAY2RGB) # debug

contours, hierarchy = cv2.findContours(img_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
for contourIdx, cnt in enumerate(contours):
    x, y, w, h = cv2.boundingRect(cnt)
    area = cv2.contourArea(contours[contourIdx])
    #print('contourIdx=', contourIdx, 'w=', w, 'h=', h, 'area=', area)

    # filter out tiny segments
    if (area < 5000):
        #cv2.fillPoly(green_mask, pts=[cnt], color=(0, 0, 255)) # red
        continue

    # draw green contour (filled)
    #cv2.fillPoly(green_mask, pts=[cnt], color=(0, 255, 0)) # green
    cv2.fillPoly(green_mask, pts=[cnt], color=(255)) # white

    # debug:
    #cv2.imshow('green_mask', green_mask)
    #cv2.waitKey(0)

cv2.imshow('green_mask', green_mask)
cv2.imwrite('reel2_green_mask.png', green_mask)


# 3. Fix mask: join segments nearby
kernel = np.ones((3,3), np.uint8)
img_dilation = cv2.dilate(green_mask, kernel, iterations=1)
green_mask = cv2.erode(img_dilation, kernel, iterations=1)

cv2.imshow('fixed green_mask', green_mask)
cv2.imwrite('reel3_img.png', green_mask)


# 4. Extract the reel area from the green mask
reel_mask = np.zeros((green_mask.shape[0], green_mask.shape[1]), np.uint8)
#reel_mask = cv2.cvtColor(green_mask, cv2.COLOR_GRAY2RGB) # debug

contours, hierarchy = cv2.findContours(green_mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
for contourIdx, cnt in enumerate(contours):
    x, y, w, h = cv2.boundingRect(cnt)
    area = cv2.contourArea(contours[contourIdx])
    print('contourIdx=', contourIdx, 'w=', w, 'h=', h, 'area=', area)

    # filter out smaller segments
    if (area > 110000):
        #cv2.fillPoly(reel_mask, pts=[cnt], color=(0, 0, 255)) # red
        continue

    # draw green contour (filled)
    #cv2.fillPoly(reel_mask, pts=[cnt], color=(0, 255, 0)) # green
    cv2.fillPoly(reel_mask, pts=[cnt], color=(255)) # white

    # debug:
    #cv2.imshow('reel_mask', reel_mask)
    #cv2.waitKey(0)

cv2.imshow('reel_mask', reel_mask)
cv2.imwrite('reel4_reel_mask.png', reel_mask)


# 5. Draw the reel area on the original image
contours, hierarchy = cv2.findContours(reel_mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
for contourIdx, cnt in enumerate(contours):
    centers, radius = cv2.minEnclosingCircle(cnt)

    # rescale these values back to the original image size
    centers_orig = (centers[0] // SCALE_FACTOR, centers[1] // SCALE_FACTOR)
    radius_orig = radius // SCALE_FACTOR

print('centers=', centers_orig, 'radius=', radius_orig)
cv2.circle(output_img, (int(centers_orig[0]), int(centers_orig[1])), int(radius_orig), (128,0,255), 5) # magenta

cv2.imshow('output_img', output_img)
cv2.imwrite('reel5_output.png', output_img)

# display just the pixels from the original image
larger_reel_mask = cv2.resize(reel_mask, (int(img.shape[1]), int(img.shape[0])))
output_reel_img = cv2.bitwise_and(img, img, mask=larger_reel_mask)

cv2.imshow('output_reel_img', output_reel_img)
cv2.imwrite('reel5_output_reel.png', output_reel_img)
cv2.waitKey(0)

At this point, its possible to use larger_reel_maskand compute a minimal enclosing circle, draw it over this mask to make it a little bit more round and allow us to retrieve the area of the reel more accurately:

But the 4 lines of code that achieve this improvement I leave as an exercise for the reader.

karlphillip
  • 92,053
  • 36
  • 243
  • 426
  • Please upvote all the answers that helped you. You can also click on the checkbox near an answer to select it as the official problem solver. By doing these things you are helping keep Stackoverflow organized and make it easier for other visitors in the future to find their answers. – karlphillip Feb 05 '20 at 13:20
  • Looking at it now, there is a couple of improvements that can be made to find the reel area with less steps. But that's a conversation for another day. – karlphillip Feb 05 '20 at 13:23
  • Thanks, but its very less accurate. Is there anything that can be used to make this work in python.? – laura Feb 06 '20 at 05:31