Detecting a lego baseplate in an image - image

I want my code to find the corners of a square lego plate in an image like the one attached.
I also want to find its dimensions, i.e. the number of "blops" in both dimensions (48x48 in the attached image).
I am currently looking at detecting the individual "blops", and the result so far is pretty good: a combination of blur, adaptiveThreshold, findContours and selection based on area finds the contours rendered in the second attached image (coloring is random).
I'm now looking for an algorithm to find the "grid" losely represented by these contours (or their mid-points), but I lack the google fu. Any ideas?
(Suggestions for different approaches are also very welcome.)
(The sample image shows bricks placed in the corners - an algorithm could expect this, if it helps.)
(The sample image has a rather wild background. I'd prefer to cope with that, if possible.)
Update 8 July 2016: I'm trying to write an algorithm that looks for streaks of adjacent contours forming lines. The algo should be able to find a number of these and, from that, deduce the form of the whole plate, even with perspective. Will update if it works...
Update December 2017: The above algorithm sort of worked, although it was a bit too unpredictable. Also I got problems with perspective (adding a "thick" lego brick changes the surface) and color recognition (shadows, camera peculiarities etc). This endeavor is on hold for now. If I resume it I will try with fixed camera positions immediately above the plate and consistent lights.

Here's a potential approach using color thresholding. The idea is to convert the image to HSV format then color threshold using a lower and upper bound with the assumption that the baseplate is in gray. This will give us a mask image. From here we morph open to remove noise, find contours, and sort for the largest contour. Next we obtain the rotated bounding box coordinates and draw this onto a new blank mask. Finally we bitwise-and the mask with the input image to get our result. To find the corner coordinates, we can use cv2.goodFeaturesToTrack() to find the points on the mask. Take a look at how to find accurate corner positions of a distorted rectangle from blurry image in python? and Shi-Tomasi Corner Detector & Good Features to Track for more details
Here's a visualization of each step:
We load the image, convert to HSV format, define a lower/upper bound, and perform color thresholding using cv2.inRange()
import cv2
import numpy as np
# Load image, convert to HSV, and color threshold
image = cv2.imread('1.png')
blank_mask = np.zeros(image.shape, dtype=np.uint8)
original = image.copy()
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
lower = np.array([0, 0, 109])
upper = np.array([179, 36, 255])
mask = cv2.inRange(hsv, lower, upper)
Next we create a rectangular kernel using cv2.getStructuringElement() and perform morphological operations using cv2.morphologyEx(). This step removes small particles of noise.
# Morph open to remove noise
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
opening = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
From here we find contours on the mask using cv2.findContours() and filter using contour area to obtain the largest contour. We then obtain the rotated boding box coordinates using cv2.minAreaRect() and cv2.boxPoints() then draw this onto a new blank mask with cv2.fillPoly(). This step gives us a "perfect" outer contour of the baseplate. Here's the detected outer contour highlighted in green and the resulting mask.
# Find contours and sort for largest contour
cnts = cv2.findContours(opening, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
# Obtain rotated bounding box and draw onto a blank mask
rect = cv2.minAreaRect(cnts)
box = cv2.boxPoints(rect)
box = np.int0(box)
cv2.drawContours(image,[box],0,(36,255,12),3)
cv2.fillPoly(blank_mask, [box], (255,255,255))
Finally we bitwise-and the mask with our original input image to obtain our result. Depending on what you need, you can change the background to black or white.
# Bitwise-and mask with input image
blank_mask = cv2.cvtColor(blank_mask, cv2.COLOR_BGR2GRAY)
result = cv2.bitwise_and(original, original, mask=blank_mask)
# result[blank_mask==0] = (255,255,255) # Color background white
To detect the corner coordinates, we can use cv2.goodFeaturesToTrack(). Here's the detected corners highlighted in purple:
Coordinates:
(91.0, 525.0)
(463.0, 497.0)
(64.0, 152.0)
(436.0, 125.0)
# Detect corners
corners = cv2.goodFeaturesToTrack(blank_mask, maxCorners=4, qualityLevel=0.5, minDistance=150)
for corner in corners:
x,y = corner.ravel()
cv2.circle(image,(x,y),8,(155,20,255),-1)
print("({}, {})".format(x,y))
Full Code
import cv2
import numpy as np
# Load image, convert to HSV, and color threshold
image = cv2.imread('1.png')
blank_mask = np.zeros(image.shape, dtype=np.uint8)
original = image.copy()
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
lower = np.array([0, 0, 109])
upper = np.array([179, 36, 255])
mask = cv2.inRange(hsv, lower, upper)
# Morph open to remove noise
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
opening = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
# Find contours and sort for largest contour
cnts = cv2.findContours(opening, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[0]
# Obtain rotated bounding box and draw onto a blank mask
rect = cv2.minAreaRect(cnts)
box = cv2.boxPoints(rect)
box = np.int0(box)
cv2.drawContours(image,[box],0,(36,255,12),3)
cv2.fillPoly(blank_mask, [box], (255,255,255))
# Bitwise-and mask with input image
blank_mask = cv2.cvtColor(blank_mask, cv2.COLOR_BGR2GRAY)
result = cv2.bitwise_and(original, original, mask=blank_mask)
result[blank_mask==0] = (255,255,255) # Color background white
# Detect corners
corners = cv2.goodFeaturesToTrack(blank_mask, maxCorners=4, qualityLevel=0.5, minDistance=150)
for corner in corners:
x,y = corner.ravel()
cv2.circle(image,(x,y),8,(155,20,255),-1)
print("({}, {})".format(x,y))
cv2.imwrite('mask.png', mask)
cv2.imwrite('opening.png', opening)
cv2.imwrite('blank_mask.png', blank_mask)
cv2.imwrite('image.png', image)
cv2.imwrite('result.png', result)
cv2.waitKey()

Related

Image processing how to detect specific custom shape in image

I want to detect a specific shape which is combination of 2 shapes Pentagon and a square. However side of a square should be around 3 times of a pentagon to match a valid shape as shown in the image.
I'm learning different image processing techniques for my requirement.
Contour Detection: Issue is knowing there are 7 corners is not enough as x : 3x need to be validated.
Haar features: Issue is images consist of background and there are text inside these object. So haar will not optimal method in my opinion.
My idea
I think may be I can detect lines corners and using side lengths I can do some math and identify the image.Is there any image processing technique that use mathematical distance, scale, location to identify object?
Original image
matching
not matching
Here's a simple approach:
Obtain binary image. Load image, convert to grayscale, Gaussian blur, then Otsu's threshold.
Filter for desired contour. We find contours then filter using a combination of contour approximation + contour area. With the observation that the shapes have a large area difference, we can use a predetermined threshold area to identify between the two.
Input -> Binary image -> Detected shape
Result
Match
Input -> Binary image -> Detected shape
Result
No Match
Since you didn't specify a language, I implemented in Python
import cv2
import numpy as np
# Load image, grayscale, Gaussian blur, Otsus threshold
image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3,3), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# Find contours
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.04 * peri, True)
area = cv2.contourArea(c)
# Filter using contour approximation and area filtering (Remove small noise)
if len(approx) > 4 and len(approx) < 8 and area > 200:
# This is the larger version
if area >= 5500:
print('Match')
# Smaller version
elif area < 5500:
print('No Match')
cv2.drawContours(image, [c], -1, (255,0,12), 3)
cv2.imshow('thresh', thresh)
cv2.imshow('image', image)
cv2.waitKey()

Detect small dots in image

I have grayscale image with dark dots that I can convert to binary (black / white) image.
Sample:
Grayscale input:
B&W image:
I need to find dots in red circles as on
The distance betwen dots is more-or-less uniform if there is no sharp corner.
I have a semi-working solution based on the original grayscale image and the Harris corner detector together with clustering, but it is quite slow and not so straigh-forward.
I have tried Hough transform for circles, but the dots are too small (10x10 px aprox.) to be detected correctly without too much noise.
However, I am able to quite correctly detect the line in grayscale image - see the red line in image. I already use this knowledge and filter dots based on the distance from the line.
However, in some cases this fail. For example the below image is quite problematic - the whick border has a "hole" and the dots are too close, connected to the thick line. I have also false positives from the numbers that are detected as dots.
Do you have any idea for a possible solution, ideally with OpenCV?
Note this is just a sample, the dots may not be on the thin line, but rather separate or the thin line is too bright etc. So the line cannot be used to detect dots.
A potential solution is to use morphological operations with a cv2.MORPH_ELLIPSE kernel to isolate the small dots. The idea is to obtain a binary image with Otsu's threshold then filter out large non-connecting objects using contour area filtering. From were we perform morph open to isolate the dots. Finally we find contours and isolate the dots. Here's the results:
Code
import cv2
import numpy as np
# Load image, grayscale, Otsu's threshold
image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# Filter out large non-connecting objects
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
area = cv2.contourArea(c)
if area < 500:
cv2.drawContours(thresh,[c],0,0,-1)
# Morph open using elliptical shaped kernel
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=3)
# Find circles
cnts = cv2.findContours(opening, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
area = cv2.contourArea(c)
if area > 20 and area < 50:
((x, y), r) = cv2.minEnclosingCircle(c)
cv2.circle(image, (int(x), int(y)), int(r), (36, 255, 12), 2)
cv2.imshow('thresh', thresh)
cv2.imshow('opening', opening)
cv2.imshow('image', image)
cv2.waitKey()

How to group nearby contours in OpenCV Python? - Zebra Crossing detection

I want to detect the zebra crossing lines. I have tried to find out the co-ordinates of zebra crossing line in the image using contour but it gives the output for the distinct white boxes (only white lines in the zebra crossing). But I need the co-ordinates of the entire zebra crossing.
Please let me know the way to group the contours or suggest me another method to detect zebra crossing.
Input image
Output image obtained
Expected output
import cv2
import numpy as np
image = cv2.imread('d.jpg',-1)
paper = cv2.resize(image,(500,500))
ret, thresh_gray = cv2.threshold(cv2.cvtColor(paper, cv2.COLOR_BGR2GRAY),
200, 255, cv2.THRESH_BINARY)
image, contours, hier = cv2.findContours(thresh_gray, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)
for c in contours:
rect = cv2.minAreaRect(c)
box = cv2.boxPoints(rect)
# convert all coordinates floating point values to int
box = np.int0(box)
cv2.drawContours(paper, [box], 0, (0, 255, 0),1)
cv2.imshow('paper', paper)
cv2.imwrite('paper.jpg',paper)
cv2.waitKey(0)
You can closing morphological operation for closing the gaps.
I cat suggest the following stages:
Find contours in thresh_gray.
Erase contours with very small area (noise).
Erase contours with low aspect ratio (assume zebra line must be long and narrow.
Use morphologyEx to perform closing morphological operation - the closing merges close components.
Find contours again in the image after erasing and closing.
At the last stage, ignore small contours.
Here is a working code sample:
import cv2
import numpy as np
image = cv2.imread('d.jpg', -1)
paper = cv2.resize(image, (500,500))
ret, thresh_gray = cv2.threshold(cv2.cvtColor(paper, cv2.COLOR_BGR2GRAY), 200, 255, cv2.THRESH_BINARY)
image, contours, hier = cv2.findContours(thresh_gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Erase small contours, and contours which small aspect ratio (close to a square)
for c in contours:
area = cv2.contourArea(c)
# Fill very small contours with zero (erase small contours).
if area < 10:
cv2.fillPoly(thresh_gray, pts=[c], color=0)
continue
# https://stackoverflow.com/questions/52247821/find-width-and-height-of-rotatedrect
rect = cv2.minAreaRect(c)
(x, y), (w, h), angle = rect
aspect_ratio = max(w, h) / min(w, h)
# Assume zebra line must be long and narrow (long part must be at lease 1.5 times the narrow part).
if (aspect_ratio < 1.5):
cv2.fillPoly(thresh_gray, pts=[c], color=0)
continue
# Use "close" morphological operation to close the gaps between contours
# https://stackoverflow.com/questions/18339988/implementing-imcloseim-se-in-opencv
thresh_gray = cv2.morphologyEx(thresh_gray, cv2.MORPH_CLOSE, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (51,51)));
# Find contours in thresh_gray after closing the gaps
image, contours, hier = cv2.findContours(thresh_gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
for c in contours:
area = cv2.contourArea(c)
# Small contours are ignored.
if area < 500:
cv2.fillPoly(thresh_gray, pts=[c], color=0)
continue
rect = cv2.minAreaRect(c)
box = cv2.boxPoints(rect)
# convert all coordinates floating point values to int
box = np.int0(box)
cv2.drawContours(paper, [box], 0, (0, 255, 0),1)
cv2.imshow('paper', paper)
cv2.imwrite('paper.jpg', paper)
cv2.waitKey(0)
cv2.destroyAllWindows()
thresh_gray before erasing small and squared contours:
thresh_gray after erasing small and squared contours:
thresh_gray after close operation:
Final result:
Remark:
I have some doubts about the benefit of using morphological operation for closing the gaps.
It might be better using a smart logic based on geometry instead.

Counting homogeneous objects along the contour

I'm trying to get amount of objects on the frame by finding their contours with OpenCV.
That's a frame after Canny filter applying:
Then I call findContours() method and leave ones which suitable in size.
When I overlay them on a frame I've got
the following picture.
It can be seen that we've got only full contour objects.
So the question is:
How can we artificially make the boundaries of objects holistic?
I tried to use dilate and erode (result of that) but after that borders of objects are glued together and we can't find their contours any more.
Since the contours are connected together, findContours will detect the connected contours as a single contour instead of individual separated circles. When you have connected contours, a potential approach is to use Watershed to label and detect each contour. Here's the results:
Input image
Output
Code
import cv2
import numpy as np
from skimage.feature import peak_local_max
from skimage.morphology import watershed
from scipy import ndimage
# Load in image, convert to gray scale, and Otsu's threshold
image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
# Compute Euclidean distance from every binary pixel
# to the nearest zero pixel then find peaks
distance_map = ndimage.distance_transform_edt(thresh)
local_max = peak_local_max(distance_map, indices=False, min_distance=20, labels=thresh)
# Perform connected component analysis then apply Watershed
markers = ndimage.label(local_max, structure=np.ones((3, 3)))[0]
labels = watershed(-distance_map, markers, mask=thresh)
# Iterate through unique labels
for label in np.unique(labels):
if label == 0:
continue
# Create a mask
mask = np.zeros(gray.shape, dtype="uint8")
mask[labels == label] = 255
# Find contours and determine contour area
cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
c = max(cnts, key=cv2.contourArea)
color = list(np.random.random(size=3) * 256)
cv2.drawContours(image, [c], -1, color, 4)
cv2.imshow('image', image)
cv2.waitKey()
Here are some other references:
Image segmentation with Watershed Algorithm
Watershed Algorithm: Marker-based Segmentation
How to define the markers for Watershed
Find contours after watershed
It seems like you have a pattern for your objects, and those objects sometimes overlap.
I'd suggest you convolve your image with an object pattern and then process the outcoming scores image.
In more detail:
Suppose for simplicity that your initial image has only one channel. and the object you're looking for looks like this:. this is our pattern. say it's size is [W_p,H_p]
First step: construct new image - scores - where each pixel S in scores = probability that this pixel is the pattern center.
One way to do that is: for each pixel P in the original image, "cut" the [W_p,H_p] patch around P ( e.g. img(Rect(P-W_p/2,P-H_p/2,W_p,H_p))), and subtract patch from pattern to find the "distance" between them (e.g. cv::sum(cv::absdiff(patch, pattern)) function in opencv), and save this sum to S.
Another way to do this is: S = P.clone(); pattern = pattern / cv::sum(pattern);
and then use cv::filter2D for S with pattern...
Now that you have a scores image, you should filter false positives:
1. take the top 2% of the scores( one way is with cv::calcHist)
2. for each pixel that has a neighbor within [W_p,H_p] with a heigher score - turn this pixel to zero !
Now you should remain with image of zeroes where only pattern centers have some value. Hurray!
If you don't know in advance how an object will look like, you can find one object using contours, then 'cut it out' using convex hull of its contour (+ bounding box), and use it as the convolution kernel for finding the rest.

How to filter white bits in a monochrome image that exceed a specified area?

I have this fancy image of "Brainbow image showing map of neuronal circuits of the mouse cerebral cortex"
and I want to count the number of green flashes so ran k-means on it with 15 clusters and I isolated the two colors that together do the job, but i am left with many green streaks/ tails and edges of yellow flashes.
I was hoping to find some algo that thresholds by area and select only the actual green flashes or maybe another aproach where I would not face this problem. I have used python k-means from sklearn.cluster
You could use color thresholding to isolate the green flashes. The idea is to convert the image to HSV format and define a lower/upper color threshold range. This will give you a binary mask. From here we can do additional processing by performing morphological opening with an elliptical shaped kernel to remove noise and tails. Finally we can find contours and filter using contour area with a defined threshold area to only keep the larger blobs. Here's the result:
Count: 116
Code
import numpy as np
import cv2
# Color threshold
image = cv2.imread('1.jpg')
original = image.copy()
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
lower = np.array([42, 67, 0])
upper = np.array([69, 255, 255])
mask = cv2.inRange(hsv, lower, upper)
# Perform morphological operations
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))
opening = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
# Find contours and filter using contour area
cnts = cv2.findContours(opening, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
count = 0
for c in cnts:
area = cv2.contourArea(c)
if area < 5:
cv2.drawContours(opening,[c], -1, (0,0,0), -1)
else:
count += 1
result = cv2.bitwise_and(original,original,mask=opening)
print('Count: {}'.format(count))
cv2.imshow('mask', mask)
cv2.imshow('opening', opening)
cv2.imshow('result', result)
cv2.waitKey()

Resources