Remove and measure a line openCV - python

Links to all images at the bottom
I have drawn a line over an arrow which captures the angle of that arrow. I would like to then remove the arrow, keep only the line, and use cv2.minAreaRect to determine the angle. So far I've got everything to work except removing the original arrow, which results in an incorrect angle generated by the cv2.minAreaRect bounding box.
Really, I just want the bold black line running through the arrow to use to measure the angle, not the arrow itself. if anyone has an idea to make this work, or a simpler way, please let me know. Thanks
Code:
import numpy as np
import cv2
image = cv2.imread("templates/a_15.png")
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(image, 127, 255, 0)
contours, hierarchy = cv2.findContours(thresh, 1, 2)
cont = contours[0]
rows,cols = image.shape[:2]
[vx,vy,x,y] = cv2.fitLine(cont, cv2.DIST_L2,0,0.01,0.01)
leftish = int((-x*vy/vx) + y)
rightish = int(((cols-x)*vy/vx)+y)
line = cv2.line(image,(cols-1,rightish),(0,leftish),(0,255,0),10)
# thresholding
thresh = cv2.threshold(line, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
# compute rotated bounding box based on all pixel values > 0 and
# use coordinates to compute a rotated bounding box of those coordinates
coordinates = np.column_stack(np.where(thresh > 0))
w = coordinates[0]
h = coordinates[1]
# Compute minimum rotated angle that contains entire image.
# Return angle values in the range [-90, 0).
# As the rectangle rotates clockwise, angle values increase towards 0.
# Once 0 is reached, angle is set back to -90 degrees.
angle = cv2.minAreaRect(coordinates)[-1]
# for angles less than -45 degrees, add 90 degrees to angle to take the inverse.
if angle < - 45:
angle = -(90 + angle)
else:
angle = -angle
# rotate image
(h, w) = image.shape[:2]
center = (w // 2, h // 2) # image center
RM = cv2.getRotationMatrix2D(center, angle, 1.0)
rotated = cv2.warpAffine(image, RM, (w, h),
flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
# correction angle for validation
cv2.putText(rotated, "Angle {:.2f} degrees".format(angle),
(10, 30), cv2.FONT_HERSHEY_DUPLEX, 0.9, (0, 255, 0), 2)
# output
print("[INFO] angle: {:.3f}".format(angle))
cv2.imshow("Line", line)
cv2.imshow("Input", image)
cv2.imshow("Rotated", rotated)
cv2.waitKey(0)
Images
original
current results
goal

Here's a possible solution. The main idea is to identify de "tip" and the "tail" of the arrow approximating some key points. After you have identified both ends, you can draw a line joining both points. It is also an advantage to know which of the endpoints is the tip, because that way you can measure the angle from a constant point.
There's more than one way to achieve this. I choose something that I have applied in the past: I will use this approach to identify the endpoints of the overall shape. My assumption is that the tip will yield more points than the tail. After that, I'll cluster all the endpoints in two groups: tip and tail. I can use K-Means for that, as it will return the mean centers for both clusters. After that, we have our tip and tail points that can be joined easily with a line. These are the steps:
Convert the image to grayscale
Get the skeleton of the image, to normalize the shape to a width of 1 pixel
Apply the method described in the link to get the arrow's endpoints
Divide the endpoints in two clusters and use K-Means to get their centers
Join both endpoints with a line
Let's see the code:
# imports:
import cv2
import numpy as np
# image path
path = "D://opencvImages//"
fileName = "CoXeb.png"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# Grayscale conversion:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
grayscaleImage = 255 - grayscaleImage
# Extend the borders for the skeleton:
extendedImg = cv2.copyMakeBorder(grayscaleImage, 5, 5, 5, 5, cv2.BORDER_CONSTANT)
# Store a deep copy of the crop for results:
grayscaleImageCopy = cv2.cvtColor(extendedImg, cv2.COLOR_GRAY2BGR)
# Compute the skeleton:
skeleton = cv2.ximgproc.thinning(extendedImg, None, 1)
The first step is to get the skeleton of the arrow. As I said, this step is needed prior to the convolution-based method that identifies the endpoints of a shape. Computing the skeleton normalizes the shape to a one pixel width. However, sometimes, if the shape is too close to the "canvas" borders, the skeleton could show some artifacts. This is avoided with a border extension. The skeleton of the arrow is this:
Check that image out. If we identify the endpoints, the tip will exhibit at least 3 points, while the tail at least 1. That's handy - the tip will always have more points than the tail. If only we could detect those points... Luckily, we can:
# Threshold the image so that white pixels get a value of 0 and
# black pixels a value of 10:
_, binaryImage = cv2.threshold(skeleton, 128, 10, cv2.THRESH_BINARY)
# Set the end-points kernel:
h = np.array([[1, 1, 1],
[1, 10, 1],
[1, 1, 1]])
# Convolve the image with the kernel:
imgFiltered = cv2.filter2D(binaryImage, -1, h)
# Extract only the end-points pixels, those with
# an intensity value of 110:
binaryImage = np.where(imgFiltered == 110, 255, 0)
# The above operation converted the image to 32-bit float,
# convert back to 8-bit uint
binaryImage = binaryImage.astype(np.uint8)
This endpoint detecting method convolves the skeleton with a special kernel that identifies endpoints. It returns a binary image where all the endpoints have the value 110. After thresholding this mid-result, we get this image, which represents the arrow endpoints:
Nice, as you see, we can group the points in two clusters and get their cluster centers. Sounds like a job for K-Means, because that's exactly what it does. We first need to treat our data, though, because K-Means operates on defined-shaped arrays of float data:
# Find the X, Y location of all the end-points
# pixels:
Y, X = binaryImage.nonzero()
# Reshape the arrays for K-means
Y = Y.reshape(-1,1)
X = X.reshape(-1,1)
Z = np.hstack((X, Y))
# K-means operates on 32-bit float data:
floatPoints = np.float32(Z)
# Set the convergence criteria and call K-means:
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
ret, label, center = cv2.kmeans(floatPoints, 2, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
# Set the cluster count, find the points belonging
# to cluster 0 and cluster 1:
cluster1Count = np.count_nonzero(label)
cluster0Count = np.shape(label)[0] - cluster1Count
print("Elements of Cluster 0: "+str(cluster0Count))
print("Elements of Cluster 1: " + str(cluster1Count))
The last two lines prints the endpoints that are assigned to Cluster 0 Cluster 1, respectively. That outputs this:
Elements of Cluster 0: 3
Elements of Cluster 1: 2
Just as expected - well, kinda. Seems that Cluster 0 is the tip and cluster 2 the tail! But the tail actually got 2 points. If you look the image of the skeleton closely, you'll see there's a small bifurcation at the tail. That's why we, in reality, got two points instead of just one. Alright, let's get the center points and draw them on the original input:
# Look for the cluster of max number of points
# That cluster will be the tip of the arrow:
maxCluster = 0
if cluster1Count > cluster0Count:
maxCluster = 1
# Check out the centers of each cluster:
matRows, matCols = center.shape
# Store the ordered end-points here:
orderedPoints = [None] * 2
# Let's identify and draw the two end-points
# of the arrow:
for b in range(matRows):
# Get cluster center:
pointX = int(center[b][0])
pointY = int(center[b][1])
# Get the "tip"
if b == maxCluster:
color = (0, 0, 255)
orderedPoints[0] = (pointX, pointY)
# Get the "tail"
else:
color = (255, 0, 0)
orderedPoints[1] = (pointX, pointY)
# Draw it:
cv2.circle(grayscaleImageCopy, (pointX, pointY), 3, color, -1)
cv2.imshow("End-Points", grayscaleImageCopy)
cv2.waitKey(0)
This is the resulting image:
The tip always gets drawn in red while the tail is drawn in blue. Very cool, let's store these points in the orderedPoints list and draw the final line in a new "canvas", with dimension same as the original image:
# Store the tip and tail points:
p0x = orderedPoints[1][0]
p0y = orderedPoints[1][1]
p1x = orderedPoints[0][0]
p1y = orderedPoints[0][1]
# Create a new "canvas" (image) using the input dimensions:
imageHeight, imageWidth = binaryImage.shape[:2]
newImage = np.zeros((imageHeight, imageWidth), np.uint8)
newImage = 255 - newImage
# Draw a line using the detected points:
(x1, y1) = orderedPoints[0]
(x2, y2) = orderedPoints[1]
lineColor = (0, 0, 0)
cv2.line(newImage , (x1, y1), (x2, y2), lineColor, thickness=2)
cv2.imshow("Detected Line", newImage)
cv2.waitKey(0)
The line overlaid on the original image and the new image containing only the line:

It sounds like you want to measure the angle of the line but because you are measuring a line you drew in the original image, you must now filter out the original image to get an accurate measure of the line...which you drew with coordinates you know the endpoints of?
I guess:
make a better filter?
draw the line in a blank image and detect angle there?
determine the angle from the known coordinates?
Since you were asking for just a line, I tried that...just made a blank image, drew your detected line on it and then used that downstream...
blankIm = np.ones((height, width, channels), dtype=np.uint8)
blankIm.fill(255)
line = cv2.line(blankIm,(cols-1,rightish),(0,leftish),(0,255,0),10)

Related

The goal is to find the line that represents the distance between the "hole" and the outer edge. Is Canny edge detection the best approach?

The goal is to find the line that represents the distance between the "hole" and the outer edge (transition from black to white). I was able to successfully binarize this photo and get a very clean black and white image. The next step would be to find the (almost) vertical line on it and calculate the perpendicular distance to the midpoint of this vertical line and the hole.
original picture
hole - zoomed in
ps: what I call "hole" is a shadow. I am shooting a laser into a hole. So the lines we can see is a steel and the black part without a line is a hole. The 2 white lines serve as a reference to measure the distance.
Is Canny edge detection the best approach? If so, what are good values for the A, B and C parameters? I can't tune it. I'm getting too much noise.
This is not complete; You have to take the time to reach the final result. But this idea might help you.
Preprocessors:
import os
import cv2
import numpy as np
Main code:
# Read original image
dir = os.path.abspath(os.path.dirname(__file__))
im = cv2.imread(dir+'/'+'im.jpg')
h, w = im.shape[:2]
print(w, h)
# Convert image to Grayscale
imGray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
cv2.imwrite(dir+'/im_1_grayscale.jpg', imGray)
# Eliminate noise and display laser light better
imHLine = imGray.copy()
imHLine = cv2.GaussianBlur(imHLine, (0, 9), 21) # 5, 51
cv2.imwrite(dir+'/im_2_h_line.jpg', imHLine)
# Make a BW mask to find the ROI of laser array
imHLineBW = cv2.threshold(imHLine, 22, 255, cv2.THRESH_BINARY)[1]
cv2.imwrite(dir+'/im_3_h_line_bw.jpg', imHLineBW)
# Remove noise with mask and extract just needed area
imHLineROI = imGray.copy()
imHLineROI[np.where(imHLineBW == 0)] = 0
imHLineROI = cv2.GaussianBlur(imHLineROI, (0, 3), 6)
imHLineROI = cv2.threshold(imHLineROI, 25, 255, cv2.THRESH_BINARY)[1] # 22
cv2.imwrite(dir+'/im_4_ROI.jpg', imHLineROI)
# Found laser array and draw box around it
cnts, _ = cv2.findContours(
imHLineROI, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts.sort(key=lambda x: cv2.boundingRect(x)[0])
pts = []
for cnt in cnts:
x2, y2, w2, h2 = cv2.boundingRect(cnt)
if h2 < h/10:
cv2.rectangle(im, (x2, y2), (x2+w2, y2+h2), (0, 255, 0), 1)
pts.append({'x': x2, 'y': y2, 'w': w2, 'h': h2})
circle = {
'left': (pts[0]['x']+pts[0]['w'], pts[0]['y']+pts[0]['h']/2),
'right': (pts[1]['x'], pts[1]['y']+pts[1]['h']/2),
}
circle['center'] = calculateMiddlePint(circle['left'], circle['right'])
circle['radius'] = (circle['right'][0]-circle['left'][0])//2
# Draw line and Circle inside it
im = drawLine(im, circle['left'], circle['right'], color=(27, 50, 120))
im = cv2.circle(im, circle['center'], circle['radius'], (255, 25, 25), 3)
# Remove pepper/salt noise to find metal edge
imVLine = imGray.copy()
imVLine = cv2.medianBlur(imVLine, 17)
cv2.imwrite(dir+'/im_6_v_line.jpg', imVLine)
# Remove remove the shadows to find metal edge
imVLineBW = cv2.threshold(imVLine, 50, 255, cv2.THRESH_BINARY)[1]
cv2.imwrite(dir+'/im_7_v_bw.jpg', imVLineBW)
# Finding the right vertical edge of metal
y1, y2 = h/5, h-h/5
x1 = horizantalDistance(imVLineBW, y1)
x2 = horizantalDistance(imVLineBW, y2)
pt1, pt2 = (x1, y1), (x2, y2)
imVLineBW = drawLine(imVLineBW, pt1, pt2)
cv2.imwrite(dir+'/im_8_v_bw.jpg', imVLineBW)
# Draw lines
im = drawLine(im, pt1, pt2)
im = drawLine(im, calculateMiddlePint(pt1, pt2), circle['center'])
# Draw final image
cv2.imwrite(dir+'/im_8_output.jpg', im)
Extra functions:
Find the first white pixel in one line of picture:
# This function only processes on a horizontal line of the image
# Its job is to examine the pixels one by one from the right and
# report the distance of the first white pixel from the right of
# the image.
def horizantalDistance(im, y):
y = int(y)
h, w = im.shape[:2]
for i in range(0, w):
x = w-i-1
if im[y][x] == 255:
return x
return -1
To draw a line in opencv:
def drawLine(im, pt1, pt2, color=(128, 0, 200), thickness=2):
return cv2.line(
im,
pt1=(int(pt1[0]), int(pt1[1])),
pt2=(int(pt2[0]), int(pt2[1])),
color=color,
thickness=thickness,
lineType=cv2.LINE_AA # Anti-Aliased
)
To calculate middle point of two 2d points:
def calculateMiddlePint(p1, p2):
return (int((p1[0]+p2[0])/2), int((p1[1]+p2[1])/2))
Output:
Original image:
Eliminate noise and process to see the laser array better:
Find the laser area to extract the hole:
Work on another copy of image to find the right side of metal object:
Remove the shadows to better see the right edge:
The final output:
I first defined an ROI area. I changed the code later but did not change the names of the variables. If you were asked.
You can try one of binarized Canny, or straight binarized image (Otsu). In both cases, you will obtain a quasi-straight line from which you can extract the right-most white pixel on every row or so.
Then use line fitting to these points (depending on image quality the fitting needs to be robust or not).
It would be wise to calibrate the method against ground truth, because the edge your are looking after borders a gradient area, and the location of the edge might be offset by several pixels.

How to infer the state of a shape from colors

I have Lego cubes forming 4x4 shape, and I'm trying to infer the status of a zone inside the image:
empty/full and the color whether if yellow or Blue.
to simplify my work I have added red marker to define the border of the shape since the camera is shaking sometimes.
Here is a clear image of the shape I'm trying to detect taken by my phone camera
( EDIT : Note that this image is not my input image, it is used just to demonstrate the required shape clearly ).
The shape from the side camera that I'm supposed to use looks like this:
(EDIT : Now this is my input image)
to focus my work on the working zone I have created a mask:
what I have tried so far is to locate the red markers by color (simple threshold without HSV color-space) as following:
import numpy as np
import matplotlib.pyplot as plt
import cv2
img = cv2.imread('sample.png')
RGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
mask = cv2.imread('mask.png')
masked = np.minimum(RGB, mask)
masked[masked[...,1]>25] = 0
masked[masked[...,2]>25] = 0
masked = masked[..., 0]
masked = cv2.medianBlur(masked,5)
plt.imshow(masked, cmap='gray')
plt.show()
and I have spotted the markers so far:
But I'm still confused:
how to detect the external borders of the desired zone, and the internal borders (each Lego cube(Yellow-Blue-Green) borders) inside the red markers precisely?.
thanks in advance for your kind advice.
I tested this approach using your undistorted image. Suppose you have the rectified camera image, so you see the lego bricks through a "bird's eye" perspective. Now, the idea is to use the red markers to estimate a center rectangle and crop that portion of the image. Then, as you know each brick's dimensions (and they are constant) you can trace a grid and extract each cell of the grid, You can compute some HSV-based masks to estimate the dominant color on each grid, and that way you know if the space is occupied by a yellow or blue brick, of it is empty.
These are the steps:
Get an HSV mask of the red markers
Use each marker to estimate the center rectangle through each marker's coordinates
Crop the center rectangle
Divide the rectangle into cells - this is the grid
Run a series of HSV-based maks on each cell and compute the dominant color
Label each cell with the dominant color
Let's see the code:
# Importing cv2 and numpy:
import numpy as np
import cv2
# image path
path = "D://opencvImages//"
fileName = "Bg9iB.jpg"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# Store a deep copy for results:
inputCopy = inputImage.copy()
# Convert the image to HSV:
hsvImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2HSV)
# The HSV mask values (Red):
lowerValues = np.array([127, 0, 95])
upperValues = np.array([179, 255, 255])
# Create the HSV mask
mask = cv2.inRange(hsvImage, lowerValues, upperValues)
The first part is very straightforward. You set the HSV range and use cv2.inRange to get a binary mask of the target color. This is the result:
We can further improve the binary mask using some morphology. Let's apply a closing with a somewhat big structuring element and 10 iterations. We want those markers as clearly defined as possible:
# Set kernel (structuring element) size:
kernelSize = 5
# Set operation iterations:
opIterations = 10
# Get the structuring element:
maxKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))
# Perform closing:
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, maxKernel, None, None, opIterations, cv2.BORDER_REFLECT101)
Which yields:
Very nice. Now, let's detect contours on this mask. We will approximate each contour to a bounding box and store its starting point and dimensions. The idea being that, while we will detect every contour, we are not sure of their order. We can sort this list later and get each bounding box from left to right, top to bottom to better estimate the central rectangle. Let's detect contours:
# Create a deep copy, convert it to BGR for results:
maskCopy = mask.copy()
maskCopy = cv2.cvtColor(maskCopy, cv2.COLOR_GRAY2BGR)
# Find the big contours/blobs on the filtered image:
contours, hierarchy = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
# Bounding Rects are stored here:
boundRectsList = []
# Process each contour 1-1:
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 = 100
# Filter blobs by area:
if rectArea > minArea:
#Store the rect:
boundRectsList.append(boundRect)
I also created a deep copy of the mask image for further use. Mainly to create this image, which is the result of the contour detection and bounding box approximation:
Notice that I have included a minimum area condition. I want to ignore noise below a certain threshold defined by minArea. Alright, now we have the bounding boxes in the boundRectsList variable. Let's sort this boxes using the Y coordinate:
# Sort the list based on ascending y values:
boundRectsSorted = sorted(boundRectsList, key=lambda x: x[1])
The list is now sorted and we can enumerate the boxes from left to right, top to bottom. Like this: First "row" -> 0, 1, Second "Row" -> 2, 3. Now, we can define the big, central, rectangle using this info. I call these "inner points". Notice the rectangle is defined as function of all the bounding boxes. For example, its top left starting point is defined by bounding box 0's bottom right ending point (both x and y). Its width is defined by bounding box 1's bottom left x coordinate, height is defined by bounding box 2's rightmost y coordinate. I'm gonna loop through each bounding box and extract their relevant dimensions to construct the center rectangle in the following way: (top left x, top left y, width, height). There's more than one way yo achieve this. I prefer to use a dictionary to get the relevant data. Let's see:
# Rectangle dictionary:
# Each entry is an index of the currentRect list
# 0 - X, 1 - Y, 2 - Width, 3 - Height
# Additionally: -1 is 0 (no dimension):
pointsDictionary = {0: (2, 3),
1: (-1, 3),
2: (2, -1),
3: (-1, -1)}
# Store center rectangle coordinates here:
centerRectangle = [None]*4
# Process the sorted rects:
rectCounter = 0
for i in range(len(boundRectsSorted)):
# Get sorted rect:
currentRect = boundRectsSorted[i]
# Get the bounding rect's data:
rectX = currentRect[0]
rectY = currentRect[1]
rectWidth = currentRect[2]
rectHeight = currentRect[3]
# Draw sorted rect:
cv2.rectangle(maskCopy, (int(rectX), int(rectY)), (int(rectX + rectWidth),
int(rectY + rectHeight)), (0, 255, 0), 5)
# Get the inner points:
currentInnerPoint = pointsDictionary[i]
borderPoint = [None]*2
# Check coordinates:
for p in range(2):
# Check for '0' index:
idx = currentInnerPoint[p]
if idx == -1:
borderPoint[p] = 0
else:
borderPoint[p] = currentRect[idx]
# Draw the border points:
color = (0, 0, 255)
thickness = -1
centerX = rectX + borderPoint[0]
centerY = rectY + borderPoint[1]
radius = 50
cv2.circle(maskCopy, (centerX, centerY), radius, color, thickness)
# Mark the circle
org = (centerX - 20, centerY + 20)
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(maskCopy, str(rectCounter), org, font,
2, (0, 0, 0), 5, cv2.LINE_8)
# Show the circle:
cv2.imshow("Sorted Rects", maskCopy)
cv2.waitKey(0)
# Store the coordinates into list
if rectCounter == 0:
centerRectangle[0] = centerX
centerRectangle[1] = centerY
else:
if rectCounter == 1:
centerRectangle[2] = centerX - centerRectangle[0]
else:
if rectCounter == 2:
centerRectangle[3] = centerY - centerRectangle[1]
# Increase rectCounter:
rectCounter += 1
This image shows each inner point with a red circle. Each circle is enumerated from left to right, top to bottom. The inner points are stored in the centerRectangle list:
If you join each inner point you get the center rectangle we have been looking for:
# Check out the big rectangle at the center:
bigRectX = centerRectangle[0]
bigRectY = centerRectangle[1]
bigRectWidth = centerRectangle[2]
bigRectHeight = centerRectangle[3]
# Draw the big rectangle:
cv2.rectangle(maskCopy, (int(bigRectX), int(bigRectY)), (int(bigRectX + bigRectWidth),
int(bigRectY + bigRectHeight)), (0, 0, 255), 5)
cv2.imshow("Big Rectangle", maskCopy)
cv2.waitKey(0)
Check it out:
Now, just crop this portion of the original image:
# Crop the center portion:
centerPortion = inputCopy[bigRectY:bigRectY + bigRectHeight, bigRectX:bigRectX + bigRectWidth]
# Store a deep copy for results:
centerPortionCopy = centerPortion.copy()
This is the central portion of the image:
Cool, now let's create the grid. You know that there must be 4 bricks per width and 4 bricks per height. We can divide the image using this info. I'm storing each sub-image, or cell, in a list. I'm also estimating each cell's center, for additional processing. These are stored in a list too. Let's see the procedure:
# Dive the image into a grid:
verticalCells = 4
horizontalCells = 4
# Cell dimensions
cellWidth = bigRectWidth / verticalCells
cellHeight = bigRectHeight / horizontalCells
# Store the cells here:
cellList = []
# Store cell centers here:
cellCenters = []
# Loop thru vertical dimension:
for j in range(verticalCells):
# Cell starting y position:
yo = j * cellHeight
# Loop thru horizontal dimension:
for i in range(horizontalCells):
# Cell starting x position:
xo = i * cellWidth
# Cell Dimensions:
cX = int(xo)
cY = int(yo)
cWidth = int(cellWidth)
cHeight = int(cellHeight)
# Crop current cell:
currentCell = centerPortion[cY:cY + cHeight, cX:cX + cWidth]
# into the cell list:
cellList.append(currentCell)
# Store cell center:
cellCenters.append((cX + 0.5 * cWidth, cY + 0.5 * cHeight))
# Draw Cell
cv2.rectangle(centerPortionCopy, (cX, cY), (cX + cWidth, cY + cHeight), (255, 255, 0), 5)
cv2.imshow("Grid", centerPortionCopy)
cv2.waitKey(0)
This is the grid:
Let's now process each cell individually. Of course, you can process each cell on the last loop, but I'm not currently looking for optimization, clarity is my priority. We need to generate a series of HSV masks with the target colors: yellow, blue and green (empty). I prefer to, again, implement a dictionary with the target colors. I'll generate a mask for each color and I'll count the number of white pixels using cv2.countNonZero. Again, I set a minimum threshold. This time of 10. With this info I can determine which mask generated the maximum number of white pixels, thus, giving me the dominant color:
# HSV dictionary - color ranges and color name:
colorDictionary = {0: ([93, 64, 21], [121, 255, 255], "blue"),
1: ([20, 64, 21], [30, 255, 255], "yellow"),
2: ([55, 64, 21], [92, 255, 255], "green")}
# Cell counter:
cellCounter = 0
for c in range(len(cellList)):
# Get current Cell:
currentCell = cellList[c]
# Convert to HSV:
hsvCell = cv2.cvtColor(currentCell, cv2.COLOR_BGR2HSV)
# Some additional info:
(h, w) = currentCell.shape[:2]
# Process masks:
maxCount = 10
cellColor = "None"
for m in range(len(colorDictionary)):
# Get current lower and upper range values:
currentLowRange = np.array(colorDictionary[m][0])
currentUppRange = np.array(colorDictionary[m][1])
# Create the HSV mask
mask = cv2.inRange(hsvCell, currentLowRange, currentUppRange)
# Get max number of target pixels
targetPixelCount = cv2.countNonZero(mask)
if targetPixelCount > maxCount:
maxCount = targetPixelCount
# Get color name from dictionary:
cellColor = colorDictionary[m][2]
# Get cell center, add an x offset:
textX = int(cellCenters[cellCounter][0]) - 100
textY = int(cellCenters[cellCounter][1])
# Draw text on cell's center:
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(centerPortion, cellColor, (textX, textY), font,
2, (0, 0, 255), 5, cv2.LINE_8)
# Increase cellCounter:
cellCounter += 1
cv2.imshow("centerPortion", centerPortion)
cv2.waitKey(0)
This is the result:
From here it is easy to identify the empty spaces on the grid. What I didn't cover was the perspective rectification of your distorted image, but there's plenty of info on how to do that. Hope this helps you out!
Edit:
If you want to apply this approach to your distorted image you need to undo the fish-eye and the perspective distortion. Your rectified image should look like this:
You probably will have to tweak some values because some of the distortion still remains, even after rectification.

How to detect a precise rectangle in a photo opencv

I was using this code to detect the rectangle on the photo, at first it was working well, untill i realized that i would have an object that is also a square in the middle :
Question:
How can i properly detect the 4 corners like on the first result picture without detecting the corner of the thing in the middle of the square. Thanks a lot.
Code:
import numpy as np
import cv2
img = cv2.imread('Photos/lastBoard.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
canny = cv2.Canny(gray, 100, 200)
corners = cv2.goodFeaturesToTrack(gray, 25, 0.01, 50)
corner_list = []
for corner in corners:
x, y = corner.ravel()
if(y < 700 and (50 < x < 800 )):
corner_list.append([int(x), int(y)])
cv2.circle(img, (x, y), 5, (36, 255, 12), -1)
cv2.imshow("yo", img)
cv2.waitKey(0)
My man, it breaks my heart you aren't using the techniques and processing we covered in your last question. You have already plenty of functions you could re-use. The rectangle you are trying to segment has a unique color (kind of green) and has a defined area and aspect ratio! Look all the things you have on the table, they are smaller than the rectangle! Plus, the rectangle is almost a square! That means that its aspect ratio is close to 1.0. If you somehow segment the rectangle, approximating its corners should be relativity easy.
This is valuable info, because it allows you to trace your action plan. I see you are using cv2.goodFeaturesToTrack to detect the corners of everything. That's OK, but it could be simplified. I propose a plan of action very similar to last time:
Try to segment the rectangle using its color, let's compute an
HSV-based mask
Let's clean the mask from noise using an area filter and some morphology
Find contours - we are looking for the biggest green contour, the rectangle.
The contour of interest has defined features. Use the area and aspect ratio to filter garbage contours.
Once you have the contour/blob of interest, approximate its corners.
Let's see the code:
# imports:
import numpy as np
import cv2
# image path
path = "D://opencvImages//"
fileName = "table1.jpg"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
inputCopy = inputImage.copy()
# The HSV mask values:
lowerValues = np.array([58, 151, 25])
upperValues = np.array([86, 255, 75])
# Convert the image to HSV:
hsvImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2HSV)
# Create the HSV mask
mask = cv2.inRange(hsvImage, lowerValues, upperValues)
The first steps aim to create the HSV mask. Very similar to last time, I've defined the HSV range of interest already and applied exactly the same stuff as before. You could (and should) explore more exotic techniques latter, but let's stick with what we know works for the time being, as the project surely is due soon. This is the result:
You see how the mask is pretty nice already? Only the green puck and the rectangle survived the thresholding. It doesn't matter that the rectangle is not complete, because we're gonna approximate its contour with a bounding rectangle! Alright, let's clean this bad boy a little bit better. Use a filterArea (this is exactly the same function we saw last time) and then a closing (dilate followed by erode) just to get a nice mask:
# Run a minimum area filter:
minArea = 50
mask = areaFilter(minArea, mask)
# Pre-process mask:
kernelSize = 3
structuringElement = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))
iterations = 2
mask = cv2.morphologyEx(mask, cv2.MORPH_DILATE, structuringElement, None, None, iterations, cv2.BORDER_REFLECT101)
mask = cv2.morphologyEx(mask, cv2.MORPH_ERODE, structuringElement, None, None, iterations, cv2.BORDER_REFLECT101)
This is the filtered mask, the noise is mostly gone:
Now, let's find contours and filtered based on area and aspect ratio, just like last time. The parameters, however, are different, because our target is not the plucks, but the rectangle:
# Find the big contours/blobs on the filtered image:
contours, hierarchy = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
# Store the poly approximation and bound
contoursPoly = [None] * len(contours)
# Store the corners of the square here:
detectedCorners = []
# Look for the outer bounding boxes:
for _, 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]
# Calculate the rect's area:
rectArea = rectWidth * rectHeight
# Calculate the aspect ratio:
aspectRatio = rectWidth / rectHeight
delta = abs(1.0 - aspectRatio)
# Set the min threshold values to identify the
# blob of interest:
minArea = 2500
epsilon = 0.2
Alright, so far so good, I hope. As you see I approximated the contour to a 4-vertex polygon and then computed its bounding rectangle. This approximation should fit very nicely to our blob of interest. Now, apply the contour filter and use the bounding rectangle data to approximate the corners. I approximate each corner, one by one, and store them in the
detectedCorners array. Then, we can draw 'em. Here, still inside the for loop:
# Is this bounding rectangle we
# are looking for?
if rectArea > minArea and delta < epsilon:
# Compute the corners/vertices:
# Corner 1 (top left)
corner1 = (rectX, rectY)
detectedCorners.append(corner1)
# Corner 2 (top right)
corner2 = (rectX + rectWidth, rectY)
detectedCorners.append(corner2)
# Corner 3 (bottom left)
corner3 = (rectX, rectY + rectHeight)
detectedCorners.append(corner3)
# Corner 4 (bottom right)
corner4 = (rectX + rectWidth, rectY + rectHeight)
detectedCorners.append(corner4)
# Draw the corner points:
for p in detectedCorners:
color = (0, 0, 255)
cv2.circle(inputCopy, (p[0], p[1]), 5, color, -1)
cv2.imshow("Square Corners", inputCopy)
cv2.waitKey(0)
Here are the results for both images. The approximated corners are the red dots:
Here's the definition and implementation of the areaFilter function:
def areaFilter(minArea, inputImage):
# Perform an area filter on the binary blobs:
componentsNumber, labeledImage, componentStats, componentCentroids = \
cv2.connectedComponentsWithStats(inputImage, connectivity=4)
# Get the indices/labels of the remaining components based on the area stat
# (skip the background component at index 0)
remainingComponentLabels = [i for i in range(1, componentsNumber) if componentStats[i][4] >= minArea]
# Filter the labeled pixels based on the remaining labels,
# assign pixel intensity to 255 (uint8) for the remaining pixels
filteredImage = np.where(np.isin(labeledImage, remainingComponentLabels) == True, 255, 0).astype('uint8')
return filteredImage

Storing given percentage of shape contour coordinates

I am building a project on Detectron2 with some mushrooms as a topic. The prediction works OK-well and I'm now trying to generate COCO-like annotations of the images with the predicted region (all XY coordinates of the region). For this, I need to do two things:
Retrieve the XY coordinates of the predicted shape/region and "downscale it" to only save the main edges (in order to avoid saving too many data points)
Plot the "saved" points back onto the main image for the user to judge if enough points are being saved
Unfortunately i'm failing at both. On the first point, I (think) that I have the same as a binary numpy object, but i'm surprised by its size AND i don't manage to transform it into sets of XY coordinates
On the second point, I am getting an error that I can't figure out how to debug:
/usr/local/lib/python3.6/dist-packages/google/colab/patches/__init__.py in cv2_imshow(a)
20 image.
21 """
---> 22 a = a.clip(0, 255).astype('uint8')
23 # cv2 stores colors as BGR; convert to RGB
24 if a.ndim == 3:
AttributeError: 'cv2.UMat' object has no attribute 'clip'
The portion of the code i'm using is here:
from detectron2.utils.visualizer import ColorMode
## Predicts some random image
dataset_dicts = get_all_mushroom_dicts(mushroom_categories, "mushroom_dataset_small/val")
d = random.sample(dataset_dicts, 1)
im = cv2.imread(d["file_name"])
mushroom_outputs = mushroom_predictor(im)
v = Visualizer(im[:, :, ::-1],
metadata=mushroom_metadata,
scale=0.8,
instance_mode=ColorMode.IMAGE_BW # remove the colors of unsegmented pixels
)
instances = mushroom_outputs["instances"].to("cpu")
mush_out = v.draw_instance_predictions(instances)
image = mush_out.get_image()[:, :, ::-1]
masks = np.asarray(instances.pred_masks)
print ("NP array shape", masks.shape)
print("Image array shape", image.shape)
print("Type before", type(image))
cv2_imshow(image) ## works fine
## ?? How to get the coordinates of the boundaries? And then take only some of them
## ??
## Assuming that (128, 128) is one of these coordinates for now
image2 = cv2.circle(image, (128, 128), 10, (255, 0, 0), 20)
print("Type after", type(image2))
cv2_imshow(image2) ## crashes
FYI I have also tried to find and draw the contour but this doesn't seem to work (see my post here https://github.com/facebookresearch/detectron2/issues/1702#event-3501434732 ).
Do you have any clue?
Thanks!
I managed to find a solution that works but part of it is not elegant:
Retrieve the XY coordinates:
masks = np.asarray(instances.pred_masks)
cnt, heirarchy = cv2.findContours(masks[0].astype("uint8"), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
border = cnt[0]
gap = 20 # take only every 20 point
for i in list(range(border.shape[0]))[0::gap]:
y = int(border[i][0][1]*0.8)
x = int(border[i][0][0]*0.8)
print("New XY coordinates", (x,y))
Draw on the picture:
image_old = mush_out.get_image()[:, :, ::-1]
cv2.imwrite("img.png", image_old)
image = cv2.imread("img.png",cv2.IMREAD_COLOR) # Still no clue why this is needed but doesnt work if you dont save and re-read...
radius = 3
color = (255, 0, 0)
cv2.circle(image, (x, y), radius, color, -1) # my previous code was wrong; cv2.circle returns void and edits directly the image
cv2_imshow(image)

Find a rotated bounding box of known size around a rectangle with noisy sides

I'm trying to find a rotated bounding box around a less-than-perfect binarized image of a rectangle. The imperfections are always different: sometimes it's hollow, sometimes there's stuff inside, sometimes one of the edges is missing a chunk, sometimes there's an extra chunk somewhere on the edge, and they're always slightly rotated by a random amount, but the size and shape of the expected bounding box is always nearly the same absolute value in pixels.
Here's some samples of what I have as inputs (resized to fit better in the post):
And ideally I'd like to find a bounding box around the outside of the white rectangle (although I'm mostly just interested in the edges) like this:
(found by inverting one of the hollow ones, getting the largest connected component, and getting a rotatedrect of forced size)
So far I've tried just getting a rotatedrect and forcing a shape afterwards, which works for almost every case except for when there's an extra chunk along one of the edges. I've tried getting connected components to isolate parts of it and get bounding boxes around those, which works for every case as long as they're hollow. I've tried dilating and eroding the image, getting contours and hough lines to try to find only the four cornerpoints, but I've had no luck with that either. I've also looked online for anything useful to no avail.
Any help or ideas would be greatly appreciated.
My solution comprises two parts:
Find (upright) bounding box of the big white rectangle by finding the biggest connected component, fill all holes in it, find outside vertical and horizontal lines (Hough), get the bounding box by taking the min/max x/y coordinates.
Match a (filled) rectangle of given size with center at center of bounding box from step 1 at different angles, print the best match as result.
Following is a simple program demonstrating this approach. The arguments at the beginning (filename, size of know rectangle, angle search range) would normally be passed in from the command line.
import cv2
import numpy as np
# arguments
file = '1.png'
w0, h0 = 425, 630 # size of known rectangle
ang_range = 1 # posible range (+/-) of angle in degrees
# read image
img = cv2.imread(file, cv2.IMREAD_GRAYSCALE)
h, w = img.shape
# find biggest connceted components
nb_components, output, stats, _ = cv2.connectedComponentsWithStats(img, connectivity=4)
sizes = stats[:, -1]
max_label, max_size = 1, sizes[1]
for i in range(2, nb_components):
if sizes[i] > max_size:
max_label = i
max_size = sizes[i]
img2 = np.zeros(img.shape, np.uint8)
img2[output == max_label] = 128
# fill holes
contours, _ = cv2.findContours(img2, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
for contour in contours:
cv2.drawContours(img2, [contour], 0, 128, -1)
# find lines
edges = cv2.Canny(img2, 50, 150, apertureSize = 3)
lines = cv2.HoughLinesP(edges, 1, np.pi/180, 40)
# find bounding lines
xmax = ymax = 0
xmin, ymin = w-1, h-1
for i in range(lines.shape[0]):
x1 = lines[i][0][0]
y1 = lines[i][0][1]
x2 = lines[i][0][2]
y2 = lines[i][0][3]
cv2.line(img2, (x1,y1), (x2,y2), 255, 2, cv2.LINE_AA)
if abs(x1-x2) < abs(y1-y2):
# vertical line
xmin = min(xmin,x1,x2)
xmax = max(xmax,x1,x2)
else:
# horizcontal line
ymin = min(ymin,y1,y2)
ymax = max(ymax,y1,y2)
cv2.rectangle(img2, (xmin,ymin), (xmax,ymax), 255, 1, cv2.LINE_AA)
cv2.imwrite(file.replace('.png', '_intermediate.png'), img2)
# rectangle of known size centered at bounding box
xc = (xmax + xmin) / 2
yc = (ymax + ymin) / 2
box = np.zeros(img.shape, np.uint8)
box[int(yc-h0/2):int(yc+h0/2), int(xc-w0/2):int(xc+w0/2)] = 255
# find best match of this rectangle at different angles
smax = angmax = 0
for ang in np.linspace(-ang_range, ang_range, 20):
rm = cv2.getRotationMatrix2D((xc,yc), ang, 1)
rotbox = cv2.warpAffine(box, rm, (w,h))
s = cv2.countNonZero(cv2.bitwise_and(rotbox, img))
if s > smax:
smax = s
angmax = ang
# output and visualize result
def draw_rotated_rect(img, size, center, angle, color, thickness):
rm = cv2.getRotationMatrix2D(center, angle, 1)
p0 = np.dot(rm,(xc-w0/2, yc-h0/2,1))
p1 = np.dot(rm,(xc-w0/2, yc+h0/2,1))
p2 = np.dot(rm,(xc+w0/2, yc+h0/2,1))
p3 = np.dot(rm,(xc+w0/2, yc-h0/2,1))
pnts = np.int32(np.vstack([p0,p1,p2,p3]) + 0.5).reshape(-1,4,2)
cv2.polylines(img, pnts, True, color, thickness, cv2.LINE_AA)
print(f'{file}: edges {pnts[0].tolist()}, angle = {angle:.2f}°')
res = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
draw_rotated_rect(res, (w0,h0), (xc,yc), angmax, (0,255,0), 2)
cv2.imwrite(file.replace('.png', '_result.png'), res)
Intermediate results to show how it works (gray = filled biggest connected component, thick white lines = Hough lines, thin white rectangle = upright bounding box):
(to view the full size pictures click on them and then remove the final m before the file extension)
Visualization of results (green = rotated rectangle of known size):
Results (should eventually be clamped to [0,image size), -1 is due to floating point rotation):
1.png: edges [[17, -1], [17, 629], [442, 629], [442, -1]], angle = 0.00°
2.png: edges [[7, 18], [9, 648], [434, 646], [432, 16]], angle = 0.26°
3.png: edges [[38, 25], [36, 655], [461, 657], [463, 27]], angle = -0.26°
4.png: edges [[36, 14], [28, 644], [453, 650], [461, 20]], angle = -0.79°
As we see in image 3, the match is not perfect. This could be due to the example images that were shrinked to somewhat differing sizes and of course I didn't know the size of the known rectangle, so I just assumed an appropriate value for the demonstration.
If this occurs with real data too, you may want to not only vary the angle to find the best match, but also shift the matching box a couple of pixels up/down and right/left. See for instance section 8.1 of Dawson-Howe: A Practical Introduction to Computer Vision with OpenCV for further details.

Categories

Resources