Detect different shapes in noisy binary image - python

I want to detect the circle and the five squares in this image:
This is the relevant part of the code I currently use:
# detect shapes in black-white RGB formatted cv2 image
def detect_shapes(img, approx_poly_accuracy=APPROX_POLY_ACCURACY):
res_dict = {
"rectangles": [],
"squares": []
}
vis = img.copy()
shape = img.shape
height, width = shape[0], shape[1]
total_area = height * width
# Morphological closing: get rid of holes
# img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)))
# Morphological opening: get rid of extensions at the border of the objects
# img = cv2.morphologyEx(img, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (121, 121)))
img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# cv2.imshow('intermediate', img)
# cv2.waitKey(0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
logging.info("Number of found contours for shape detection: {0}".format(len(contours)))
# vis = img.copy()
# cv2.drawContours(vis, contours, -1, (0, 255, 0), 2)
cv2.imshow('vis', vis)
cv2.waitKey(0)
for contour in contours:
area = cv2.contourArea(contour)
if area < MIN_SHAPE_AREA:
logging.warning("Area too small: {0}. Skipping.".format(area))
continue
if area > MAX_SHAPE_AREA_RATIO * total_area:
logging.warning("Area ratio too big: {0}. Skipping.".format(area / total_area))
continue
approx = cv2.approxPolyDP(contour, approx_poly_accuracy * cv2.arcLength(contour, True), True)
cv2.drawContours(vis, [approx], -1, (0, 0, 255), 2)
la = len(approx)
# find the center of the shape
M = cv2.moments(contour)
if M['m00'] == 0.0:
logging.warning("Unable to compute shape center! Skipping.")
continue
x = int(M['m10'] / M['m00'])
y = int(M['m01'] / M['m00'])
if la < 3:
logging.warning("Invalid shape detected! Skipping.")
continue
if la == 3:
logging.info("Triangle detected at position {0}".format((x, y)))
elif la == 4:
logging.info("Quadrilateral detected at position {0}".format((x, y)))
if approx.shape != (4, 1, 2):
raise ValueError("Invalid shape before reshape to (4, 2): {0}".format(approx.shape))
approx = approx.reshape(4, 2)
r_check, data = check_rect_or_square(approx)
blob_data = {"position": (x, y), "approx": approx}
blob_data.update(data)
if r_check == 2:
res_dict["squares"].append(blob_data)
elif r_check == 1:
res_dict["rectangles"].append(blob_data)
elif la == 5:
logging.info("Pentagon detected at position {0}".format((x, y)))
elif la == 6:
logging.info("Hexagon detected at position {0}".format((x, y)))
else:
logging.info("Circle, ellipse or arbitrary shape detected at position {0}".format((x, y)))
cv2.drawContours(vis, [contour], -1, (0, 255, 0), 2)
cv2.imshow('vis', vis)
cv2.waitKey(0)
logging.info("res_dict: {0}".format(res_dict))
return res_dict
The problem is: if I set the approx_poly_accuracy parameter too high, the circle is detected as a polygon (Hexagon or Octagon, for example). If I set it too low, the squares are not detected as squares, but as Pentagons, for example:
The red lines are the approximated contours, the green lines are the original contours. The text is detected as a completely wrong contour, it should never be approximated to this level (I don't care about the text so much, but if it is detected as a polygon with less than 5 vertices, it will be a false positive).
For a human, it is obvious that the left object is a circle and that the five objects on the right are squares, so there should be a way to make the computer realize that with high accuracy too. How do I modify this code to properly detect all objects?
What I already tried:
Apply filters like MedianFilter. It made things worse, because the rounded edges of the squares promoted them being detected as a polygon with more than four vertices.
Variate the approx_poly_accuracy parameter. There is no value that fits my purposes, considering that some other images might even have a some more noise.
Find an implementation of the RDP algorithm that allows me to specify an EXACT number of output points. This would allow me to to compute the suggested polygons for a certain number of points (for example in the range 3..10) and then calculate (A_1 + A_2) / A_common - 1 to use the area instead of the arc length as an accuracy, which would probably lead to a better result. I have not yet found a good implementation for that. I will now try to use a numerical solver method to dynamically figure out the correct epsilon parameter for RDP. The approach is not really clean and efficient though. I will post the results here as soon as available. If someone has a better approach, please let me know.

A possible approach would involve the calculation of some blob descriptors and filter blobs according to those properties. For example, you can compute the blob's aspect ratio, the (approximated) number of vertices and area. The steps are very straightforward:
Load the image and convert it to grayscale.
(Invert) Threshold the image. Let’s make sure the blobs are colored in white.
Get the binary image’s contours.
Compute two features: aspect ratio and number of vertices
Filter the blobs based on those features
Let’s see the code:
# Imports:
import cv2
import numpy as np
# Load the image:
fileName = "yh6Uz.png"
path = "D://opencvImages//"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# Prepare a deep copy of the input for results:
inputImageCopy = inputImage.copy()
# Grayscale conversion:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Threshold via Otsu:
_, binaryImage = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# Find the blobs on the binary image:
contours, hierarchy = cv2.findContours(binaryImage, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# Store the bounding rectangles here:
circleData = []
squaresData = []
Alright. So far, I’ve loaded, thresholded and computed contours on the input image. Additionally, I’ve prepared two lists to store the bounding box of the squares and the circle. Let’s create the feature filter:
for i, c in enumerate(contours):
# Get blob perimeter:
currentPerimeter = cv2.arcLength(c, True)
# Approximate the contour to a polygon:
approx = cv2.approxPolyDP(c, 0.04 * currentPerimeter, True)
# Get polygon's number of vertices:
vertices = len(approx)
# Get the polygon's bounding rectangle:
(x, y, w, h) = cv2.boundingRect(approx)
# Compute bounding box area:
rectArea = w * h
# Compute blob aspect ratio:
aspectRatio = w / h
# Set default color for bounding box:
color = (0, 0, 255)
I loop through each contour and calculate the current blob’s perimeter and polygon approximation. This info is used to approximately compute the blob vertices. The aspect ratio calculation is very easy. I first get the blob’s bounding box and get its dimensions: top left corner (x, y), width and height. The aspect ratio is just the width divided by the height.
The squares and the circle a very compact. These means that their aspect ratio should be close to 1.0. However, the squares have exactly 4 vertices, while the (approximated) circle has more. I use this info to build a very basic feature filter. It first checks aspect ratio, area and then number of vertices. I use the difference between the ideal feature and the real feature. The parameter delta adjusts the filter tolerance. Be sure to also filter tiny blobs, use the area for this:
# Set minimum tolerable difference between ideal
# feature and actual feature:
delta = 0.15
# Set the minimum area:
minArea = 400
# Check features, get blobs with aspect ratio
# close to 1.0 and area > min area:
if (abs(1.0 - aspectRatio) < delta) and (rectArea > minArea):
print("Got target blob.")
# If the blob has 4 vertices, it is a square:
if vertices == 4:
print("Target is square")
# Save bounding box info:
tempTuple = (x, y, w, h)
squaresData.append(tempTuple)
# Set green color:
color = (0, 255, 0)
# If the blob has more than 6 vertices, it is a circle:
elif vertices > 6:
print("Target is circle")
# Save bounding box info:
tempTuple = (x, y, w, h)
circleData.append(tempTuple)
# Set blue color:
color = (255, 0, 0)
# Draw bounding rect:
cv2.rectangle(inputImageCopy, (int(x), int(y)), (int(x + w), int(y + h)), color, 2)
cv2.imshow("Rectangles", inputImageCopy)
cv2.waitKey(0)
This is the result. The squares are identified with a green rectangle and the circle with a blue one. Additionally, the bounding boxes are stored in squaresData and circleData respectively:

Related

Recognizing corner's page with openCV partialy fails

I would like to get the 4 corners of a page,
The steps I took:
Converted to grayscale
Applied threshold the image
Applied Canny for detecting edges
After that I have used findContours
Draw the approx polygon for each polygon, my assumption was the relevant polygon must have 4 vertices.
but along the way I found out my solution sometimes misses,
apparently my solution is not robust enough (probably a bit a naive solution).
I think some of the reasons for those paper corner detection failure are:
The thresholds are picked manually for canny detection.
The same about the epsilon value for approxPolyDP
My Code
import cv2
import numpy as np
image = cv2.imread('page1.jpg')
descalingFactor = 3
imgheight, imgwidth = image.shape[:2]
resizedImg = cv2.resize(image, (int(imgwidth / descalingFactor), int(imgheight / descalingFactor)),
interpolation=cv2.INTER_AREA)
cv2.imshow(winname="original", mat=resizedImg)
cv2.waitKey()
gray = cv2.cvtColor(resizedImg, cv2.COLOR_BGR2GRAY)
cv2.imshow(winname="gray", mat=gray)
cv2.waitKey()
img_blur = cv2.GaussianBlur(gray, (5, 5), 1)
cv2.imshow(winname="blur", mat=img_blur)
cv2.waitKey()
canny = cv2.Canny(gray,
threshold1=120,
threshold2=255,
edges=1)
cv2.imshow(winname="Canny", mat=canny)
cv2.waitKey()
contours, _ = cv2.findContours(image=canny, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_SIMPLE)
contours = sorted(contours, key=cv2.contourArea, reverse=True)
for idx, cnt in enumerate(contours):
# print("Contour #", idx)
# print("Contour #", idx, " len(cnt): ", len(cnt))
cv2.drawContours(image=resizedImg, contours=[cnt], contourIdx=0, color=(255, 0, 0), thickness=3)
cv2.imshow(winname="contour" + str(idx), mat=resizedImg)
conv = cv2.convexHull(cnt)
epsilon = 0.1 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
cv2.drawContours(resizedImg, [approx], 0, (0, 0, 255), 3)
cv2.waitKey(0)
if len(approx) == 4:
print("found the paper!!")
break
pts = np.squeeze(approx)
Another approach
I was wondering wouldn't it be a better approach to fit a polygon with 4 vertices (Quadrilateral) to the contour , and then check if the area difference between the polygon to the contour is below a specified threshold.
Can somebody please suggest a more robust solution (demonstrating it with code), thank you.
The images:
image1: https://ibb.co/K2SqLwZ
image2: https://ibb.co/mbGFsNp
image3: https://ibb.co/m6QKkzw
image4: https://ibb.co/xh7W41V
As fmw42 suggested, you need to restrict the problem more. There are way too many variables to build a "works under all circumstances" solution. A possible, very basic, solution would be to try and get the convex hull of the page.
Another, more robust approach, would be to search for the four vertices of the corners and extrapolate lines to approximate the paper edges. That way you don't need perfect, clean edges, because you would reconstruct them using the four (maybe even three) corners.
To find the vertices you can run Hough Line detector or a Corner Detector on the edges and get at least four discernible clusters of end/starting points. From that you can average the four clusters to get a pair of (x, y) points per corner and extrapolate lines using those points.
That solution would be hypothetical and pretty laborious for a Stack Overflow question, so let me try the first proposal - detection via convex hull. Here are the steps:
Threshold the input image
Get edges from the input
Get the external contours of the edges using a minimum area filter
Get the convex hull of the filtered image
Get the corners of the convex hull
Let's see the code:
# imports:
import cv2
import numpy as np
# image path
path = "D://opencvImages//"
fileName = "img2.jpg"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# Deep copy for results:
inputImageCopy = inputImage.copy()
# Convert BGR to grayscale:
grayInput = cv2.cvtColor(inputImageCopy, cv2.COLOR_BGR2GRAY)
# Threshold via Otsu:
_, binaryImage = cv2.threshold(grayInput, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
The first step is to get a binary image, very straightforward. This is the result if you threshold via Otsu:
It is never a good idea to try and segment an object from a textured (or high frequency) background, however, in this case the paper it is discernible in the image histogram and the binary image is reasonably good. Let's try and detect edges on this image, I'm applying Canny with the same parameters as your code:
# Get edges:
cannyImage = cv2.Canny(binaryImage, threshold1=120, threshold2=255, edges=1)
Which produces this:
Seems good enough, the target edges are mostly present. Let's detect contours. The idea is to set an area filter, because the target contour is the biggest amongst the rest. I (heuristically) set a minimum area of 100000 pixels. Once the target contour is found I get its convex hull, like this:
# Find the EXTERNAL contours on the binary image:
contours, hierarchy = cv2.findContours(cannyImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Store the corners:
cornerList = []
# Look for the outer bounding boxes (no children):
for i, c in enumerate(contours):
# Approximate the contour to a polygon:
contoursPoly = cv2.approxPolyDP(c, 3, True)
# Convert the polygon to a bounding rectangle:
boundRect = cv2.boundingRect(contoursPoly)
# Get the bounding rect's data:
rectX = boundRect[0]
rectY = boundRect[1]
rectWidth = boundRect[2]
rectHeight = boundRect[3]
# Estimate the bounding rect area:
rectArea = rectWidth * rectHeight
# Set a min area threshold
minArea = 100000
# Filter blobs by area:
if rectArea > minArea:
# Get the convex hull for the target contour:
hull = cv2.convexHull(c)
# (Optional) Draw the hull:
color = (0, 0, 255)
cv2.polylines(inputImageCopy, [hull], True, color, 2)
You'll notice I've prepared beforehand a list (cornerList) in which I'll store (hopefully) all the corners. The last two lines of the previous snippet are optional, they draw the convex hull via cv2.polylines, this would be the resulting image:
Still inside the loop, after we compute the convex hull, we will get the corners via cv2.goodFeaturesToTrack, which implements a Corner Detector. The function receives a binary image, so we need to prepare a black image with the convex hull points drawn in white:
# Create image for good features to track:
(height, width) = cannyImage.shape[:2]
# Black image same size as original input:
hullImg = np.zeros((height, width), dtype =np.uint8)
# Draw the points:
cv2.drawContours(hullImg, [hull], 0, 255, 2)
cv2.imshow("hullImg", hullImg)
cv2.waitKey(0)
This is the image:
Now, we must set the corner detector. It needs the number of corners you are looking for, a minimum "quality" parameter that discards poor points detected as "corners" and a minimum distance between the corners. Check out the documentation for more parameters. Let's set the detector, it will return an array of points where it detected a corner. After we get this array, we will store each point in our cornerList, like this:
# Set the corner detection:
maxCorners = 4
qualityLevel = 0.01
minDistance = int(max(height, width) / maxCorners)
# Get the corners:
corners = cv2.goodFeaturesToTrack(hullImg, maxCorners, qualityLevel, minDistance)
corners = np.int0(corners)
# Loop through the corner array and store/draw the corners:
for c in corners:
# Flat the array of corner points:
(x, y) = c.ravel()
# Store the corner point in the list:
cornerList.append((x,y))
# (Optional) Draw the corner points:
cv2.circle(inputImageCopy, (x, y), 5, 255, 5)
cv2.imshow("Corners", inputImageCopy)
cv2.waitKey(0)
Additionally you can draw the corners as circles, it will yield this image:
This is the same algorithm tested on your third image:

HoughCircles circle detection not working?

I have been trying to write a program that can detect circles on my screen.
This is my screen before code processing
As you can see on the image, there are three circles that the code should detect. I am using HoughCircles function from OpenCV library to achieve this task. My code is below.
ss = gui.screenshot()
img = cv2.cvtColor(np.array(ss), cv2.COLOR_RGB2BGR)
output = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1.2, 100)
if circles is not None:
print("circles found", len(circles))
circles = np.round(circles[0, :]).astype("int")
for (x, y, r) in circles:
cv2.circle(output, (x, y), r, (0, 255, 0), 4)
cv2.rectangle(output, (x - 5, y - 5), (x + 5, y + 5), (0, 128, 255), -1)
cv2.imshow("output", np.hstack([gray, output]))
cv2.waitKey(0)
cv2.imshow("output", gray)
cv2.waitKey(0)
I am first taking screenshot of my screen. Then, I convert it to use it for opencv.
However, this code does not detect any circles for the screenshot shown in the first picture. I know this because when ran, my program does not print "circles found". Moreover, to show that I have been taking screenshots and transforming them to grayscale properly, I have this image taken from the last two lines of my code.
picture in a gray scale
To show that my code works with other circle images, here is a picture of a regular circle:
before detection
after detection
Any help would be very appreciated!
Here's an alternative solution to detect the circles without using the Hough Transform. As your input image has a very distinct blue hue to the blobs of interest, you can try to create a segmentation mask based on their HSV values. Then, detect contours and approximate each contour using a circle. The last step can be implemented using the cv2.minEnclosingCircle, which, as its name suggest, can compute the Minimum Enclosing Circle of a contour.
Let's see the code:
# image path
path = "D://opencvImages//"
fileName = "XUzFw.png"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# Create a deep copy of the input for results:
inputImageCopy = inputImage.copy()
# Convert the image to the HSV color space:
hsvImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2HSV)
# Set the HSV values:
lowRange = np.array([78, 0, 158])
uppRange = np.array([125, 255, 255])
# Create the HSV mask
mask = cv2.inRange(hsvImage, lowRange, uppRange)
This generates the following segmentation mask:
As you can see, the only blobs that remain are the circles. Now, let's compute the contours and find the minimum enclosing circle:
# Find the circle blobs on the binary mask:
contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Use a list to store the center and radius of the target circles:
detectedCircles = []
# Look for the outer contours:
for i, c in enumerate(contours):
# Approximate the contour to a circle:
(x, y), radius = cv2.minEnclosingCircle(c)
# Compute the center and radius:
center = (int(x), int(y))
radius = int(radius)
# Draw the circles:
cv2.circle(inputImageCopy, center, radius, (0, 0, 255), 2)
# Store the center and radius:
detectedCircles.append([center, radius])
# Let's see the results:
cv2.namedWindow("Circles", cv2.WINDOW_NORMAL)
cv2.imshow("Circles", inputImageCopy)
cv2.waitKey(0)
This is the result of the detection:
Additionally, you can check out the data stored in the detectedCircles list:
# Check out the detected circles:
for i in range(len(detectedCircles)):
# Get circle data:
center, r = detectedCircles[i]
# Print it:
print("i: "+str(i)+" x: "+str(center[0])+" y: "+str(center[1])+" r: "+str(r))
Which yields:
i: 0 x: 395 y: 391 r: 35
i: 1 x: 221 y: 391 r: 36
i: 2 x: 567 y: 304 r: 35
These are the parameters of houghCircles that works for me. You should also consider running a gaussian blur over the image before trying to find the circles.
I'm not a huge fan of houghCircles. I find it to be really finicky and I don't like how much of what it does is hidden inside the function. It makes tuning it mostly trial-and-error. These parameters work for this particular image, but I wouldn't count on this continuing to work under different lighting conditions or for different colors.
import cv2
import numpy as np
# load image
img = cv2.imread("spheres.png");
# grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY);
gray = cv2.GaussianBlur(gray,(5,5),0);
# circles
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp = 1, minDist = 100, param1=65, param2=20, minRadius=20, maxRadius=50)
# draw circles
if circles is not None:
# round to ints
circles = np.uint16(np.around(circles));
for circle in circles[0, :]:
# unpack and draw
x, y, radius = circle;
center = (x,y);
cv2.circle(img, center, radius, (255, 0, 255), 3);
# show
cv2.imshow("Image", img);
cv2.imshow("Gray", gray);
cv2.waitKey(0);

How to order order cv2 contours in a grid which may have some distortion?

I have wrote a python file to detect contours in a cv2 grid and order them by going down the columns from left to right. (See grid1 image below).
This is fairly trivial to sort I have pulled the top left corner of the contour and sorted by its x then by its y coordinate then use the sorted corners to sort the contour list. This works fine when the grid is perfectly straight.
Now if the grid has distortion then this no longer works looking at grid2 we can see that the x coordinate of the top left corner of the piece labelled 2 is less than x coordinate of the topleft corner of the piece labelled 1 (as shown by the green line).
Hence when I apply my sorting function that worked for grid1 it sorts by x then y and consequently the piece labelled 2 is incorrectly ordered to be the first element of the sorted contours instead of the second which it should be.
I am looking for a good method to sort both cases correctly.
Anyone have a suggestion(s)?
You may based your ordering selection based both on the distance of a corner for the origin, and the relative corner position.
Find contours and hierarchy.
Keep contours with no child (based on hierarchy).
Find corners of bounding rectangles.
Analyze the corners based on the following conditions (or find more simple conditions):
Top left contours is the one with the minimum distance of top left corner.
Bottom right contours is the one with the maximum distance of top left corner.
Other two contours can be separated by maximum x and maximum y (after eliminating the top left and bottom right).
The solution below, draws bounding rectangles in colors for testing:
Red
Green
Blue
Yellow
Here is a working code sample (please read the comments):
import numpy as np
import cv2
# Read input image as Grayscale
img = cv2.imread('img.png', cv2.IMREAD_GRAYSCALE)
# Convert img to uint8 binary image with values 0 and 255
# All black pixels goes to 0, and other pixels goes to 255
ret, thresh_gray = cv2.threshold(img, 1, 255, cv2.THRESH_BINARY)
# Find contours in thresh_gray.
cnts, hiers = cv2.findContours(thresh_gray, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)[-2:] # [-2:] indexing takes return value before last (due to OpenCV compatibility issues).
corners = [] # List of corners
dist = np.array([]) # Array of distance from axes origin
# Iterate cnts and hiers, find bounding rectangles, and add corners to a list
for c, h in zip(cnts, hiers[0]):
# If contours has no child
if h[2] == -1:
# Get bounding rectangle
x, y, w, h = cv2.boundingRect(c)
# Append corner to list of corners - format is corners[i] holds a tuple: ((x0, y0), (x1, y1))
p0 = (x, y)
p1 = (x+w, y+h)
corners.append((p0, p1))
# Distance of corners from origin
d = np.array([np.linalg.norm(p0), np.linalg.norm(p1)])
if dist.size == 0:
dist = d
else:
dist = np.vstack((dist, d))
top_left = np.argmin(dist[:,0]) # Index of top left corner (assume minimum distance from origin)
bottom_right = np.argmax(dist[:,1]) # Index of top bottom right corner (assume maximum distance from origin)
tmp_corners = np.array(corners)
tmp_corners[top_left, :, :] = np.array(((0,0), (0,0))) #Ignore top_left corners
tmp_corners[bottom_right, :, :] = np.array(((0,0), (0,0))) #Ignore bottom_right corners
bottom_left = np.argmax(tmp_corners[:,1,1]) #Maximum y is bottom left
tmp_corners[bottom_left, :, :] = np.array(((0,0), (0,0))) #Ignore bottom_left corners
top_right = np.argmax(tmp_corners[:,1,0]) #Maximum x is top right
# Convert Grayscale to BGR (just for testing - for drawing rectangles in green color).
out = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
# Draw rectangles (for testing)
# 1. Red
# 2. Green
# 3. Blue
# 4. Yellow
cv2.rectangle(out, corners[top_left][0], corners[top_left][1], (0, 0, 255), thickness = 2)
cv2.rectangle(out, corners[bottom_left][0], corners[bottom_left][1], (0, 255, 0), thickness = 2)
cv2.rectangle(out, corners[top_right][0], corners[top_right][1], (255, 0, 0), thickness = 2)
cv2.rectangle(out, corners[bottom_right][0], corners[bottom_right][1], (0, 255, 255), thickness = 2)
cv2.imwrite('out.png', out) #Save out to file (for testing).
# Show result (for testing).
cv2.imshow('thresh_gray', thresh_gray)
cv2.imshow('out', out)
cv2.waitKey(0)
cv2.destroyAllWindows()
Result:

Convert ellipse in image to circle (warp ellipse to circle like a polygon warping to rectangle)

I have an image of an ellipse and if an image has ellipse i am finding it using findcontours() and then i want to convert this ellipse to a circle.
see the example
and i want transform each of them to like this
First I have applied canny edge detection.Then on this image findcontour() is applied.
I have found the ellipse using findcontours() to get all contours and get the required elliptical contour and then the i am using fitellipse() to get center, rotation angle and major and minor axis of ellipse.
I have then tried to rotate the image by the rotated angle and then scale height and width of image w.r.t minor and major axis(i.e. making major axis and minor axis length same) but also I am not getting the proper circular object image as above. There will be some rotation left/it will still be like an ellipse which will be near to circle or so.
_, contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
if len(c) >= 5:
a = cv2.fitEllipse(c)
(x, y), (MA, ma), angle = a
area = (math.pi * MA * ma)/4
if abs(x-image.shape[0]/2) <= 2 and abs(y-image.shape[1]/2) <= 2 and (area - cv2.contourArea(c)) < 50:
screenCount = c
width, height = MA, ma
centerX, centerY = x, y
ellipseAngle = angle
print(width, height, centerX, centerY, ellipseAngle)
# cv2.drawContours(img, c, -1, (0, 255, 0), 4)
cv2.ellipse(img, a, (0, 0, 255), 2, 8)
cv2.imshow("ellipse", img)
break
img = image.copy()
if ellipseAngle < 90:
rotatedImg = imutils.rotate(img, ellipseAngle)
else:
rotatedImg = imutils.rotate(img, -(ellipseAngle - 90))
Then i have scaled as per major and minor axis
after applying findcontour() i got these 2 contours for 1st image in the post
from these any contour is fine right? i am using the first contour from countour as per the code and fitellipse() gives me this ellipse
EDITED - If there are any better approach to solve this problem it would be helpful.
There are several issues I can see in the code:
You are using an edge detection algorithm, and getting the contours of the result. This is OK in principle, but it leads to an image that has two contours: one for the inner edge and one for the outer edge of the edge detection result. It is easier to just threshold the image and obtain a single edge. Though if the image gets more complex an edge detection might be relevant. Indeed, any one of the two contours you obtain should be useful.
The line if abs(x-image.shape[0]/2) <= 2 and abs(y-image.shape[1]/2) <= 2 and (area - cv2.contourArea(c)) < 50 is very restrictive, it didn't trigger for the second image for me.
Rotating by -(ellipseAngle - 90) if the angle is negative is strange. You should rotate all ellipses in the same way.
The code below produces a circular output for both ellipse images shown in the question. I think that the ellipse parameters determined this way are not very precise, but it looks like they're good enough for this application.
import cv2
import numpy as np
img = cv2.imread('im1.png',0)
_, thresh = cv2.threshold(img, 128, 255, type=cv2.THRESH_BINARY_INV)
_, contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
params = cv2.fitEllipse(contours[0])
angle = params[2]
scale = params[1]
scale = scale[0]/scale[1]
M = cv2.getRotationMatrix2D((img.shape[0]/2, img.shape[1]/2), angle, 1)
# Let's add the scaling too:
M[:,0:2] = np.array([[1,0],[0,scale]]) # M[:,0:2]
M[1,2] = M[1,2] * scale # This moves the ellipse so it doesn't end up outside the image (it's not correct to keep the ellipse in the middle of the image)
out = cv2.warpAffine(img, M, img.shape, borderValue=255)
cv2.imshow('out',out)
cv2.waitKey()
Using PyDIP (I'm an author) you can get a more precise measure in the idealized case of the OP by not thresholding, and using the grey-values around the edges of the ellipse to get a more precise fit. We compute the second order central moments of the image, and derive the ellipse parameters from those. It is important here that the background is exactly 0, and that the foreground (ellipse pixels) are uniform in intensity except at the edge, where the intermediate grey-values add information about the sub-pixel location of the edge.
import PyDIP as dip
import numpy as np
img = -dip.ImageRead('im1.png').TensorElement(0) # We use the inverted first channel
params = dip.Moments(img).secondOrder
M = np.array([[params[0],params[2]],[params[2],params[1]]])
d, V = np.linalg.eig(M)
d = np.sqrt(d)
scale = d[0]/d[1]
angle = np.arctan2(V[1,0],V[0,0])
img = dip.Rotation2D(img, -angle)
img = dip.Resampling(img, [scale, 1])
img.Show()

How to find the centre of these sometimes-overlapping circles

As part of a project I'm working on, I need to find the centre-point of some "blobs" in an image using OpenCV with Python.
I'm having a bit of trouble with it, and would truly appreciate any help or insight :)
My current method is to: get the contours of the images, overlay ellipses on those, use the blob detector to find the centre of each of these.
This works fairly well, but occasionally I have extraneous blobs that I need to ignore, and sometimes the blobs are touching each-other.
Here's an example of when it goes well:
Good source image:
After extracting contours:
With the blobs detected:
And when it goes poorly (you can see that it's incorrectly overlayed an ellipse over three blobs, and detected one that I don't want):
Bad source image:
After extracting contours:
With the blobs detected:
This is the code I currently use. I'm unsure of any other option.
def process_and_detect(img_path):
img = cv2.imread(path)
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 50, 150, 0)
im2, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
drawn_img = np.zeros(img.shape, np.uint8)
min_area = 50
min_ellipses = []
for cnt in contours:
if cv2.contourArea(cnt) >= min_area:
ellipse = cv2.fitEllipse(cnt)
cv2.ellipse(drawn_img,ellipse,(0,255,0),-1)
plot_img(drawn_img, size=12)
# Change thresholds
params = cv2.SimpleBlobDetector_Params()
params.filterByColor = True
params.blobColor = 255
params.filterByCircularity = True
params.minCircularity = 0.75
params.filterByArea = True
params.minArea = 150
# Set up the detector
detector = cv2.SimpleBlobDetector_create(params)
# Detect blobs.
keypoints = detector.detect(drawn_img)
for k in keypoints:
x = round(k.pt[0])
y = round(k.pt[1])
line_length = 20
cv2.line(img, (x-line_length, y), (x+line_length, y), (255, 0, 0), 2)
cv2.line(img, (x, y-line_length), (x, y+line_length), (255, 0, 0), 2)
plot_img(img, size=12)
Thank you so much for reading this far, I sincerely hope someone can help me out, or point me in the right direction. Thanks!
Blob detector
Currently, your implementation is redundant. From the SimpleBlobDetector() docs:
The class implements a simple algorithm for extracting blobs from an image:
Convert the source image to binary images by applying thresholding with several thresholds from minThreshold (inclusive) to maxThreshold (exclusive) with distance thresholdStep between neighboring thresholds.
Extract connected components from every binary image by findContours() and calculate their centers.
Group centers from several binary images by their coordinates. Close centers form one group that corresponds to one blob, which is controlled by the minDistBetweenBlobs parameter.
From the groups, estimate final centers of blobs and their radiuses and return as locations and sizes of keypoints.
So you're implementing part of the steps already, which might give some unexpected behavior. You could try playing with the parameters to see if you can figure out some that work for you (try creating trackbars to play with the parameters and get live results of your algorithm with different blob detector parameters).
Modifying your pipeline
However, you've already got most of your own pipeline written, so you can easily remove the blob detector and implement your own algorithm. If you simply drop your threshold a bit, you can easily get clearly marked circles, and then blob detection is as simple as contour detection. If you have a separate contour for each blob, then you can calculate the centroid of the contour with moments(). For example:
def process_and_detect(img_path):
img = cv2.imread(img_path)
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 100, 255, cv2.THRESH_BINARY)
contours = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[1]
line_length = 20
for c in contours:
if cv2.contourArea(c) >= min_area:
M = cv2.moments(c)
x = int(M['m10']/M['m00'])
y = int(M['m01']/M['m00'])
cv2.line(img, (x-line_length, y), (x+line_length, y), (255, 0, 0), 2)
cv2.line(img, (x, y-line_length), (x, y+line_length), (255, 0, 0), 2)
Getting more involved
This same pipeline can be used to automatically loop through threshold values so you don't have to guess and hardcode those values. Since the blobs all seem roughly the same size, you can loop through until all contours have roughly the same area. You could do this for e.g. by finding the median contour size, defining some percentage of that median size above and below that you'll allow, and checking if all the contours detected fit in those bounds.
Here's an animated gif of what I mean. Notice that the gif stops once the contours are separated:
Then you can simply find the centroids of those separated contours. Here's the code:
def process_and_detect(img_path):
img = cv2.imread(img_path)
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
for thresh_val in range(0, 255):
# threshold and detect contours
thresh = cv2.threshold(imgray, thresh_val, 255, cv2.THRESH_BINARY)[1]
contours = cv2.findContours(thresh,
cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[1]
# filter contours by area
min_area = 50
filtered_contours = [c for c in contours
if cv2.contourArea(c) >= min_area]
area_contours = [cv2.contourArea(c) for c in filtered_contours]
# acceptable deviation from median contour area
median_area = np.median(area_contours)
dev = 0.3
lowerb = median_area - dev*median_area
upperb = median_area + dev*median_area
# break when all contours are within deviation from median area
if ((area_contours > lowerb) & (area_contours < upperb)).all():
break
# draw center location of blobs
line_length = 8
cross_color = (255, 0, 0)
for c in filtered_contours:
M = cv2.moments(c)
x = int(M['m10']/M['m00'])
y = int(M['m01']/M['m00'])
cv2.line(img, (x-line_length, y), (x+line_length, y), cross_color, 2)
cv2.line(img, (x, y-line_length), (x, y+line_length), cross_color, 2)
Note that here I looped through all possible threshold values with range(0, 255) to give 0, 1, ..., 254 but really you could start higher and skip through a few values at a time with, say, range(50, 200, 5) to get 50, 55, ..., 195 which would of course be much faster.
The "standard" approach for such blob-splitting problem is by means of the watershed transform. It can be applied on the binary image, using a transform distance, or directly on the grayscale image.
Oversegmentation problems can make it tricky, but it seems that your case will not suffer from that.
To find the center, I would usually recommend a weighted average of the pixel coordinates to get a noise reduction effect, but in this case I would probably go for the location of the maximum intensity, which won't be influenced by the deformation of the shape.
Here is what you get with a grayscale watershed (the region intensity is the average). Contrary to what I initially thought, there is some fragmentation due to irregularities in the blobs
You can improve with a little of lowpass filtering before segmentation.

Categories