How to fix perspective of an image using OpenCV - python

I have an input image which contains a letter inside a box, what I want to do is to fix the perspective of the image so I can easily use it with my letter recognition model.
This is the code that I use for pre-processing:
def preprocess(image):
# Convert to grayscale
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# NOISE REMOVAL - START
# Apply CLAHE (makes the image lightness more uniform)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
image = clahe.apply(image)
cv2.imwrite("preprocess_stages/0_clahe.png", image)
# Apply NlMeansDenoising (removes noise but loses some details)
image = cv2.fastNlMeansDenoising(image, None, 10, 7, 21)
cv2.imwrite("preprocess_stages/1_nl_means_denoising.png", image)
# Apply GaussianBlur with a medium kernel (removes noise but loses some edges)
image = cv2.GaussianBlur(image, (5, 5), 0)
cv2.imwrite("preprocess_stages/2_gaussian_blur.png", image)
# NOISE REMOVAL - END
# Apply OTSU thresholding (we need the letter to be white)
_, image = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
cv2.imwrite("preprocess_stages/3_otsu.png", image)
# Morphological erosion (removes noise and small objects)
kernel = np.ones((10, 10), np.uint8)
image = cv2.erode(image, kernel, iterations=1)
cv2.imwrite("preprocess_stages/4_morphological_erosion.png", image)
return image
I have seen many example on the internet and most of them require the 4 corner points to be provided as input too... there are few examples on how to calculate them and most of them don't work for my case, nonetheless I've tried writing something:
UPDATE: I successfully wrote a working code, even though I changed the Morphological Opening to a simple Erosion since it works better for me.
def fix_perspective_convex_hull(preprocessed):
# RETR_TREE retrieves all of the contours and reconstructs a full hierarchy of nested contours
# used for maximum accuracy (my deduction)
contours, _ = cv2.findContours(preprocessed, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# Draw all contours to a new image
image = cv2.cvtColor(preprocessed, cv2.COLOR_GRAY2BGR)
cv2.drawContours(image, contours, -1, (0, 255, 0), 2)
cv2.imwrite("perspective_stages/0_contours.png", image)
for i, contour in enumerate(contours):
hull = cv2.convexHull(contour)
# Reduce to quadrilateral
epsilon = 0.1 * cv2.arcLength(hull, True)
approximated = cv2.approxPolyDP(hull, epsilon, True)
# Draw approximated contour
image = cv2.cvtColor(preprocessed, cv2.COLOR_GRAY2BGR)
cv2.drawContours(image, [approximated], -1, (255, 0, 0), 1)
cv2.imwrite("perspective_stages/1_approximated_contour_{}.png".format(i), image)
if len(approximated) == 4:
image = cv2.cvtColor(preprocessed, cv2.COLOR_GRAY2BGR)
cv2.drawContours(image, [approximated], -1, (0, 0, 255), 2)
cv2.imwrite("perspective_stages/2_final_contour.png", image)
letter_hull = [a[0] for a in approximated] # Remove extra dimension [[x, y]] -> [x, y]
break
# Create placeholder for rectangle points
rectangle = np.zeros((4, 2), dtype="float32")
# Top-left point has smallest sum
# Bottom-right point has largest sum
# sum = x + y
s = np.sum(letter_hull, axis=1)
rectangle[0] = letter_hull[np.argmin(s)]
rectangle[2] = letter_hull[np.argmax(s)]
# Top-right point has smallest difference
# Bottom-left point has largest difference
# difference = x - y
d = np.diff(letter_hull, axis=1)
rectangle[1] = letter_hull[np.argmin(d)]
rectangle[3] = letter_hull[np.argmax(d)]
# Compute width and height of new image based on points
(top_left, top_right, bottom_right, bottom_left) = rectangle
# Width may be either top-right to top-left or bottom-right to bottom-left
# TL;DR: Pythagorean theorem
width_top = np.sqrt(((top_right[0] - top_left[0]) ** 2) + ((top_right[1] - top_left[1]) ** 2))
width_bottom = np.sqrt(((bottom_right[0] - bottom_left[0]) ** 2) + ((bottom_right[1] - bottom_left[1]) ** 2))
width = max(int(width_top), int(width_bottom))
# Height may be either top-right to bottom-right or top-left to bottom-left
# TL;DR: Pythagorean theorem
height_right = np.sqrt(((top_right[0] - bottom_right[0]) ** 2) + ((top_right[1] - bottom_right[1]) ** 2))
height_left = np.sqrt(((top_left[0] - bottom_left[0]) ** 2) + ((top_left[1] - bottom_left[1]) ** 2))
height = max(int(height_right), int(height_left))
# Create destination points
destination = np.array([
[0, 0],
[width - 1, 0],
[width - 1, height - 1],
[0, height - 1]
], dtype="float32")
# Compute perspective transform matrix
matrix = cv2.getPerspectiveTransform(rectangle, destination)
warped = cv2.warpPerspective(preprocessed, matrix, (width, height))
# Add padding
warped = cv2.copyMakeBorder(warped, 10, 10, 10, 10, cv2.BORDER_CONSTANT, value=(0, 0, 0))
# Return warped image
return warped
Results:
Original
Preprocessed
Finding a quadrilateral
Warped
Inverted and resized for the neural network
UPDATE 2: My code only works for letters without curves, such as the H. When I try to run the code on curve letters (such as the S) I get the following results:

Related

How to approximate the convex hull of a curved letter to a quadrilateral?

I have an input image such as:
Which will turn out after being preprocessed as:
Now, this image is a little bit inclined and what I want to do is to fix it with cv2.warpPerspective().
So, in order to do that, I generate a quadrilateral approximating (with cv2.approxPolyDP()) the convex hull. This is the actual code that I use:
hull = cv2.convexHull(contour)
# Reduce to quadrilateral
epsilon = 0.1 * cv2.arcLength(hull, True)
approximated = cv2.approxPolyDP(hull, epsilon, True)
The convex hull of that preprocessed image looks like this:
But, when I approximate it, I get the following results:
What I want to achieve is something like this:
Here's an example on how it works with a letter without curves:
Which will get transformed into:
In short, the issue lays when approximating the convex hull to a quadrilateral.
How can I achieve such quadrilateral on a letter with curves?
Here's the full code that I have written to do that:
def _perspective_fix_convex_hull(self):
# RETR_TREE retrieves all of the contours and reconstructs a full hierarchy of nested contours
# used for maximum accuracy (my deduction)
contours, _ = cv2.findContours(self.preprocessed_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# Draw all contours to a new image
self.image_contours = cv2.cvtColor(self.preprocessed_image, cv2.COLOR_GRAY2BGR)
cv2.drawContours(self.image_contours, contours, -1, (0, 255, 0), 2)
self.convex_hulls_contours = []
self.approximated_contours = []
for contour in contours:
# Get contour area
area = cv2.contourArea(contour)
if area < 200:
# Too small for a letter
continue
image = cv2.cvtColor(self.preprocessed_image, cv2.COLOR_GRAY2BGR)
hull = cv2.convexHull(contour)
cv2.drawContours(image, [hull], -1, (0, 255, 0), 2)
self.convex_hulls_contours.append(image)
# Reduce to quadrilateral
epsilon = 0.1 * cv2.arcLength(hull, True)
approximated = cv2.approxPolyDP(hull, epsilon, True)
# Draw approximated contour
image = cv2.cvtColor(self.preprocessed_image, cv2.COLOR_GRAY2BGR)
cv2.drawContours(image, [approximated], -1, (255, 0, 0), 1)
self.approximated_contours.append(image)
if len(approximated) == 4:
image = cv2.cvtColor(self.preprocessed_image, cv2.COLOR_GRAY2BGR)
cv2.drawContours(image, [approximated], -1, (0, 0, 255), 2)
self.final_contour = image
letter_hull = [a[0] for a in approximated] # Remove extra dimension [[x, y]] -> [x, y]
break
# Create placeholder for rectangle points
rectangle = np.zeros((4, 2), dtype="float32")
# Top-left point has smallest sum
# Bottom-right point has largest sum
# sum = x + y
s = np.sum(letter_hull, axis=1)
rectangle[0] = letter_hull[np.argmin(s)]
rectangle[2] = letter_hull[np.argmax(s)]
# Top-right point has smallest difference
# Bottom-left point has largest difference
# difference = x - y
d = np.diff(letter_hull, axis=1)
rectangle[1] = letter_hull[np.argmin(d)]
rectangle[3] = letter_hull[np.argmax(d)]
# Compute width and height of new image based on points
(top_left, top_right, bottom_right, bottom_left) = rectangle
# Width may be either top-right to top-left or bottom-right to bottom-left
# TL;DR: Pythagorean theorem
width_top = np.sqrt(((top_right[0] - top_left[0]) ** 2) + ((top_right[1] - top_left[1]) ** 2))
width_bottom = np.sqrt(((bottom_right[0] - bottom_left[0]) ** 2) + ((bottom_right[1] - bottom_left[1]) ** 2))
width = max(int(width_top), int(width_bottom))
# Height may be either top-right to bottom-right or top-left to bottom-left
# TL;DR: Pythagorean theorem
height_right = np.sqrt(((top_right[0] - bottom_right[0]) ** 2) + ((top_right[1] - bottom_right[1]) ** 2))
height_left = np.sqrt(((top_left[0] - bottom_left[0]) ** 2) + ((top_left[1] - bottom_left[1]) ** 2))
height = max(int(height_right), int(height_left))
# Create destination points
destination = np.array([
[0, 0],
[width - 1, 0],
[width - 1, height - 1],
[0, height - 1]
], dtype="float32")
# Compute perspective transform matrix
matrix = cv2.getPerspectiveTransform(rectangle, destination)
warped = cv2.warpPerspective(self.preprocessed_image, matrix, (width, height))
# Add padding
self.warped_image = cv2.copyMakeBorder(warped, 10, 10, 10, 10, cv2.BORDER_CONSTANT, value=(0, 0, 0))

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

Python/OpenCV — Centroid Determination in Bacterial Clusters

I am currently working on developing an algorithm to determine centroid positions from (Brightfield) microscopy images of bacterial clusters. This is currently a major open problem in image processing.
This question is a follow-up to: Python/OpenCV — Matching Centroid Points of Bacteria in Two Images.
Currently, the algorithm is effective for sparse, spaced-out bacteria. However, it becomes totally ineffective when the bacteria become clustered together.
In these images, notice how the bacterial centroids are located effectively.
Bright-Field Image #1
Bright-Field Image #2
Bright-Field Image #3
However, the algorithm fails when the bacteria cluster at varying levels.
Bright-Field Image #4
Bright-Field Image #5
Bright-Field Image #6
Bright-Field Image #7
Bright-Field Image #8
Original Images
Bright-Field Image #1
Bright-Field Image #2
Bright-Field Image #3
Bright-Field Image #4
Bright-Field Image #5
Bright-Field Image #6
Bright-Field Image #7
Bright-Field Image #8
I'd like to optimize my current algorithm so it's more robust for these type of images. This is the program I'm running.
import cv2
import numpy as np
import os
kernel = np.array([[0, 0, 1, 0, 0],
[0, 1, 1, 1, 0],
[1, 1, 1, 1, 1],
[0, 1, 1, 1, 0],
[0, 0, 1, 0, 0]], dtype=np.uint8)
def e_d(image, it):
image = cv2.erode(image, kernel, iterations=it)
image = cv2.dilate(image, kernel, iterations=it)
return image
path = r"(INSERT IMAGE DIRECTORY HERE)"
img_files = [file for file in os.listdir(path)]
def segment_index(index: int):
segment_file(img_files[index])
def segment_file(img_file: str):
img_path = path + "\\" + img_file
print(img_path)
img = cv2.imread(img_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Applying adaptive mean thresholding
th = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 11, 2)
# Removing small noise
th = e_d(th.copy(), 1)
# Finding contours with RETR_EXTERNAL flag and removing undesired contours and
# drawing them on a new image.
cnt, hie = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
cntImg = th.copy()
for contour in cnt:
x, y, w, h = cv2.boundingRect(contour)
# Eliminating the contour if its width is more than half of image width
# (bacteria will not be that big).
if w > img.shape[1] / 2:
continue
cntImg = cv2.drawContours(cntImg, [cv2.convexHull(contour)], -1, 255, -1)
# Removing almost all the remaining noise.
# (Some big circular noise will remain along with bacteria contours)
cntImg = e_d(cntImg, 3)
# Finding new filtered contours again
cnt2, hie2 = cv2.findContours(cntImg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Now eliminating circular type noise contours by comparing each contour's
# extent of overlap with its enclosing circle.
finalContours = [] # This will contain the final bacteria contours
for contour in cnt2:
# Finding minimum enclosing circle
(x, y), radius = cv2.minEnclosingCircle(contour)
center = (int(x), int(y))
radius = int(radius)
# creating a image with only this circle drawn on it(filled with white colour)
circleImg = np.zeros(img.shape, dtype=np.uint8)
circleImg = cv2.circle(circleImg, center, radius, 255, -1)
# creating a image with only the contour drawn on it(filled with white colour)
contourImg = np.zeros(img.shape, dtype=np.uint8)
contourImg = cv2.drawContours(contourImg, [contour], -1, 255, -1)
# White pixels not common in both contour and circle will remain white
# else will become black.
union_inter = cv2.bitwise_xor(circleImg, contourImg)
# Finding ratio of the extent of overlap of contour to its enclosing circle.
# Smaller the ratio, more circular the contour.
ratio = np.sum(union_inter == 255) / np.sum(circleImg == 255)
# Storing only non circular contours(bacteria)
if ratio > 0.55:
finalContours.append(contour)
finalContours = np.asarray(finalContours)
# Finding center of bacteria and showing it.
bacteriaImg = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
for bacteria in finalContours:
M = cv2.moments(bacteria)
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
bacteriaImg = cv2.circle(bacteriaImg, (cx, cy), 5, (0, 0, 255), -1)
cv2.imshow("bacteriaImg", bacteriaImg)
cv2.waitKey(0)
# Segment Each Image
for i in range(len(img_files)):
segment_index(i)
Ideally I would like at least to improve on a couple of the posted images.
The mask is always the weak point in identifying objects, and the most important step. This will improve identifying images with high numbers of bacteria. I have modified your e_d function by adding an OPEN and another ERODE pass with the kernal, and changed the it (number of iterations) variable (to 1, 2 instead of 1,3) for your code to do this. This is by no means a finished effort, but I hope it will give you an idea of what you might try to enhance it further. I used the images you provided, and since they already have a red dot, this may be interfering with my result images... but you can see it is able to identify more bacteria on most. Some of my results show two dots, and the image with only one bacteria, I missed it, each quite possibly because it was already marked. Try it with the raw images and see how it does.
Also, since the bacteria are relatively uniform in both size and shape, I think you could work with the ratio and/or average of height to width of each bacteria to filter out the extreme shapes (small or large) and the skinny, long shapes too. You can measure enough bacteria to see what is the average contour length, or height and width, or height/width ratio, etc., to find reasonable tolerances rather than the proportion to the image size itself. Another suggestion, would be to rethink how you are masking the images all together, possibly to try it in two steps. One to find the boundary of the long shape containing the bacteria, and then to find the bacteria within it. This assumes all of your images will be similar to these, and if that is so, it may help to eliminate the stray hits outside of this boundary, that are never bacteria.
#!usr/bin/env python
# https://stackoverflow.com/questions/63182075/python-opencv-centroid-determination-in-bacterial-clusters
import cv2
import numpy as np
import os
kernel = np.array([[0, 0, 1, 0, 0],
[0, 1, 1, 1, 0],
[1, 1, 1, 1, 1],
[0, 1, 1, 1, 0],
[0, 0, 1, 0, 0]], dtype=np.uint8)
def e_d(image, it):
print(it)
image = cv2.erode(image, kernel, iterations=it)
image = cv2.dilate(image, kernel, iterations=it)
image = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel, iterations = 1)
image = cv2.morphologyEx(image, cv2.MORPH_ERODE, kernel, iterations = 1)
return image
#path = r"(INSERT IMAGE DIRECTORY HERE)"
path = r"E:\stackimages"
img_files = [file for file in os.listdir(path)]
def segment_index(index: int):
segment_file(img_files[index])
def segment_file(img_file: str):
img_path = path + "\\" + img_file
print(img_path)
head, tail = os.path.split(img_path)
img = cv2.imread(img_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.imshow("bacteriaImg-1", img)
cv2.waitKey(0)
# Applying adaptive mean thresholding
th = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 11, 2)
# Removing small noise
th = e_d(th.copy(), 1)
# Finding contours with RETR_EXTERNAL flag and removing undesired contours and
# drawing them on a new image.
cnt, hie = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
cntImg = th.copy()
for contour in cnt:
x, y, w, h = cv2.boundingRect(contour)
# Eliminating the contour if its width is more than half of image width
# (bacteria will not be that big).
if w > img.shape[1] / 2:
continue
else:
cntImg = cv2.drawContours(cntImg, [cv2.convexHull(contour)], -1, 255, -1)
# Removing almost all the remaining noise.
# (Some big circular noise will remain along with bacteria contours)
cntImg = e_d(cntImg, 2)
cv2.imshow("bacteriaImg-2", cntImg)
cv2.waitKey(0)
# Finding new filtered contours again
cnt2, hie2 = cv2.findContours(cntImg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Now eliminating circular type noise contours by comparing each contour's
# extent of overlap with its enclosing circle.
finalContours = [] # This will contain the final bacteria contours
for contour in cnt2:
# Finding minimum enclosing circle
(x, y), radius = cv2.minEnclosingCircle(contour)
center = (int(x), int(y))
radius = int(radius)
# creating a image with only this circle drawn on it(filled with white colour)
circleImg = np.zeros(img.shape, dtype=np.uint8)
circleImg = cv2.circle(circleImg, center, radius, 255, -1)
# creating a image with only the contour drawn on it(filled with white colour)
contourImg = np.zeros(img.shape, dtype=np.uint8)
contourImg = cv2.drawContours(contourImg, [contour], -1, 255, -1)
# White pixels not common in both contour and circle will remain white
# else will become black.
union_inter = cv2.bitwise_xor(circleImg, contourImg)
# Finding ratio of the extent of overlap of contour to its enclosing circle.
# Smaller the ratio, more circular the contour.
ratio = np.sum(union_inter == 255) / np.sum(circleImg == 255)
# Storing only non circular contours(bacteria)
if ratio > 0.55:
finalContours.append(contour)
finalContours = np.asarray(finalContours)
# Finding center of bacteria and showing it.
bacteriaImg = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
for bacteria in finalContours:
M = cv2.moments(bacteria)
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
bacteriaImg = cv2.circle(bacteriaImg, (cx, cy), 5, (0, 0, 255), -1)
cv2.imshow("bacteriaImg", bacteriaImg)
cv2.waitKey(0)
# Segment Each Image
for i in range(len(img_files)):
segment_index(i)
Here's some code that you can try and see if it works for you. It uses an alternative approach to segmenting images. You can fiddle around with parameters to see what combination gives you most acceptable results.
import numpy as np
import cv2
import matplotlib.pyplot as plt
# Adaptive threshold params
gw = 11
bs = 7
offset = 5
bact_aspect_min = 2.0
bact_aspect_max = 10.0
bact_area_min = 20 # in pixels
bact_area_max = 1000
url = "/path/to/image"
img_color = cv2.imread(url)
img = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY)
rows, cols = img.shape
img_eq = img.copy()
cv2.equalizeHist(img, img_eq)
img_blur = cv2.medianBlur(img_eq, gw)
th = cv2.adaptiveThreshold(img_blur, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, bs, offset)
_, contours, hier = cv2.findContours(th.copy(), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
for i in range(len(contours)):
# Filter closed contours
rect = cv2.minAreaRect(contours[i])
area = cv2.contourArea(contours[i])
(x, y), (width, height), angle = rect
if min(width, height) == 0:
continue
aspect_ratio = max(width, height) / min(width, height)
if hier[0][i][3] != -1 and \
bact_aspect_min < aspect_ratio < bact_aspect_max and \
bact_area_min < area < bact_area_max:
M = cv2.moments(contours[i])
cx = int(M['m10'] / M['m00'])
cy = int(M['m01'] / M['m00'])
img_color = cv2.circle(img_color, (cx, cy), 3, (255, 0, 0), cv2.FILLED)
plt.imshow(img_color)
It seems that your bacterias seem fused/overlapped in most of the images and it is extremely hard to gauge their size when they are fused and to separate them. Best way is to run this code snippet in Jupyter/ipywidgets with a range of parameter values and see what works best. Good luck!
EDIT 1
I have updated the code to use a slight bit different technique and idea. Basically using l2 contours (holes) to ascertain bacteria, this is much more in line with the shape of the bacteria. You can, again, fiddle around with the parameters to see what works best. Set of parameters in the code gave me satisfactory results. You may want to filter the image a bit more to remove false positives.
Couple of other tricks can be used in addition to the one in the latest code:
Try out ADAPTIVE_THRESH_GAUSSIAN_C
Try equalized image without blurring
Use level 1 contours along with level 2
Use different size constraints for l1 and l2 contours.
I think a combination of all these should provide you with a pretty decent result.

How to detect Sudoku grid board in OpenCV

I'm working on a personal project using opencv in python. Want to detect a sudoku grid.
The original image is:
So far I have created this:
Then tried to select a big blob. Result may be similar to this:
I got a black image as result:
The code is:
import cv2
import numpy as np
def find_biggest_blob(outerBox):
max = -1
maxPt = (0, 0)
h, w = outerBox.shape[:2]
mask = np.zeros((h + 2, w + 2), np.uint8)
for y in range(0, h):
for x in range(0, w):
if outerBox[y, x] >= 128:
area = cv2.floodFill(outerBox, mask, (x, y), (0, 0, 64))
#cv2.floodFill(outerBox, mask, maxPt, (255, 255, 255))
image_path = 'Images/Results/sudoku-find-biggest-blob.jpg'
cv2.imwrite(image_path, outerBox)
cv2.imshow(image_path, outerBox)
def main():
image = cv2.imread('Images/Test/sudoku-grid-detection.jpg', 0)
find_biggest_blob(image)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == '__main__':
main()
The code in repl is: https://repl.it/#gmunumel/SudokuSolver
Any idea?
Here's an approach:
Convert image to grayscale and median blur to smooth image
Adaptive threshold to obtain binary image
Find contours and filter for largest contour
Perform perspective transform to obtain top-down view
After converting to grayscale and median blurring, we adaptive threshold to obtain a binary image
Next we find contours and filter using contour area. Here's the detected board
Now to get a top-down view of the image, we perform a perspective transform. Here's the result
import cv2
import numpy as np
def perspective_transform(image, corners):
def order_corner_points(corners):
# Separate corners into individual points
# Index 0 - top-right
# 1 - top-left
# 2 - bottom-left
# 3 - bottom-right
corners = [(corner[0][0], corner[0][1]) for corner in corners]
top_r, top_l, bottom_l, bottom_r = corners[0], corners[1], corners[2], corners[3]
return (top_l, top_r, bottom_r, bottom_l)
# Order points in clockwise order
ordered_corners = order_corner_points(corners)
top_l, top_r, bottom_r, bottom_l = ordered_corners
# Determine width of new image which is the max distance between
# (bottom right and bottom left) or (top right and top left) x-coordinates
width_A = np.sqrt(((bottom_r[0] - bottom_l[0]) ** 2) + ((bottom_r[1] - bottom_l[1]) ** 2))
width_B = np.sqrt(((top_r[0] - top_l[0]) ** 2) + ((top_r[1] - top_l[1]) ** 2))
width = max(int(width_A), int(width_B))
# Determine height of new image which is the max distance between
# (top right and bottom right) or (top left and bottom left) y-coordinates
height_A = np.sqrt(((top_r[0] - bottom_r[0]) ** 2) + ((top_r[1] - bottom_r[1]) ** 2))
height_B = np.sqrt(((top_l[0] - bottom_l[0]) ** 2) + ((top_l[1] - bottom_l[1]) ** 2))
height = max(int(height_A), int(height_B))
# Construct new points to obtain top-down view of image in
# top_r, top_l, bottom_l, bottom_r order
dimensions = np.array([[0, 0], [width - 1, 0], [width - 1, height - 1],
[0, height - 1]], dtype = "float32")
# Convert to Numpy format
ordered_corners = np.array(ordered_corners, dtype="float32")
# Find perspective transform matrix
matrix = cv2.getPerspectiveTransform(ordered_corners, dimensions)
# Return the transformed image
return cv2.warpPerspective(image, matrix, (width, height))
image = cv2.imread('1.jpg')
original = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.medianBlur(gray, 3)
thresh = cv2.adaptiveThreshold(blur,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV,11,3)
cnts = cv2.findContours(thresh, 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)
for c in cnts:
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.015 * peri, True)
transformed = perspective_transform(original, approx)
break
cv2.imshow('transformed', transformed)
cv2.imwrite('board.png', transformed)
cv2.waitKey()
Here is my solution that will generalize to any image whether it is warped or not.
Convert the image to grayscale
Apply adaptive thresholding to convert the image to binary
(Adaptive thresholding works better than normal thresholding because the original image can have different lighting at different areas)
Identify the Corners of the large square
Perspective transform of the image to the final square image
Depending on the amount of skewness of the original image the corners identified may be out of order, do we need to arrange them in the correct order. the method used here is to identify the centroid of the large square and identify the order of the corners from there
Here is the code:
import cv2
import numpy as np
# Helper functions for getting square image
def euclidian_distance(point1, point2):
# Calcuates the euclidian distance between the point1 and point2
#used to calculate the length of the four sides of the square
distance = np.sqrt((point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2)
return distance
def order_corner_points(corners):
# The points obtained from contours may not be in order because of the skewness of the image, or
# because of the camera angle. This function returns a list of corners in the right order
sort_corners = [(corner[0][0], corner[0][1]) for corner in corners]
sort_corners = [list(ele) for ele in sort_corners]
x, y = [], []
for i in range(len(sort_corners[:])):
x.append(sort_corners[i][0])
y.append(sort_corners[i][1])
centroid = [sum(x) / len(x), sum(y) / len(y)]
for _, item in enumerate(sort_corners):
if item[0] < centroid[0]:
if item[1] < centroid[1]:
top_left = item
else:
bottom_left = item
elif item[0] > centroid[0]:
if item[1] < centroid[1]:
top_right = item
else:
bottom_right = item
ordered_corners = [top_left, top_right, bottom_right, bottom_left]
return np.array(ordered_corners, dtype="float32")
def image_preprocessing(image, corners):
# This function undertakes all the preprocessing of the image and return
ordered_corners = order_corner_points(corners)
print("ordered corners: ", ordered_corners)
top_left, top_right, bottom_right, bottom_left = ordered_corners
# Determine the widths and heights ( Top and bottom ) of the image and find the max of them for transform
width1 = euclidian_distance(bottom_right, bottom_left)
width2 = euclidian_distance(top_right, top_left)
height1 = euclidian_distance(top_right, bottom_right)
height2 = euclidian_distance(top_left, bottom_right)
width = max(int(width1), int(width2))
height = max(int(height1), int(height2))
# To find the matrix for warp perspective function we need dimensions and matrix parameters
dimensions = np.array([[0, 0], [width, 0], [width, width],
[0, width]], dtype="float32")
matrix = cv2.getPerspectiveTransform(ordered_corners, dimensions)
# Return the transformed image
transformed_image = cv2.warpPerspective(image, matrix, (width, width))
#Now, chances are, you may want to return your image into a specific size. If not, you may ignore the following line
transformed_image = cv2.resize(transformed_image, (252, 252), interpolation=cv2.INTER_AREA)
return transformed_image
# main function
def get_square_box_from_image(image):
# This function returns the top-down view of the puzzle in grayscale.
#
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.medianBlur(gray, 3)
adaptive_threshold = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 11, 3)
corners = cv2.findContours(adaptive_threshold, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
corners = corners[0] if len(corners) == 2 else corners[1]
corners = sorted(corners, key=cv2.contourArea, reverse=True)
for corner in corners:
length = cv2.arcLength(corner, True)
approx = cv2.approxPolyDP(corner, 0.015 * length, True)
print(approx)
puzzle_image = image_preprocessing(image, approx)
break
return puzzle_image
# Call the get_square_box_from_image method on any sudoku image to get the top view of the puzzle
original = cv2.imread("large_puzzle.jpg")
sudoku = get_square_box_from_image(original)
Here are the results from the given image and a custom example

How to compute coordinates of rectange and then do perspective transformation using opencv and python

Original pic
after canny pic
pic after perspective tranform
Hi,
I am doing a small OCR POC.See the original pic, I only care about the content inside the nameplate.Before recognising characters, I need to do perspective transformation for this pictures to increase correctness. See the second picutures, I already did canny to get the contours of the rectangle.
I want to get the coordinates of the 4 corners of the rectangle(labled in red),so that I can derived the matrix and do perspective transfromation. The last picture is the output I want.
I am new to opencv... can anybody give me some idea regarding how to get the coordinates of 4 corners? I have googled and learn some terms, such as hough transformation?
Is it a good way to detect line and then computer the position of the corners?
It would be great if somebody can show me some python code to do this, thanks in advance.
/* below is my currunt code
# coding:utf8
import cv2
import numpy as np
import sys
if __name__ == '__main__':
imagePath = sys.argv[1]
img = cv2.imread(imagePath)
img = cv2.GaussianBlur(img,(3,3),0)
canny = cv2.Canny(img, 50, 150)
#element2 = cv2.getStructuringElement(cv2.MORPH_RECT, (4, 4))
#dilation = cv2.dilate(canny, element2, iterations = 1)
cv2.imwrite("canny.jpg", dilation)
cv2.waitKey(0)
cv2.destroyAllWindows()
I adapted the code from pyimagesearch to work in python 3.5 and opencv 3.3
import os
import cv2
import imutils
import numpy as np
import pytesseract
from PIL import Image
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
def image_process(image_path):
# Open image
image = cv2.imread(image_path)
ratio = image.shape[0] / 500.0
orig = image.copy()
image = imutils.resize(image, height=500)
# Canny edge detect
edged = cv2.Canny(image, 75, 200)
# Find the countours
img, cnts, hierarchy = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
# Find the contours that are the largest (not sure if applies to this project) and has four components (is a rectangle)
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:5]
for c in cnts:
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.02 * peri, True)
if len(approx) == 4:
screenCnt = approx
break
warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio)
return warped
def main():
image_path = None # You're going to need to change this
image = image_process(image_path)
cv2.imshow('image', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == '__main__':
main()

Categories

Resources