So I have this image:
And I need calculate area of an especific part, so I made this code:
# packages
from imutils import perspective
from imutils import contours
import numpy as np
import imutils
import cv2
image = cv2.imread('image1.jpg')
# load the image, convert it to grayscale, and blur it slightly
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (7, 7), 0)
edged = cv2.Canny(gray, 80, 150)
edged = cv2.dilate(edged, None, iterations=1)
edged = cv2.erode(edged, None, iterations=1)
# find contours
cnts = cv2.findContours(edged.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
# sort the contours from left-to-right and initialize the
(cnts, _) = contours.sort_contours(cnts)
pixelsPerMetric = None
# draw the necessary contour
cv2.drawContours(image, max(cnts, key=cv2.contourArea), -1, (0, 0, 255), 5)
cv2.imshow("Image", image)
cv2.waitKey(0)
And I obtained this image:
And I would like to measure the area (mm²) with red contourn. As reference I have the coin (572.55 mm), and other question. Is it possible measure the red and black proportion inside the red contourn. Some suggestion.
I would go like this. Essentially:
in the first part you will work on the external contours: you will find the coin and the slice external borders
the coin area will help you measure the area in cm^2 of the entire slice, if you want it in mm^2 you can multiply that value by 100
at that point you just need to quantify the lean part of the slice, which was inspired by this great snippet and fmw42's great comment above
the fat part of the slice can be found by difference, i.e. with my values I get that 57.71% of the slice is lean, so the remaining 42.29% is fat
if you don't want the entire lean part as in this snippet just calculate the area of your red contour, you seem ready to do it: keep in mind that the slice would be fatter than what I calculated here
Without further ado, here is the code:
import cv2
import numpy as np
import math
# input image
path = "image1.jpg"
# 1 EUR coin diameter in cm
coinDiameter = 2.325
# real area for the coin in cm^2
coinArea = (coinDiameter/2)**2 * math.pi
# initializing the multiplying factor for real size
realAreaPerPixel = 1
# pixel to cm^2
def pixelToArea(objectSizeInPixel, coinSizeInPixel):
# how many cm^2 per pixel?
realAreaPerPixel = coinArea / coinSizeInPixel
print("realAreaPerPixel: ", realAreaPerPixel)
# object area in cm^2
objectArea = realAreaPerPixel * objectSizeInPixel
return objectArea
# finding coin and steak contours
def getContours(img, imgContour):
# find all the contours from the B&W image
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# needed to filter only our contours of interest
finalContours = []
# for each contour found
for cnt in contours:
# cv2.drawContours(imgContour, cnt, -1, (255, 0, 255), 2)
# find its area in pixel
area = cv2.contourArea(cnt)
print("Detected Contour with Area: ", area)
# minimum area value is to be fixed as the one that leaves the coin as the small object on the scene
if (area > 5000):
perimeter = cv2.arcLength(cnt, True)
# smaller epsilon -> more vertices detected [= more precision]
epsilon = 0.002*perimeter
# check how many vertices
approx = cv2.approxPolyDP(cnt, epsilon, True)
#print(len(approx))
finalContours.append([len(approx), area, approx, cnt])
# we want only two objects here: the coin and the meat slice
print("---\nFinal number of External Contours: ", len(finalContours))
# so at this point finalContours should have only two elements
# sorting in ascending order depending on the area
finalContours = sorted(finalContours, key = lambda x:x[1], reverse=False)
# drawing contours for the final objects
for con in finalContours:
cv2.drawContours(imgContour, con[3], -1, (0, 0, 255), 3)
return imgContour, finalContours
# sourcing the input image
img = cv2.imread(path)
cv2.imshow("Starting image", img)
cv2.waitKey()
# blurring
imgBlur = cv2.GaussianBlur(img, (7, 7), 1)
# graying
imgGray = cv2.cvtColor(imgBlur, cv2.COLOR_BGR2GRAY)
# canny
imgCanny = cv2.Canny(imgGray, 255, 195)
kernel = np.ones((2, 2))
imgDil = cv2.dilate(imgCanny, kernel, iterations = 3)
# cv2.imshow("Diluted", imgDil)
imgThre = cv2.erode(imgDil, kernel, iterations = 3)
imgFinalContours, finalContours = getContours(imgThre, img)
# first final contour has the area of the coin in pixel
coinPixelArea = finalContours[0][1]
print("Coin Area in pixel", coinPixelArea)
# second final contour has the area of the meat slice in pixel
slicePixelArea = finalContours[1][1]
print("Entire Slice Area in pixel", slicePixelArea)
# let's go cm^2
print("Coin Area in cm^2:", coinArea)
print("Entire Slice Area in cm^2:", pixelToArea(slicePixelArea, coinPixelArea))
# show the contours on the unfiltered starting image
cv2.imshow("Final External Contours", imgFinalContours)
cv2.waitKey()
# now let's detect and quantify the lean part
# convert to HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# set lower and upper color limits
lowerVal = np.array([0, 159, 160])
upperVal = np.array([101, 255, 253])
# Threshold the HSV image to get only red colors
mask = cv2.inRange(hsv, lowerVal, upperVal)
# apply mask to original image - this shows the red with black blackground
final = cv2.bitwise_and(img, img, mask= mask)
# show selection
cv2.imshow("Lean Cut", final)
cv2.waitKey()
# convert it to grayscale because countNonZero() wants 1 channel images
gray = cv2.cvtColor(final, cv2.COLOR_BGR2GRAY)
# cv2.imshow("Gray", gray)
# cv2.waitKey()
meatyPixelArea = cv2.countNonZero(gray)
print("Red Area in pixel: ", meatyPixelArea)
print("Red Area in cm^2: ", pixelToArea(meatyPixelArea, coinPixelArea))
# finally the body-fat ratio calculation
print("Body-Fat Ratio: ", meatyPixelArea/slicePixelArea*100, "%")
cv2.destroyAllWindows()
Related
I am trying to make a perspective transformation with OpenCV in python. I want to align the image and find the coordinates of top left top right and bottom left and bottom right contours. here is my code so far where I can identify all the contours.
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray,(3,3),0)
edges = cv2.Canny(blur,50,100)
contours, hierarchy = cv2.findContours(edges,cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) #find contours
cv2.drawContours(img,contours,-1,(0,255,0),2)
cv2.imshow('Contours',img)
cv2.waitKey(0)
Here is an image of what I get.
I'd be very thankful if you could try to help me solve this problem!
Original Image
Demo Inputs:
Demo Inputs
Demo Input 1:
Demo Input 1
Demo Input 2:
Demo Input 2
Demo Input 3:
Demo Input 3
Demo Input 4:
Demo Input 4
Desired Output:
Desired Output
Here is one way to do that in Python/OpenCV.
Read the input
Read the template (for its dimension)
Convert the input to gray and threshold
Pad the threshold (to preserve the corners when apply morphology)
Apply morphology close
Remove the padding
Get the largest external contour
Get its perimeter and approximate to 4 corners as the input corners for the warping
Get the output corners for the warping from the dimensions of the template
Get the perspective transformation matrix
Warp the input to match the template
Save the results
Input:
Template:
import cv2
import numpy as np
# read image
img = cv2.imread("omr_test.jpg")
hh, ww = img.shape[:2]
# read template
template = cv2.imread("omr_template.jpg")
ht, wd = template.shape[:2]
# convert img to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# do otsu threshold on gray image
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1]
# pad thresh with black to preserve corners when apply morphology
pad = cv2.copyMakeBorder(thresh, 20, 20, 20, 20, borderType=cv2.BORDER_CONSTANT, value=0)
# apply morphology
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15,15))
morph = cv2.morphologyEx(pad, cv2.MORPH_CLOSE, kernel)
# remove padding
morph = morph[20:hh+20, 20:ww+20]
# get largest external contour
contours = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
big_contour = max(contours, key=cv2.contourArea)
# get perimeter and approximate a polygon
peri = cv2.arcLength(big_contour, True)
corners = cv2.approxPolyDP(big_contour, 0.04 * peri, True)
# draw polygon on input image from detected corners
polygon = img.copy()
cv2.polylines(polygon, [corners], True, (0,255,0), 2, cv2.LINE_AA)
# Alternate: cv2.drawContours(page,[corners],0,(0,0,255),1)
# print the number of found corners and the corner coordinates
# They seem to be listed counter-clockwise from the top most corner
print(len(corners))
print(corners)
# reformat input corners to x,y list
icorners = []
for corner in corners:
pt = [ corner[0][0],corner[0][1] ]
icorners.append(pt)
icorners = np.float32(icorners)
# get corresponding output corners form width and height
ocorners = [ [0,0], [0,ht], [wd,ht], [wd,0] ]
ocorners = np.float32(ocorners)
# get perspective tranformation matrix
M = cv2.getPerspectiveTransform(icorners, ocorners)
# do perspective
warped = cv2.warpPerspective(img, M, (wd, ht))
# write results
cv2.imwrite("omr_test_thresh.jpg", thresh)
cv2.imwrite("omr_test_morph.jpg", morph)
cv2.imwrite("omr_test_polygon.jpg", polygon)
cv2.imwrite("omr_test_warped.jpg", warped)
# display it
cv2.imshow("thresh", thresh)
cv2.imshow("pad", pad)
cv2.imshow("morph", morph)
cv2.imshow("polygon", polygon)
cv2.imshow("warped", warped)
cv2.waitKey(0)
Threshold Image:
Morphology Image:
Polygon Image:
Warped Input:
Here is a correction that solves whether the input image is rotated cw or ccw in Python/OpenCV.
Read the input
Read the template (for its dimensions)
Convert the input to gray and threshold
Pad the threshold (to preserve the corners when applying morphology)
Apply morphology close
Remove the padding
Get the largest external contour
Get its perimeter and approximate to 4 corners as the input corners for the warping
Sort the corners by Y and test the first two sorted corners for X. If diff=X2-X1 is negative, then the output corners need to be list in a slightly differently order.
Get the output corners for the warping from the dimensions of the template and the diff of the sorted corners.
Get the perspective transformation matrix
Warp the input to match the template
Save the results
Input 1 (cw rotated):
:
Input 2 (ccw rotated):
import cv2
import numpy as np
# read image
#img = cv2.imread("omr_test.jpg")
img = cv2.imread("omr_test2.jpg")
hh, ww = img.shape[:2]
# read template
template = cv2.imread("omr_template.jpg")
ht, wd = template.shape[:2]
# convert img to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# do otsu threshold on gray image
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1]
# pad thresh with black to preserve corners when apply morphology
pad = cv2.copyMakeBorder(thresh, 20, 20, 20, 20, borderType=cv2.BORDER_CONSTANT, value=0)
# apply morphology
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15,15))
morph = cv2.morphologyEx(pad, cv2.MORPH_CLOSE, kernel)
# remove padding
morph = morph[20:hh+20, 20:ww+20]
# get largest external contour
contours = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
big_contour = max(contours, key=cv2.contourArea)
# get perimeter and approximate a polygon
peri = cv2.arcLength(big_contour, True)
corners = cv2.approxPolyDP(big_contour, 0.04 * peri, True)
# draw polygon on input image from detected corners
polygon = img.copy()
cv2.polylines(polygon, [corners], True, (0,255,0), 2, cv2.LINE_AA)
# print the number of found corners and the corner coordinates
# They seem to be listed counter-clockwise from the top most corner
print(len(corners))
print(corners)
# reformat input corners to x,y list
sortcorners = []
for corner in corners:
pt = [ corner[0][0],corner[0][1] ]
sortcorners.append(pt)
icorners = np.float32(sortcorners)
# sort corners on y
def takeSecond(elem):
return elem[1]
sortcorners.sort(key=takeSecond)
# check if second corner x is left or right of first corner x
x1 = sortcorners[0][0]
x2 = sortcorners[1][0]
diff = x2 - x1
print(x1, x2)
# get corresponding output corners from width and height
if diff >= 0:
ocorners = [ [0,0], [0,ht], [wd,ht], [wd,0] ]
else:
ocorners = [ [wd,0], [0,0], [0,ht], [wd,ht]]
ocorners = np.float32(ocorners)
# get perspective tranformation matrix
M = cv2.getPerspectiveTransform(icorners, ocorners)
# do perspective
warped = cv2.warpPerspective(img, M, (wd, ht))
# write results
cv2.imwrite("omr_test2_thresh.jpg", thresh)
cv2.imwrite("omr_test2_morph.jpg", morph)
cv2.imwrite("omr_test2_polygon.jpg", polygon)
cv2.imwrite("omr_test2_warped.jpg", warped)
# display it
cv2.imshow("thresh", thresh)
cv2.imshow("pad", pad)
cv2.imshow("morph", morph)
cv2.imshow("polygon", polygon)
cv2.imshow("warped", warped)
cv2.waitKey(0)
Result for first input (cw rotated):
Result for second input (ccw rotated):
So I have this image:
And I need calculate area of an especific part, so I made this code:
# packages
from imutils import perspective
from imutils import contours
import numpy as np
import imutils
import cv2
image = cv2.imread('image1.jpg')
# load the image, convert it to grayscale, and blur it slightly
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (7, 7), 0)
edged = cv2.Canny(gray, 80, 150)
edged = cv2.dilate(edged, None, iterations=1)
edged = cv2.erode(edged, None, iterations=1)
# find contours
cnts = cv2.findContours(edged.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = imutils.grab_contours(cnts)
# sort the contours from left-to-right and initialize the
(cnts, _) = contours.sort_contours(cnts)
pixelsPerMetric = None
# draw the necessary contour
cv2.drawContours(image, max(cnts, key=cv2.contourArea), -1, (0, 0, 255), 5)
cv2.imshow("Image", image)
cv2.waitKey(0)
And I obtained this image:
And I would like to measure the area (mm²) with red contourn. As reference I have the coin (572.55 mm), and other question. Is it possible measure the red and black proportion inside the red contourn. Some suggestion.
I would go like this. Essentially:
in the first part you will work on the external contours: you will find the coin and the slice external borders
the coin area will help you measure the area in cm^2 of the entire slice, if you want it in mm^2 you can multiply that value by 100
at that point you just need to quantify the lean part of the slice, which was inspired by this great snippet and fmw42's great comment above
the fat part of the slice can be found by difference, i.e. with my values I get that 57.71% of the slice is lean, so the remaining 42.29% is fat
if you don't want the entire lean part as in this snippet just calculate the area of your red contour, you seem ready to do it: keep in mind that the slice would be fatter than what I calculated here
Without further ado, here is the code:
import cv2
import numpy as np
import math
# input image
path = "image1.jpg"
# 1 EUR coin diameter in cm
coinDiameter = 2.325
# real area for the coin in cm^2
coinArea = (coinDiameter/2)**2 * math.pi
# initializing the multiplying factor for real size
realAreaPerPixel = 1
# pixel to cm^2
def pixelToArea(objectSizeInPixel, coinSizeInPixel):
# how many cm^2 per pixel?
realAreaPerPixel = coinArea / coinSizeInPixel
print("realAreaPerPixel: ", realAreaPerPixel)
# object area in cm^2
objectArea = realAreaPerPixel * objectSizeInPixel
return objectArea
# finding coin and steak contours
def getContours(img, imgContour):
# find all the contours from the B&W image
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# needed to filter only our contours of interest
finalContours = []
# for each contour found
for cnt in contours:
# cv2.drawContours(imgContour, cnt, -1, (255, 0, 255), 2)
# find its area in pixel
area = cv2.contourArea(cnt)
print("Detected Contour with Area: ", area)
# minimum area value is to be fixed as the one that leaves the coin as the small object on the scene
if (area > 5000):
perimeter = cv2.arcLength(cnt, True)
# smaller epsilon -> more vertices detected [= more precision]
epsilon = 0.002*perimeter
# check how many vertices
approx = cv2.approxPolyDP(cnt, epsilon, True)
#print(len(approx))
finalContours.append([len(approx), area, approx, cnt])
# we want only two objects here: the coin and the meat slice
print("---\nFinal number of External Contours: ", len(finalContours))
# so at this point finalContours should have only two elements
# sorting in ascending order depending on the area
finalContours = sorted(finalContours, key = lambda x:x[1], reverse=False)
# drawing contours for the final objects
for con in finalContours:
cv2.drawContours(imgContour, con[3], -1, (0, 0, 255), 3)
return imgContour, finalContours
# sourcing the input image
img = cv2.imread(path)
cv2.imshow("Starting image", img)
cv2.waitKey()
# blurring
imgBlur = cv2.GaussianBlur(img, (7, 7), 1)
# graying
imgGray = cv2.cvtColor(imgBlur, cv2.COLOR_BGR2GRAY)
# canny
imgCanny = cv2.Canny(imgGray, 255, 195)
kernel = np.ones((2, 2))
imgDil = cv2.dilate(imgCanny, kernel, iterations = 3)
# cv2.imshow("Diluted", imgDil)
imgThre = cv2.erode(imgDil, kernel, iterations = 3)
imgFinalContours, finalContours = getContours(imgThre, img)
# first final contour has the area of the coin in pixel
coinPixelArea = finalContours[0][1]
print("Coin Area in pixel", coinPixelArea)
# second final contour has the area of the meat slice in pixel
slicePixelArea = finalContours[1][1]
print("Entire Slice Area in pixel", slicePixelArea)
# let's go cm^2
print("Coin Area in cm^2:", coinArea)
print("Entire Slice Area in cm^2:", pixelToArea(slicePixelArea, coinPixelArea))
# show the contours on the unfiltered starting image
cv2.imshow("Final External Contours", imgFinalContours)
cv2.waitKey()
# now let's detect and quantify the lean part
# convert to HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# set lower and upper color limits
lowerVal = np.array([0, 159, 160])
upperVal = np.array([101, 255, 253])
# Threshold the HSV image to get only red colors
mask = cv2.inRange(hsv, lowerVal, upperVal)
# apply mask to original image - this shows the red with black blackground
final = cv2.bitwise_and(img, img, mask= mask)
# show selection
cv2.imshow("Lean Cut", final)
cv2.waitKey()
# convert it to grayscale because countNonZero() wants 1 channel images
gray = cv2.cvtColor(final, cv2.COLOR_BGR2GRAY)
# cv2.imshow("Gray", gray)
# cv2.waitKey()
meatyPixelArea = cv2.countNonZero(gray)
print("Red Area in pixel: ", meatyPixelArea)
print("Red Area in cm^2: ", pixelToArea(meatyPixelArea, coinPixelArea))
# finally the body-fat ratio calculation
print("Body-Fat Ratio: ", meatyPixelArea/slicePixelArea*100, "%")
cv2.destroyAllWindows()
This might be a bit too "general" question, but how do I perform GRAYSCALE image segmentation and keep the largest contour? I am trying to remove background noise (i.e. labels) from breast mammograms, but I am not successful. Here is the original image:
First, I applied AGCWD algorithm (based on paper "Efficient Contrast Enhancement Using Adaptive Gamma Correction With Weighting Distribution") in order to get better contrast of the image pixels, like so:
Afterwards, I tried executing following steps:
Image segmentation using OpenCV's KMeans clustering algorithm:
enhanced_image_cpy = enhanced_image.copy()
reshaped_image = np.float32(enhanced_image_cpy.reshape(-1, 1))
number_of_clusters = 10
stop_criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.1)
ret, labels, clusters = cv2.kmeans(reshaped_image, number_of_clusters, None, stop_criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
clusters = np.uint8(clusters)
Canny Edge Detection:
removed_cluster = 1
canny_image = np.copy(enhanced_image_cpy).reshape((-1, 1))
canny_image[labels.flatten() == removed_cluster] = [0]
canny_image = cv2.Canny(canny_image,100,200).reshape(enhanced_image_cpy.shape)
show_images([canny_image])
Find and Draw Contours:
initial_contours_image = np.copy(canny_image)
initial_contours_image_bgr = cv2.cvtColor(initial_contours_image, cv2.COLOR_GRAY2BGR)
_, thresh = cv2.threshold(initial_contours_image, 50, 255, 0)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(initial_contours_image_bgr, contours, -1, (255,0,0), cv2.CHAIN_APPROX_SIMPLE)
show_images([initial_contours_image_bgr])
Here is how image looks after I draw 44004 contours:
I am not sure how can I get one BIG contour, instead of 44004 small ones. Any ideas how to fix my approach, or possibly any ideas on using alternative approach to get rid of label in top right corner.
Thanks in advance!
Here is one way to do that in Python OpenCV
Read the image
Threshold and invert so the borders are black
Remove the borders of the image as follows (so as to make it easier to get the relevant contours later):
Count the number of non-zero pixels in each column and find the first and last column that have counts greater than 0
Count the number of non-zero pixels in each row and find the first and last row that have counts greater than 0
Crop the image to remove the borders
Crop thresh1 and invert to make thresh2
Get the external contours from thresh2
Find the largest contour and draw as white filled on a black background as a mask
Make all pixels in the cropped image black where the mask is black
Save the results -
Input:
import cv2
import numpy as np
# read image
img = cv2.imread('xray3.png')
# convert to gray
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# threshold and invert
thresh1 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)[1]
thresh1 = 255 - thresh1
# remove borders
# count number of white pixels in columns as new 1D array
count_cols = np.count_nonzero(thresh1, axis=0)
# get first and last x coordinate where black
first_x = np.where(count_cols>0)[0][0]
last_x = np.where(count_cols>0)[0][-1]
print(first_x,last_x)
# count number of white pixels in rows as new 1D array
count_rows = np.count_nonzero(thresh1, axis=1)
# get first and last y coordinate where black
first_y = np.where(count_rows>0)[0][0]
last_y = np.where(count_rows>0)[0][-1]
print(first_y,last_y)
# crop image
crop = img[first_y:last_y+1, first_x:last_x+1]
# crop thresh1 and invert
thresh2 = thresh1[first_y:last_y+1, first_x:last_x+1]
thresh2 = 255 - thresh2
# get external contours and keep largest one
contours = cv2.findContours(thresh2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
big_contour = max(contours, key=cv2.contourArea)
# make mask from contour
mask = np.zeros_like(thresh2 , dtype=np.uint8)
cv2.drawContours(mask, [big_contour], 0, 255, -1)
# make crop black everywhere except where largest contour is white in mask
result = crop.copy()
result[mask==0] = (0,0,0)
# write result to disk
cv2.imwrite("xray3_thresh1.jpg", thresh1)
cv2.imwrite("xray3_crop.jpg", crop)
cv2.imwrite("xray3_thresh2.jpg", thresh2)
cv2.imwrite("xray3_mask.jpg", mask)
cv2.imwrite("xray3_result.png", result)
# display it
cv2.imshow("thresh1", thresh1)
cv2.imshow("crop", crop)
cv2.imshow("thresh2", thresh2)
cv2.imshow("mask", mask)
cv2.imshow("result", result)
cv2.waitKey(0)
Threshold 1 image:
Cropped image:
Threshold 2 image:
Mask image:
Result:
I am trying to detect the outer boundary of the circular object in the images below:
I tried OpenCV's Hough Circle, but the code is not working for every image. I also tried to adjust parameters such as minRadius and maxRadius in Hough Circle but its not working on every image.
The aim is to detect the object from the image and crop it.
Expected output:
Source code:
import imutils
import cv2
import numpy as np
from matplotlib import pyplot as plt
image = cv2.imread("path to the image i have provided")
r = 600.0 / image.shape[1]
dim = (600, int(image.shape[0] * r))
resized = cv2.resize(image, dim, interpolation = cv2.INTER_AREA)
cv2.imwrite("path to were we want to save downscaled image", resized)
image = cv2.imread('path of downscaled image')
image1 = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
image2 = cv2.GaussianBlur(image1, (5, 5), 0)
edged = cv2.Canny(image2, 30, 150)
img = cv2.medianBlur(image2,5)
cimg = cv2.cvtColor(img,cv2.COLOR_GRAY2BGR)
circles = cv2.HoughCircles(edged,cv2.HOUGH_GRADIENT,1,20,
param1=50,param2=30,minRadius=200,maxRadius=280)
circles = np.uint16(np.around(circles))
max_circle = max(circles[0,:], key=lambda x:x[2])
# print(max_circle)
# # Create mask
height,width = image1.shape
mask = np.zeros((height,width), np.uint8)
for i in [max_circle]:
cv2.circle(mask,(i[0],i[1]),i[2],(255,255,255),thickness=-1)
masked_data = cv2.bitwise_and(image, image, mask=mask)
_,thresh = cv2.threshold(mask,1,255,cv2.THRESH_BINARY)
# Find Contour
contours = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[0]
x,y,w,h = cv2.boundingRect(contours[0])
# Crop masked_data
crop = masked_data[y:y+h,x:x+w]
#Code to close Window
cv2.imshow('OG',image)
cv2.imshow('Cropped ROI',crop)
cv2.imwrite("path to save roi image", crop)
cv2.waitKey(0)
cv2.destroyAllWindows()
Second Answer: an approach based on color segmentation.
While I was editing the question to improve it's readability and was inserting and resizing all the images from the link you shared to make it easier for everyone to visualize what you are trying to do, it occurred to me that this problem might be a better candidate for an approach based on segmentation by color:
This simpler (but clever) approach assumes that the reel appears pretty much in the same location and has more or less the same dimensions every time:
To discover the approximate color of the reel in the image, define a list of Regions of Interest (ROIs) to sample pixels from and determine the min and max color of that area in the HSV color space. The location and size of the ROI are values derived from the size of the image. In the images below, you can see the ROIs as draw as blue-ish rectangles:
Once the min and max HSV colors have been found, a threshold operation with cv2.inRange() can be executed to segment the reel:
Then, iterate though all the contours in the binary image and assume that the largest one represents the reel. Use this contour and draw it in a separate mask to be able to extract the pixels from original image:
At this stage, it is also possible to compute a bounding box for the contour and extract it's precise location to be able to perform a crop operation later and completely isolate the reel in the image:
This approach works for EVERY image shared on the question.
Source code:
import cv2
import numpy as np
import sys
# initialize global H, S, V values
min_global_h = 179
min_global_s = 255
min_global_v = 255
max_global_h = 0
max_global_s = 0
max_global_v = 0
# load input image from the cmd-line
filename = sys.argv[1]
img = cv2.imread(sys.argv[1])
if (img is None):
print('!!! Failed imread')
sys.exit(-1)
# create an auxiliary image for debugging purposes
dbg_img = img.copy()
# initiailize a list of Regions of Interest that need to be scanned to identify good HSV values to threhsold by color
w = img.shape[1]
h = img.shape[0]
roi_w = int(w * 0.10)
roi_h = int(h * 0.10)
roi_list = []
roi_list.append( (int(w*0.25), int(h*0.15), roi_w, roi_h) )
roi_list.append( (int(w*0.25), int(h*0.60), roi_w, roi_h) )
# convert image to HSV color space
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# iterate through the ROIs to determine the min/max HSV color of the reel
for rect in roi_list:
x, y, w, h = rect
x2 = x + w
y2 = y + h
print('ROI rect=', rect)
cropped_hsv_img = hsv_img[y:y+h, x:x+w]
h, s, v = cv2.split(cropped_hsv_img)
min_h = np.min(h)
min_s = np.min(s)
min_v = np.min(v)
if (min_h < min_global_h):
min_global_h = min_h
if (min_s < min_global_s):
min_global_s = min_s
if (min_v < min_global_v):
min_global_v = min_v
max_h = np.max(h)
max_s = np.max(s)
max_v = np.max(v)
if (max_h > max_global_h):
max_global_h = max_h
if (max_s > max_global_s):
max_global_s = max_s
if (max_v > max_global_v):
max_global_v = max_v
# debug: draw ROI in original image
cv2.rectangle(dbg_img, (x, y), (x2, y2), (255,165,0), 4) # red
cv2.imshow('ROIs', cv2.resize(dbg_img, dsize=(0, 0), fx=0.5, fy=0.5))
#cv2.waitKey(0)
cv2.imwrite(filename[:-4] + '_rois.png', dbg_img)
# define min/max color for threshold
low_hsv = np.array([min_h, min_s, min_v])
max_hsv = np.array([max_h, max_s, max_v])
#print('low_hsv=', low_hsv)
#print('max_hsv=', max_hsv)
# threshold image by color
img_bin = cv2.inRange(hsv_img, low_hsv, max_hsv)
cv2.imshow('binary', cv2.resize(img_bin, dsize=(0, 0), fx=0.5, fy=0.5))
cv2.imwrite(filename[:-4] + '_binary.png', img_bin)
#cv2.imshow('img_bin', cv2.resize(img_bin, dsize=(0, 0), fx=0.5, fy=0.5))
#cv2.waitKey(0)
# create a mask to store the contour of the reel (hopefully)
mask = np.zeros((img_bin.shape[0], img_bin.shape[1]), np.uint8)
crop_x, crop_y, crop_w, crop_h = (0, 0, 0, 0)
# iterate throw all the contours in the binary image:
# assume that the first contour with an area larger than 100k belongs to the reel
contours, hierarchy = cv2.findContours(img_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
for contourIdx, cnt in enumerate(contours):
area = cv2.contourArea(contours[contourIdx])
print('contourIdx=', contourIdx, 'area=', area)
# draw potential reel blob on the mask (in white)
if (area > 100000):
crop_x, crop_y, crop_w, crop_h = cv2.boundingRect(cnt)
centers, radius = cv2.minEnclosingCircle(cnt)
cv2.circle(mask, (int(centers[0]), int(centers[1])), int(radius), (255), -1) # fill with white
break
cv2.imshow('mask', cv2.resize(mask, dsize=(0, 0), fx=0.5, fy=0.5))
cv2.imwrite(filename[:-4] + '_mask.png', mask)
# copy just the reel area into its own image
reel_img = cv2.bitwise_and(img, img, mask=mask)
cv2.imshow('reel_img', cv2.resize(reel_img, dsize=(0, 0), fx=0.5, fy=0.5))
cv2.imwrite(filename[:-4] + '_reel.png', reel_img)
# crop the reel to a smaller image
if (crop_w != 0 and crop_h != 0):
cropped_reel_img = reel_img[crop_y:crop_y+crop_h, crop_x:crop_x+crop_w]
cv2.imshow('cropped_reel_img', cv2.resize(cropped_reel_img, dsize=(0, 0), fx=0.5, fy=0.5))
output_filename = filename[:-4] + '_crop.png'
cv2.imwrite(output_filename, cropped_reel_img)
cv2.waitKey(0)
First answer: an approach based on pre-processing the image and executing an adaptiveThreshold operation.
There might be other ways of solving this problem that are not based on Hough Circles. Here is the result of an approach that is not:
Preprocess the image! Decreasing the size of the image and executing a blur helps with segmentation:
The segmentation method uses a cv2.adaptiveThreshold() to create a binary image that preserves the most important objects: the center of the reel and the external edge of the reel. This is an important step since we are only interested in what exists between these two objects. However, life is not perfect and neither is this segmentation. The shadow of reel on the table became part of the binary objects detected. Also, the outer edge is not fully connected as you can see on the resulting image on the right (look at the top left of the circumference):
To join broken segments, a morphological operation can be executed:
Finally, the entire reel area can be exposed by iterating through the contours of the image above and discarding those whose area is larger than what is expected for a reel. The resulting binary image (on the left) can then be used as a mask to identify the reel location on the original image:
Keep in mind that I'm not trying to find an universal solution for your problem. I'm merely showing that there might be other solutions that don't depend on Hough Circles.
Also, this code might need some adjustments to work on a larger number of cases.
Source code:
import cv2
import numpy as np
import sys
img = cv2.imread("test_images/reel.jpg")
if (img is None):
print('!!! Failed imread')
sys.exit(-1)
# create output image
output_img = img.copy()
# 1. Preprocess the image: downscale to speed up processing and execute a blur
SCALE_FACTOR = 0.5
smaller_img = cv2.resize(img, dsize=(0, 0), fx=SCALE_FACTOR, fy=SCALE_FACTOR)
blur_img = cv2.medianBlur(smaller_img, 9)
cv2.imwrite('reel1_blur_img.png', blur_img)
# 2. Segment the image to identify the 2 most important contours: the center of the reel and the outter edge
gray_img = cv2.cvtColor(blur_img, cv2.COLOR_BGR2GRAY)
img_bin = cv2.adaptiveThreshold(gray_img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 19, 4)
cv2.imwrite('reel2_img_bin.png', img_bin)
green_mask = np.zeros((img_bin.shape[0], img_bin.shape[1]), np.uint8)
#green_mask = cv2.cvtColor(img_bin, cv2.COLOR_GRAY2RGB) # debug
contours, hierarchy = cv2.findContours(img_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
for contourIdx, cnt in enumerate(contours):
x, y, w, h = cv2.boundingRect(cnt)
area = cv2.contourArea(contours[contourIdx])
#print('contourIdx=', contourIdx, 'w=', w, 'h=', h, 'area=', area)
# filter out tiny segments
if (area < 5000):
#cv2.fillPoly(green_mask, pts=[cnt], color=(0, 0, 255)) # red
continue
# draw green contour (filled)
#cv2.fillPoly(green_mask, pts=[cnt], color=(0, 255, 0)) # green
cv2.fillPoly(green_mask, pts=[cnt], color=(255)) # white
# debug:
#cv2.imshow('green_mask', green_mask)
#cv2.waitKey(0)
cv2.imshow('green_mask', green_mask)
cv2.imwrite('reel2_green_mask.png', green_mask)
# 3. Fix mask: join segments nearby
kernel = np.ones((3,3), np.uint8)
img_dilation = cv2.dilate(green_mask, kernel, iterations=1)
green_mask = cv2.erode(img_dilation, kernel, iterations=1)
cv2.imshow('fixed green_mask', green_mask)
cv2.imwrite('reel3_img.png', green_mask)
# 4. Extract the reel area from the green mask
reel_mask = np.zeros((green_mask.shape[0], green_mask.shape[1]), np.uint8)
#reel_mask = cv2.cvtColor(green_mask, cv2.COLOR_GRAY2RGB) # debug
contours, hierarchy = cv2.findContours(green_mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
for contourIdx, cnt in enumerate(contours):
x, y, w, h = cv2.boundingRect(cnt)
area = cv2.contourArea(contours[contourIdx])
print('contourIdx=', contourIdx, 'w=', w, 'h=', h, 'area=', area)
# filter out smaller segments
if (area > 110000):
#cv2.fillPoly(reel_mask, pts=[cnt], color=(0, 0, 255)) # red
continue
# draw green contour (filled)
#cv2.fillPoly(reel_mask, pts=[cnt], color=(0, 255, 0)) # green
cv2.fillPoly(reel_mask, pts=[cnt], color=(255)) # white
# debug:
#cv2.imshow('reel_mask', reel_mask)
#cv2.waitKey(0)
cv2.imshow('reel_mask', reel_mask)
cv2.imwrite('reel4_reel_mask.png', reel_mask)
# 5. Draw the reel area on the original image
contours, hierarchy = cv2.findContours(reel_mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
for contourIdx, cnt in enumerate(contours):
centers, radius = cv2.minEnclosingCircle(cnt)
# rescale these values back to the original image size
centers_orig = (centers[0] // SCALE_FACTOR, centers[1] // SCALE_FACTOR)
radius_orig = radius // SCALE_FACTOR
print('centers=', centers_orig, 'radius=', radius_orig)
cv2.circle(output_img, (int(centers_orig[0]), int(centers_orig[1])), int(radius_orig), (128,0,255), 5) # magenta
cv2.imshow('output_img', output_img)
cv2.imwrite('reel5_output.png', output_img)
# display just the pixels from the original image
larger_reel_mask = cv2.resize(reel_mask, (int(img.shape[1]), int(img.shape[0])))
output_reel_img = cv2.bitwise_and(img, img, mask=larger_reel_mask)
cv2.imshow('output_reel_img', output_reel_img)
cv2.imwrite('reel5_output_reel.png', output_reel_img)
cv2.waitKey(0)
At this point, its possible to use larger_reel_maskand compute a minimal enclosing circle, draw it over this mask to make it a little bit more round and allow us to retrieve the area of the reel more accurately:
But the 4 lines of code that achieve this improvement I leave as an exercise for the reader.
I am trying to detect all the squared shaped dice images so that i can crop them individually and use that for OCR.
Below is the Original image:
Here is the code i have got but it is missing some squares.
def find_squares(img):
img = cv2.GaussianBlur(img, (5, 5), 0)
squares = []
for gray in cv2.split(img):
for thrs in range(0, 255, 26):
if thrs == 0:
bin = cv2.Canny(gray, 0, 50, apertureSize=5)
bin = cv2.dilate(bin, None)
else:
_retval, bin = cv2.threshold(gray, thrs, 255, cv2.THRESH_BINARY)
bin, contours, _hierarchy = cv2.findContours(bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
cnt_len = cv2.arcLength(cnt, True)
cnt = cv2.approxPolyDP(cnt, 0.02*cnt_len, True)
if len(cnt) == 4 and cv2.contourArea(cnt) > 1000 and cv2.isContourConvex(cnt):
cnt = cnt.reshape(-1, 2)
max_cos = np.max([angle_cos( cnt[i], cnt[(i+1) % 4], cnt[(i+2) % 4] ) for i in range(4)])
#print(cnt)
a = (cnt[1][1] - cnt[0][1])
if max_cos < 0.1 and a < img.shape[0]*0.8:
squares.append(cnt)
return squares
dice = cv2.imread('img1.png')
squares = find_squares(dice)
cv2.drawContours(dice, squares, -1, (0, 255, 0), 3)
Here are the Output images:
As per my analysis, some squares are missing due to missing canny edges along the dice because of smooth intensity transition between dice and background.
Given the constraint that there will always be 25 dices in square grid pattern (5*5) can we predict the missing square positions based on recognised squares?
Or can we modify above algorithm for square detection algorithm?
Sharpen square edges. Load the image, convert to grayscale, median blur to smooth, and sharpen to enhance edges.
Obtain binary image and remove noise. We threshold to obtain a black/white binary image. Depending on the image, Otsu's thresholding or adaptive thresholding would work. From here we create a rectangular kernel and perform morphological transformations to remove noise and enhance the square contours.
Detect and extract squares. Next we find contours and filter using minimum/maximum threshold area. Any contours that pass our filter will be our squares so to extract each ROI, we obtain the bounding rectangle coordinates, crop using Numpy slicing, and save each square image.
Sharpen image with
cv2.filter2D() using a generic sharpening kernel, other kernels can be found here.
Now threshold to get a binary image
There's little particles of noise so to remove them, we perform morphological operations
Next find contours and filter using cv2.contourArea() with minimum/maximum threshold values.
We can crop each desired square region using Numpy slicing and save each ROI like this
x,y,w,h = cv2.boundingRect(c)
ROI = image[y:y+h, x:x+w]
cv2.imwrite('ROI_{}.png'.format(image_number), ROI)
import cv2
import numpy as np
# Load image, grayscale, median blur, sharpen image
image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.medianBlur(gray, 5)
sharpen_kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]])
sharpen = cv2.filter2D(blur, -1, sharpen_kernel)
# Threshold and morph close
thresh = cv2.threshold(sharpen, 160, 255, cv2.THRESH_BINARY_INV)[1]
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
close = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)
# Find contours and filter using threshold area
cnts = cv2.findContours(close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
min_area = 100
max_area = 1500
image_number = 0
for c in cnts:
area = cv2.contourArea(c)
if area > min_area and area < max_area:
x,y,w,h = cv2.boundingRect(c)
ROI = image[y:y+h, x:x+w]
cv2.imwrite('ROI_{}.png'.format(image_number), ROI)
cv2.rectangle(image, (x, y), (x + w, y + h), (36,255,12), 2)
image_number += 1
cv2.imshow('sharpen', sharpen)
cv2.imshow('close', close)
cv2.imshow('thresh', thresh)
cv2.imshow('image', image)
cv2.waitKey()
That extra piece of information is absolutely golden. Yes, given the 5x5 matrix of dice, you can nail the positions quite well. The dice you can identify give you the center, size, and orientation of the dice. Simply continue those patterns along both axes. For your second pass, increase the contrast in each "region of interest" where you expect to find the edge of a douse (never say die!). You know within a few pixels where the edges will be: simply attenuate the image until you identify those edges.