Detect width of objects in image - python

I have images like the following (these are the output of a UNET on the original image):
, ,
I thought segmenting the original image was the difficult task, but I have been struggling with the following task:
for each image I have to compute the width of the green area in correspondence with the red one, as follows:
, ,
What I tried to do:
Detecting the edgs of the green area was straightforward. Then I found the lines in polar coordinates through the Hough Transform:
image = cv2.imread("../../../../example_1.png")
image[:, :, 2] = 0
canny = cv2.Canny(image, 150, 250, 3)
lines = cv2.HoughLinesP(canny, cv2.HOUGH_PROBABILISTIC, np.pi/180, 20)
for line in lines:
for x0, y0, x1, y1 in line:
cv2.line(image, (x0,y0), (x1,y1), (255, 255, 255), 1)
fig = plt.figure(figsize = (15, 10))
fig.add_subplot(1, 2, 1).set_title("canny")
plt.imshow(canny, cmap = "gray")
fig.add_subplot(1, 2, 2).set_title("lines")
plt.imshow(image)
Which gives:
As you can see there are many candidates lines (many false positives) and I'm struggling to keep the ones I need. Furthermore: How would you calculate width?
Using houghlines in polar coordinates I was able to retrieve the angle each line has with the origin of the axis, so to calculate width I should find a pair of parallel lines (same angle or almost same angle) with different distance from the origin. I'm not sure this is the best approach.
Thanks for helping

As #Micka suggested you can use cv2.minAreaRect to do this. To do this you can apply simple thresholding on the two red and green planes followed by contour estimation. Then the largest contours can be taken and the smallest rectangle by area can be found. Using its coordinates the height and width can be calculated and then their ratio. The code is
img = cv2.imread('red_green.png')
red = img[:, :, 2] # to segment out red area
green = img[:, :, 1] # to segment out green are
ret, thresh1 = cv2.threshold(red, 5, 255, cv2.THRESH_BINARY)
ret, thresh2 = cv2.threshold(green, 5, 255, cv2.THRESH_BINARY)
_, cnts1, _ = cv2.findContours(thresh1,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
_, cnts2, _ = cv2.findContours(thresh2,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
c1 = max(cnts1, key = cv2.contourArea)
c2 = max(cnts2, key = cv2.contourArea)
rect1 = cv2.minAreaRect(c1)
rect2 = cv2.minAreaRect(c2)
box1 = cv2.boxPoints(rect1)
box2 = cv2.boxPoints(rect2)
box1 = np.int0(box1)
box2 = np.int0(box2)
cv2.drawContours(img, [box1], 0, (0, 255, 255), 2)
cv2.drawContours(img, [box2], 0, (0, 255, 255), 2)
(p1, p2, p3, p4) = box1 # Unpacking tuple
h1 = (((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)**0.5) # calculating width by calculating distance
w1 = (((p2[0]-p3[0])**2 + (p2[1]-p3[1])**2)**0.5) # calculating height by calculating distance
(p1, p2, p3, p4) = box2 # Unpacking tuple
h2 = (((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)**0.5) # calculating width by calculating distance
w2 = (((p2[0]-p3[0])**2 + (p2[1]-p3[1])**2)**0.5) # calculating height by calculating distance
rofh = h2/h1
rofw = w2/w1
print("ratio of height = ", rofh, "and ratio by width = ", rofw)
cv2.imshow('image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
The resulting image is

Related

Finding text between two lines using Python OpenCV

I want to identify and highlight / crop the text between two lines using Python (cv2).
One line is a wavy line at the top, and the second line somewhere in the page. This line can appear at any height on the page, ranging from just after 1 line to just before the last line.
An example,
I believe I need to use HoughLinesP() somehow with proper parameters for this.
I've tried some examples involving a combination of erode + dilate + HoughLinesP.
e.g.
img = cv2.imread(image)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
kernel_size = 5
blur_gray = cv2.GaussianBlur(gray, (kernel_size, kernel_size), 0)
# erode / dilate
erode_kernel_param = (5, 200) # (5, 50)
dilate_kernel_param = (5, 5) # (5, 75)
img_erode = cv2.erode(blur_gray, np.ones(erode_kernel_param))
img_dilate = cv2.dilate(img_erode, np.ones(dilate_kernel_param))
# %% Second, process edge detection use Canny.
low_threshold = 50
high_threshold = 150
edges = cv2.Canny(img_dilate, low_threshold, high_threshold)
# %% Then, use HoughLinesP to get the lines.
# Adjust the parameters for better performance.
rho = 1 # distance resolution in pixels of the Hough grid
theta = np.pi / 180 # angular resolution in radians of the Hough grid
threshold = 15 # min number of votes (intersections in Hough grid cell)
min_line_length = 600 # min number of pixels making up a line
max_line_gap = 20 # max gap in pixels between connectable line segments
line_image = np.copy(img) * 0 # creating a blank to draw lines on
# %% Run Hough on edge detected image
# Output "lines" is an array containing endpoints of detected line segments
lines = cv2.HoughLinesP(edges, rho, theta, threshold, np.array([]),
min_line_length, max_line_gap)
if lines is not None:
for line in lines:
for x1, y1, x2, y2 in line:
cv2.line(line_image, (x1, y1), (x2, y2), (255, 0, 0), 5)
# %% Draw the lines on the image
lines_edges = cv2.addWeighted(img, 0.8, line_image, 1, 0)
However, in many cases the lines dont get identified propery.
Some examples of errors being,
Too many lines being identified (ones in the text as well)
Lines not being identified completely
Lines not being identified at all
Am I on the right track? Do I just need to hit the correct combination of parameters for this purpose? or is there a simpler way / trick which will let me reliably crop the text between these two lines?
In case it's relevant, I need to do this for ~450 pages.
Here's the link to the book, in case someone wants to examine more examples of pages.
https://archive.org/details/in.ernet.dli.2015.553713/page/n13/mode/2up
Thank you.
Solution
I've made minor modifications to the answer by Ari (Thank you), and made the code a bit more comprehensible for my own sake, here's my code.
The core idea is,
Find contours and their bounding rectangles.
Two "widest" contours would represent the two lines.
Thereafter, take the lower side of the top rectangle and upper side of the bottom rectangle to bound the area (text) we are interested in.
for image in images:
base_img = cv2.imread(image)
height, width, channels = base_img.shape
img = cv2.cvtColor(base_img, cv2.COLOR_BGR2GRAY)
ret, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
img = cv2.bitwise_not(img)
contours, hierarchy = cv2.findContours(
img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
)
# Get rectangle bounding contour
rects = [cv2.boundingRect(contour) for contour in contours]
# Rectangle is (x, y, w, h)
# Top-Left point of the image is (0, 0), rightwards X, downwards Y
# Sort the contours bigger width first
rects.sort(key=lambda r: r[2], reverse=True)
# Get the 2 "widest" rectangles
line_rects = rects[:2]
line_rects.sort(key=lambda r: r[1])
# If at least two rectangles (contours) were found
if len(line_rects) >= 2:
top_x, top_y, top_w, top_h = line_rects[0]
bot_x, bot_y, bot_w, bot_h = line_rects[1]
# Cropping the img
# Crop between bottom y of the upper rectangle (i.e. top_y + top_h)
# and the top y of lower rectangle (i.e. bot_y)
crop_img = base_img[top_y+top_h:bot_y]
# Highlight the area by drawing the rectangle
# For full width, 0 and width can be used, while
# For exact width (erroneous) top_x and bot_x + bot_w can be used
rect_img = cv2.rectangle(
base_img,
pt1=(0, top_y + top_h),
pt2=(width, bot_y),
color=(0, 255, 0),
thickness=2
)
cv2.imwrite(image.replace('.jpg', '.rect.jpg'), rect_img)
cv2.imwrite(image.replace('.jpg', '.crop.jpg'), crop_img)
else:
print(f"Insufficient contours in {image}")
You can find the Contours, and then take the two with the biggest width.
base_img = cv2.imread('a.png')
img = cv2.cvtColor(base_img, cv2.COLOR_BGR2GRAY)
ret, img = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
img = cv2.bitwise_not(img)
cnts, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# sort the cnts bigger width first
cnts.sort(key=lambda c: cv2.boundingRect(c)[2], reverse=True)
# get the 2 big lines
lines = [cv2.boundingRect(cnts[0]), cv2.boundingRect(cnts[1])]
# higher line first
lines.sort(key=lambda c: c[1])
# croping the img
crop_img = base_img[lines[0][1]:lines[1][1]]

How can I find the center the hole with hough circles

For this image, I tried to use hough cirlce to find the center of the "black hole".
After playing with the parameters of cv2.HoughCircles for a long time, the following is the best I can get.
raw image:
# reproducible code for stackoverflow
import cv2
import os
import sys
from matplotlib import pyplot as plt
import numpy as np
# read image can turn it gray
img = cv2.imread(FILE)
cimg = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_gray = dst = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
plt.figure(figsize = (18,18))
plt.imshow(cimg, cmap = "gray")
# removing noises
element = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
closing = cv2.morphologyEx(y, cv2.MORPH_CLOSE, element, iterations = 7)
plt.figure(figsize = (12,12))
plt.imshow(closing, cmap = "gray")
# try to find the circles
circles = cv2.HoughCircles(closing,cv2.HOUGH_GRADIENT,3,50,
param1=50,param2=30,minRadius=20,maxRadius=50)
circles = np.uint16(np.around(circles))
for i in circles[0,:]:
# draw the outer circle
cv2.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)
# draw the center of the circle
cv2.circle(cimg,(i[0],i[1]),2,(0,0,255),3)
plt.figure(figsize = (12,12))
plt.imshow(cimg)
Update::
The one with Canny:
edges = cv2.Canny(closing, 100, 300)
plt.figure(figsize = (12,12))
plt.imshow(edges, cmap = "gray")
circles = cv2.HoughCircles(edges,cv2.HOUGH_GRADIENT,2,50,
param1=50,param2=30,minRadius=20,maxRadius=60)
circles = np.uint16(np.around(circles))
cimg = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
for i in circles[0,:]:
# draw the outer circle
cv2.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)
# draw the center of the circle
cv2.circle(cimg,(i[0],i[1]),2,(0,0,255),3)
plt.figure(figsize = (12,12))
plt.imshow(cimg)
Still not the right circle that is wanted.
Update:
#crackanddie
Sometimes there is 6 or 9 in the identity number.
The circle in 6 or 9 is not very round.
Is there any way to filter that out?
This is an alternative method if you do not want to implement or fiddle with Hough's parameters. You must be sure there's at least one circle visible in your picture. The idea is to create a segmentation mask based on the CMYK color space and filter the blobs of interest by circularity and area. These are the steps:
Convert the image from BGR to CMYK
Threshold the K channel to get a binary mask
Filter blobs by circularity and area
Approximate the filtered blobs as circles
I'm choosing the CMYK color space because the circle is mostly black. The K (key) channel (in this case - black) should do a good job of representing the blob of interest, albeit, with some noise - as usual. Let's see the code:
# Imports:
import cv2
import numpy as np
# image path
path = "D://opencvImages//"
fileName = "dyj3O.jpg"
# load image
bgr = cv2.imread(path + fileName)
Alright, we need to convert the image from BGR to CMYK. OpenCV does not offer the conversion, so we need to do it manually. The formula is very straightforward. I'm just interested on the K channel, so I just calculate it like this:
# Make float and divide by 255:
bgrFloat = bgr.astype(np.float) / 255.
# Calculate K as (1 - whatever is biggest out of bgrFloat)
kChannel = 1 - np.max(bgrFloat, axis=2)
# Convert back to uint 8:
kChannel = 255 * kChannel
kChannel = kChannel.astype(np.uint8)
Gotta keep en eye on the data types, because there are float operations going on. This is the result:
As you see, the hole is almost 100% white, that's cool, we can threshold this image via Otsu like this:
# Compute binary mask of the hole via Otsu:
_, binaryImage = cv2.threshold(kChannel, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
Which gives you this nice binary mask:
Now, here comes the laborious part. Let's find contours on this image. For every contour/blob compute circularity and area. Use this info to filter noise and get the contour of interest, keep in mind that a perfect circle should have circularity close to 1.0. Once you get a contour of interest, approximate a circle to it. This is the process:
# Find the big contours/blobs on the filtered image:
contours, hierarchy = cv2.findContours(binaryImage, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
# Store the detected circles here:
detectedCircles = []
# Look for the potential contours of interest:
for _, c in enumerate(contours):
# Get the blob's area and perimeter:
contourArea = cv2.contourArea(c)
contourPerimeter = cv2.arcLength(c, True)
# Compute circularity:
if contourPerimeter > 0:
circularity = (4 * 3.1416 * contourArea) / (pow(contourPerimeter, 2))
else:
circularity = 0.0
# Set the min threshold values to identify the
# blob of interest:
minCircularity = 0.7
minArea = 2000
if circularity >= minCircularity and contourArea >= minArea:
# Approximate the contour to a circle:
(x, y), radius = cv2.minEnclosingCircle(c)
# Compute the center and radius:
center = (int(x), int(y))
# Cast radius to in:
radius = int(radius)
# Store the center and radius:
detectedCircles.append([center, radius])
# Draw the circles:
cv2.circle(bgr, center, radius, (0, 255, 0), 2)
cv2.imshow("Detected Circles", bgr)
print("Circles Found: " + str(len(detectedCircles)))
Additionally, I have stored the circle (center and radius) in the detectedCircles list. This is the final result:
Circles Found: 1
Here it is:
import numpy as np
import cv2
def threshold_gray_const(image_, rang: tuple):
return cv2.inRange(image_, rang[0], rang[1])
def binary_or(image_1, image_2):
return cv2.bitwise_or(image_1, image_2)
def negate_image(image_):
return cv2.bitwise_not(image_)
def particle_filter(image_, power):
# Abdrakov's particle filter
nb_components, output, stats, centroids = cv2.connectedComponentsWithStats(image_, connectivity=8)
sizes = stats[1:, -1]
nb_components = nb_components - 1
min_size = power
img2 = np.zeros(output.shape, dtype=np.uint8)
for i in range(0, nb_components):
if sizes[i] >= min_size:
img_to_compare = threshold_gray_const(output, (i + 1, i + 1))
img2 = binary_or(img2, img_to_compare)
img2 = img2.astype(np.uint8)
return img2
def reject_borders(image_):
# Abdrakov's border rejecter
out_image = image_.copy()
h, w = image_.shape[:2]
for row in range(h):
if out_image[row, 0] == 255:
cv2.floodFill(out_image, None, (0, row), 0)
if out_image[row, w - 1] == 255:
cv2.floodFill(out_image, None, (w - 1, row), 0)
for col in range(w):
if out_image[0, col] == 255:
cv2.floodFill(out_image, None, (col, 0), 0)
if out_image[h - 1, col] == 255:
cv2.floodFill(out_image, None, (col, h - 1), 0)
return out_image
src = cv2.imread("your_image")
img_gray = dst = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
element = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
closing = cv2.morphologyEx(img_gray, cv2.MORPH_CLOSE, element, iterations=2)
tv, thresh = cv2.threshold(closing, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
neg = negate_image(thresh)
rej = reject_borders(neg)
filtered = particle_filter(rej, 300)
edges = cv2.Canny(filtered, 100, 200)
circles = cv2.HoughCircles(edges, cv2.HOUGH_GRADIENT, 3, 50, param1=50, param2=30, minRadius=20, maxRadius=50)
circles = np.uint16(np.around(circles))
for i in circles[0, :]:
# draw the outer circle
cv2.circle(src, (i[0], i[1]), i[2], (0, 255, 0), 2)
# draw the center of the circle
cv2.circle(src, (i[0], i[1]), 2, (0, 0, 255), 3)
cv2.imshow("closing", closing)
cv2.imshow("edges", edges)
cv2.imshow("out", src)
cv2.waitKey(0)
I changed cv2.morphologyEx parameters a bit, because they were too strong. And after this noise removing I made a binary image using cv2.THRESH_OTSU parameter, negated it, rejected borders and filtered a bit. Then I used cv2.Canny to find edges and this 'cannied' image I passed into cv2.HoughCircles. If any questions - ask me :)
If you want to use a "thinking out of the box" solution then check this solution out. Remember this might have a few false positives in some cases and would only work in cases where circle contour is complete or joined.
import numpy as np
import cv2
import matplotlib.pyplot as plt
from math import pi
pi_eps = 0.1
rgb = cv2.imread('/path/to/your/image/find_circle.jpg')
gray = cv2.cvtColor(rgb, cv2.COLOR_BGR2GRAY)
th = cv2.adaptiveThreshold(gray,255, cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY_INV,21,5)
contours, hier = cv2.findContours(th.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
out_img = rgb.copy()
for i in range(len(contours)):
x,y,w,h = cv2.boundingRect(contours[i])
ar = min(w,h)/max(w,h)
# For a circle aspect ratio is close to 1.0
# In your use case circle diameter is between 40px-100px
if ar < 0.9 or \
w < 40 or w > 100:
continue
# P = 2 * PI * r
perimeter = cv2.arcLength(contours[i], True)
if perimeter == 0:
continue
# Second level confirmation could be done using PI = P * P / (4 * A)
# A = PI * r * r
area = cv2.contourArea(contours[i])
if area == 0:
continue
# d = (w+h) / 2 average diameter
# A contour is a circle if (P / d) = PI
ctr_pi = perimeter / ((w+h) / 2)
if abs(ctr_pi - pi) < pi_eps * pi:
cv2.circle(out_img, (int(x+w/2), int(y+h/2)), int(max(w,h)/2), (0, 255, 0), 1)
print("Center of the circle: ", x + w/2, y+h/2)
plt.imshow(out_img)

OpenCV: using Canny and Shi-Tomasi to detect round corners of a playing card

I want to do some planar rectification, to convert from left to right:
I have the code to do the correction, but I need the 4 corner coords.
I'm using the following code to find them:
import cv2
image = cv2.imread('input.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
canny = cv2.Canny(gray, 120, 255, 1)
corners = cv2.goodFeaturesToTrack(canny,4,0.5,50)
for corner in corners:
x,y = corner.ravel()
cv2.circle(image,(x,y),5,(36,255,12),-1)
cv2.imshow("result", image)
cv2.waitKey()
It reads the image, and transforms it to grayscale + canny
But the resultant corners (found by cv2.goodFeaturesToTrack) aren't the desired ones:
I need the external corners of the card, any clue to achieve it?
Thanks
This is the input.png:
Update: Added four point perspective transform.
I have skipped perspective transform as the question is about finding right corners.
You can skip the loop by getting contour with maximum area then processing it. Some blurring may help it further. Press Esc button to get next image output.
Another useful method, how to find corners points of a shape in an image in opencv?
Ouput Images
Code
"""
Task: Detect card corners and fix perspective
"""
import cv2
import numpy as np
img = cv2.imread('resources/KSuVq.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(gray,127,255,0)
cv2.imshow('Thresholded original',thresh)
cv2.waitKey(0)
## Get contours
contours,h = cv2.findContours(thresh,cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
## only draw contour that have big areas
imx = img.shape[0]
imy = img.shape[1]
lp_area = (imx * imy) / 10
#################################################################
# Four point perspective transform
# https://www.pyimagesearch.com/2014/08/25/4-point-opencv-getperspective-transform-example/
#################################################################
def order_points(pts):
# initialzie a list of coordinates that will be ordered
# such that the first entry in the list is the top-left,
# the second entry is the top-right, the third is the
# bottom-right, and the fourth is the bottom-left
rect = np.zeros((4, 2), dtype = "float32")
# the top-left point will have the smallest sum, whereas
# the bottom-right point will have the largest sum
s = pts.sum(axis = 1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
# now, compute the difference between the points, the
# top-right point will have the smallest difference,
# whereas the bottom-left will have the largest difference
diff = np.diff(pts, axis = 1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
# return the ordered coordinates
return rect
def four_point_transform(image, pts):
# obtain a consistent order of the points and unpack them
# individually
rect = order_points(pts)
(tl, tr, br, bl) = rect
# compute the width of the new image, which will be the
# maximum distance between bottom-right and bottom-left
# x-coordiates or the top-right and top-left x-coordinates
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
maxWidth = max(int(widthA), int(widthB))
# compute the height of the new image, which will be the
# maximum distance between the top-right and bottom-right
# y-coordinates or the top-left and bottom-left y-coordinates
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
maxHeight = max(int(heightA), int(heightB))
# now that we have the dimensions of the new image, construct
# the set of destination points to obtain a "birds eye view",
# (i.e. top-down view) of the image, again specifying points
# in the top-left, top-right, bottom-right, and bottom-left
# order
dst = np.array([
[0, 0],
[maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1],
[0, maxHeight - 1]], dtype = "float32")
# compute the perspective transform matrix and then apply it
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
# return the warped image
return warped
#################################################################
## Get only rectangles given exceeding area
for cnt in contours:
approx = cv2.approxPolyDP(cnt,0.01 * cv2.arcLength(cnt, True), True)
## calculate number of vertices
#print(len(approx))
if len(approx) == 4 and cv2.contourArea(cnt) > lp_area:
print("rectangle")
tmp_img = img.copy()
cv2.drawContours(tmp_img, [cnt], 0, (0, 255, 255), 6)
cv2.imshow('Contour Borders', tmp_img)
cv2.waitKey(0)
tmp_img = img.copy()
cv2.drawContours(tmp_img, [cnt], 0, (255, 0, 255), -1)
cv2.imshow('Contour Filled', tmp_img)
cv2.waitKey(0)
# Make a hull arround the contour and draw it on the original image
tmp_img = img.copy()
mask = np.zeros((img.shape[:2]), np.uint8)
hull = cv2.convexHull(cnt)
cv2.drawContours(mask, [hull], 0, (255, 255, 255), -1)
cv2.imshow('Convex Hull Mask', mask)
cv2.waitKey(0)
# Draw minimum area rectangle
tmp_img = img.copy()
rect = cv2.minAreaRect(cnt)
box = cv2.boxPoints(rect)
box = np.int0(box)
cv2.drawContours(tmp_img, [box], 0, (0, 0, 255), 2)
cv2.imshow('Minimum Area Rectangle', tmp_img)
cv2.waitKey(0)
# Draw bounding rectangle
tmp_img = img.copy()
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(tmp_img, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.imshow('Bounding Rectangle', tmp_img)
cv2.waitKey(0)
# Bounding Rectangle and Minimum Area Rectangle
tmp_img = img.copy()
rect = cv2.minAreaRect(cnt)
box = cv2.boxPoints(rect)
box = np.int0(box)
cv2.drawContours(tmp_img, [box], 0, (0, 0, 255), 2)
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(tmp_img, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.imshow('Bounding Rectangle', tmp_img)
cv2.waitKey(0)
# determine the most extreme points along the contour
# https://www.pyimagesearch.com/2016/04/11/finding-extreme-points-in-contours-with-opencv/
tmp_img = img.copy()
extLeft = tuple(cnt[cnt[:, :, 0].argmin()][0])
extRight = tuple(cnt[cnt[:, :, 0].argmax()][0])
extTop = tuple(cnt[cnt[:, :, 1].argmin()][0])
extBot = tuple(cnt[cnt[:, :, 1].argmax()][0])
cv2.drawContours(tmp_img, [cnt], -1, (0, 255, 255), 2)
cv2.circle(tmp_img, extLeft, 8, (0, 0, 255), -1)
cv2.circle(tmp_img, extRight, 8, (0, 255, 0), -1)
cv2.circle(tmp_img, extTop, 8, (255, 0, 0), -1)
cv2.circle(tmp_img, extBot, 8, (255, 255, 0), -1)
print("Corner Points: ", extLeft, extRight, extTop, extBot)
cv2.imshow('img contour drawn', tmp_img)
cv2.waitKey(0)
#cv2.destroyAllWindows()
## Perspective Transform
tmp_img = img.copy()
pts = np.array([extLeft, extRight, extTop, extBot])
warped = four_point_transform(tmp_img, pts)
cv2.imshow("Warped", warped)
cv2.waitKey(0)
cv2.destroyAllWindows()
References
https://docs.opencv.org/4.5.0/dd/d49/tutorial_py_contour_features.html
https://www.pyimagesearch.com/2016/04/11/finding-extreme-points-in-contours-with-opencv/
https://www.pyimagesearch.com/2014/08/25/4-point-opencv-getperspective-transform-example/
Canny is a tool for edge detection, and if correctly tuned it does what it says on the tin.
Once you get the edges, you must define what a corner is. For instance, is it a sharp turn in a edge?
You'd like to use the function cv2.goodFeaturesToTrack, which is supposed to be a corner detection tool, but once again, what is a corner? It uses the Shi-Tomasi algorithm to find the N "best" corners in an image, which is just a threshold, and some minimum distance between points.
In the end, it is guaranteed to almost never bear the four corners you want. You should try these alternatives, and stick with the best option:
try to get more corners and geometrically determine the four "outmost" ones.
combine your method with some other transformation, or object-matching. For instance, if you are looking for a rectangular-ish image, try to match it against a template, compute the transform matrix and resolve edges after transformation.
use a different edge detection method, or a combination of methods.
Note that a card doesn't have sharp corners like a piece of paper, so you'll end up cropping the card or skewing it if using any "corner" on the rounded edges, or trying to locate an edge outside the actual "white" of the card, to avoid the skew (try to inscribe the card into a sharp-edge rectangle) - note that Canny is not effective in this case.
Here is one way to find the corners in Python OpenCV. I note this is more complicates since the green dots on the input complicate the issue and they likely would not be in the input image. One could simply threshold on the green dots using cv2.inRange() to find the green dots. But I will assume this is not really what you want.
- Read the input
- Convert to gray
- Threshold
- Get the largest contour and draw it on the input
- Reduce the number of vertices in the contour as a polygon and draw the polygon on the input.
- The polygon has 5 vertices and two are virtually the same. Normally, one would get 4 verices if the green dots were not there. So draw a white filled polygon on a black background.
- Get the corners from the white polygon on black background and draw on these vertices
- Save the results
Input:
import cv2
import numpy as np
import time
# load image
img = cv2.imread("hello.png")
# convert to gray
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# threshold
thresh = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)[1]
# get the largest contour
contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
big_contour = max(contours, key=cv2.contourArea)
peri = cv2.arcLength(big_contour, True)
# draw contour on input in red
result = img.copy()
result2 = np.zeros_like(img)
cv2.drawContours(result, [big_contour], 0, (0,0,255), 1)
cv2.drawContours(result2, [big_contour], 0, (0,0,255), 1)
# reduce to fewer vertices on polygon
poly = cv2.approxPolyDP(big_contour, 0.1 * peri, False)
# draw polygon on input in green
cv2.polylines(result, [poly], False, (0,255,0), 1)
cv2.polylines(result2, [poly], False, (0,255,0), 1)
# list polygon points
print("Polygon Points:")
for p in poly:
px = p[0][0]
py = p[0][1]
print(px,py)
print('')
# draw white filled polygon on black background
result3 = np.zeros_like(thresh)
cv2.fillPoly(result3,[poly],255)
# get corners
corners = cv2.goodFeaturesToTrack(result3,4,0.01,50,useHarrisDetector=True,k=0.04)
# print corner coords and draw circles
result3 = cv2.merge([result3,result3,result3])
print("Corners:")
for c in corners:
x,y = c.ravel()
print(int(x), int(y))
cv2.circle(result3,(x,y),3,(0,0,255),-1)
# save result
cv2.imwrite("hello_contours.png", result)
cv2.imwrite("hello_polygon.png", result2)
cv2.imwrite("hello_corners.png", result3)
# display it
cv2.imshow("thresh", thresh)
cv2.imshow("result", result)
cv2.imshow("result2", result2)
cv2.imshow("result3", result3)
cv2.waitKey(0)
Contours and Polygon on input image:
Contours and Polygon on black background:
Polygon Vertices:
227 69
41 149
114 284
307 167
228 70
Note the first and last vertices are within one pixel of each other
Corners on white polygon on black background:
Corner Vertices:
306 167
42 149
114 283
227 69

Finding length of each line segment passing through centroid , and how to constrict the line till outer contour

Input Image
Processed Image
import numpy as np
import cv2
img = cv2.imread('Image(i).png', 0)
ret, img =cv2.threshold(img, 128, 255, cv2.THRESH_BINARY)
img_bw = img<=120
img_bw =img_bw.astype('uint8')
#Fit the ellipses
contours0, hierarchy = cv2.findContours( img.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
outer_ellipse = [cv2.approxPolyDP(contours0[0], 0.1, True)]
inner_ellipse = [cv2.approxPolyDP(contours0[0], 0.1, True)]
ref = np.zeros_like(img_bw)
out=img.copy()
h, w = img.shape[:2]
vis = np.zeros((h, w, 3), np.uint8)
cv2.drawContours( vis, outer_ellipse, -1, (255,0,0), 1)
cv2.drawContours( vis, inner_ellipse, -1, (0,0,255), 1)
##Extract contour of ellipses
cnt_outer = np.vstack(outer_ellipse).squeeze()
cnt_inner = np.vstack(inner_ellipse).squeeze()
#Determine centroid
M = cv2.moments(cnt_inner)
cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])
print (cx, cy)
#Draw full segment lines
#cv2.line(vis,(cx,0),(cx,w),(150,0,0),1)
width = img.shape[1]
height = img.shape[0]
N = 20
for i in range(N):
tmp = np.zeros_like(img_bw)
theta = i*(360/N)
theta *= np.pi/180.0
cv2.line(tmp, (cx, cy),
(int(cx-np.cos(theta)*w),
int(cy+np.sin(theta)*h)), (150,0,0), 1)
(row,col) = np.nonzero(np.logical_and(tmp, ref))
#cv2.line(out, (cx, cy), (col,row),(255,0,0), 1)
# Show the image
cv2.imshow('Output', out)
cv2.waitKey(0)
cv2.destroyAllWindows()
As seen in processed image the lines passing through centroid are not constricted till outer contour and are passing trough it.
I want the lines to be stopped at the outer contour so as that I can measure distance from centroid to the outer contour.
First image is the input image and second image is of line segments passing through centroid.
Here's a possible approach:
draw your outer contour filled with black on a white background
You now have a black ellipse. Then, without actually drawing anything:
use skimage.draw.line to get the list of points along all your radii
use Numpy argmax() to get first white pixel along radii
Here is the code:
#!/usr/bin/env python3
import cv2
import math
from skimage.draw import line
import numpy as np
# Load image as greyscale
img = cv2.imread('ellipses.png', cv2.IMREAD_GRAYSCALE)
_, img = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY)
h, w = img.shape
#Fit the ellipses
contours, hierarchy = cv2.findContours( img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
outer_ellipse = [cv2.approxPolyDP(contours[0], 0.1, True)]
# Draw outer contour filled with black on white background
vis = np.zeros_like(img) + 255
cnt = cv2.drawContours(vis, outer_ellipse, -1, 0, -1)
# Centroid by existing method
cx, cy = 365, 335
maxThickness = 0
# Take 10 points along top
for x in range(0,w,int(w/10)):
# ... and bottom
for y in 0, h-1:
# Get y and x of all pixels between centroid and top and bottom edge
yy, xx = line(cy, cx, 0, x)
firstWhiteIndex = np.argmax(vis[yy,xx])
fx, fy = xx[firstWhiteIndex], yy[firstWhiteIndex]
# Get length of this radial line
length = np.sqrt((cx-fx)**2 + (cy-fy)**2)
# Remember if longer than all others so far seen
if length > maxThickness:
maxThickness = length
fxMax, fyMax = fx, fy
# Take 10 points down left side
for y in range(0,h,int(h/10)):
# ... and right
for x in 0, w-1:
# Get y and x of all pixels between centroid and left and right edge
yy, xx = line(cy, cx, 0, x)
firstWhiteIndex = np.argmax(vis[yy,xx])
fx, fy = xx[firstWhiteIndex], yy[firstWhiteIndex]
# Get length of this radial line
length = np.sqrt((cx-fx)**2 + (cy-fy)**2)
# Remember if longer than all others so far seen
if length > maxThickness:
maxThickness = length
fxMax, fyMax = fx, fy
print(f'Max thickness: {maxThickness}')
# Draw thickest radius in mid-grey
cv2.line(img, (cx,cy), (fxMax, fyMax), 128, 5)
cv2.imwrite('result.png', img)
I have an approach that is not the best but this is what I can think of now.
While drawing lines in the above image, modify the code and do the following:
Before the for loop, draw a binary image of the same size containing only the outer contour circle. Save this image for later use.
Now in the for loop, draw each line in a separate binary blank image. Thus, now you will have two images, first the image having only the outer circle, and second image will only contain the line.
Now perform a bitwise_and operation on these 2 images.
Now you will get a white pixel only that is the point of intersection of the line and the outer circle.
Now find the coordinates of the white pixel in the image found and hence you will have the coordinate of point of intersection.
Obviously this is not the most efficient way but it is real time. Also, keep this in mind that the outer circle width should be atleast 2 in the image and the lines should be of width 1. You may get more than one point of intersection in some cases, take any 1 of them. The difference in them will be only of 1-2 pixels that can be neglected.

How can we generate new sampling points by bilinear interpolation?

I have a picture in grayscle and I need to generate new sampling points (circled in red in the picture) by bilinear interpolation. Is there a formula or a function to compute the values of these points in python?
According to the diagram, it doesn't look like bi-linear interpolation, it looks like an average between two angles.
I am not sure if my solution is what you are looking for, but I assume it gives you a lead...
I tried solving it by finding the circle automatically using cv2.HoughCircles, and mark x, y positions using trigonometry.
The solution uses the following stages:
Convert image to gray and to binary.
Find circles using cv2.HoughCircles.
Iterate circles, and find the circle with center closest to the center of the image.
Compute angles in steps of 45 degrees [0, 45, 90, 135...], name them alpha.
Compute angles between the above angles [22.5, 67.5, 112.5...], name them beta.
We don't really need alpha for computing beta.
It is just for demonstrating the kind of interpolation you suppose to do.
Compute x, y of each point using trigonometry.
Mark "alpha" points with cyan circles.
Mark "beta" points with yellow circles.
You may store x, y of "beta" points - those are the points you are looking for.
Here is the code:
import cv2
import numpy as np
# Read input imgae
img = cv2.imread('image.png')
# Convert to Grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Convert to binary image, and invert polarity
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
rows, cols = thresh.shape
circles = cv2.HoughCircles(thresh, cv2.HOUGH_GRADIENT, 1, minDist=rows//8, param1=50, param2=60, minRadius=rows//8, maxRadius=rows//2)
# Find the circle with center closest to the center of the image
min_dist_from_center = 1e9
min_c = []
for c in circles[0,:]:
# Euclidean distance from the center of circle to center of image
dist_from_center = np.linalg.norm([c[0] - cols/2, c[1] - rows/2])
if dist_from_center < min_dist_from_center:
min_dist_from_center = dist_from_center
min_c = c
c = min_c
# Draw circle for testing
cv2.circle(img, (c[0], c[1]), c[2], (0, 255, 0), 2)
# Array of angles in 45 degrees difference
alpha_arr = np.arange(0, 360+45, 45) # [ 0, 45, 90, 135, 180, 225, 270, 315, 360]
betta_arr = (alpha_arr[1:] + alpha_arr[0:-1])/2 # [ 22.5, 67.5, 112.5, 157.5, 202.5, 247.5, 292.5, 337.5] Points between alpha
# Compute x, y coordinates by angle and radius
r = c[2]
for alpha, beta in zip(alpha_arr[:-1], betta_arr):
x = r*np.cos(np.deg2rad(alpha)) + c[0] # x = r*cos(alpha) + center_x
y = r*np.sin(np.deg2rad(alpha)) + c[1] # y = r*sin(alpha) + center_y
# Draw small cyan circle to mark alpha points
cv2.circle(img, (int(x), int(y)), 12, (255, 255, 0), 3)
x = r*np.cos(np.deg2rad(beta)) + c[0] # x = r*cos(alpha) + center_x
y = r*np.sin(np.deg2rad(beta)) + c[1] # y = r*sin(alpha) + center_y
# Draw small yellow circle to mark beta points
cv2.circle(img, (int(x), int(y)), 10, (0, 255, 255), 3)
# Show images for testing
cv2.imshow('thresh', thresh)
cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
Result:

Categories

Resources