I am currently working on a OMR data extraction project where i have to check students OMR sheets. here is my code snippet:
import cv2
import numpy as np
# read image
path = './data/images/5.jpg'
img = cv2.imread(path)
h, w = img.shape[:2]
# resize image
img = cv2.resize(img, (w//2, h//2))
img = img[0:h-15, 0:w-5]
# threshold on white color
lower=(225,225,225)
upper=(255,255,255)
thresh = cv2.inRange(img, lower, upper)
thresh = 255 - thresh
imgCanny = cv2.Canny(thresh, 10, 50)
# # get contours
contoursImage = img.copy()
firstOMRBoxImage = img.copy()
contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
cv2.drawContours(contoursImage, contours[0], -1, (0,255,0), 2)
def rectContour(contours):
rectContours = []
for i in contours:
area = cv2.contourArea(i)
if area > 50:
peri = cv2.arcLength(i, True)
approx = cv2.approxPolyDP(i, 0.02*peri, True)
if (len(approx) == 4):
rectContours.append(i)
rectContours = sorted(rectContours, key=cv2.contourArea, reverse=True)
firstOMRBox = getCornerPoints(rectContours[0])
secondOMRBox = getCornerPoints(rectContours[1])
thirdOMRBox = getCornerPoints(rectContours[3])
fourthOMRBox = getCornerPoints(rectContours[2])
rollNoPoints = getCornerPoints(rectContours[4])
districtPoints = getCornerPoints(rectContours[5])
nameAndDatePoints = getCornerPoints(rectContours[7])
candidateSign = getCornerPoints(rectContours[8])
invigilatorSign = getCornerPoints(rectContours[9])
groupPoints = getCornerPoints(rectContours[10])
classPoints = getCornerPoints(rectContours[12])
if firstOMRBox.size != 0 and secondOMRBox.size != 0:
cv2.drawContours(firstOMRBoxImage, firstOMRBox, -1, (0,255,0), 30)
cv2.drawContours(firstOMRBoxImage, secondOMRBox, -1, (255,0,0), 30)
cv2.drawContours(firstOMRBoxImage, thirdOMRBox, -1, (0,0,255), 30)
cv2.drawContours(firstOMRBoxImage, fourthOMRBox, -1, (255,255,0), 30)
cv2.drawContours(firstOMRBoxImage, rollNoPoints, -1, (0,255,255), 30)
cv2.drawContours(firstOMRBoxImage, districtPoints, -1, (255,0,255), 30)
cv2.drawContours(firstOMRBoxImage, nameAndDatePoints, -1, (255,255,255), 30)
cv2.drawContours(firstOMRBoxImage, candidateSign, -1, (0,0,0), 30)
cv2.drawContours(firstOMRBoxImage, invigilatorSign, -1, (255,255,255), 30)
cv2.drawContours(firstOMRBoxImage, groupPoints, -1, (0,0,255), 30)
cv2.drawContours(firstOMRBoxImage, classPoints, -1, (255,0,0), 30)
firstOMRBox = reorder(firstOMRBox)
secondOMRBox = reorder(secondOMRBox)
# Get the width and height of the first OMR box
# Calculate the width and height of the first OMR box
width_omr = np.linalg.norm(firstOMRBox[0][0] - firstOMRBox[1][0])
height_omr = np.linalg.norm(firstOMRBox[0][0] - firstOMRBox[2][0])
# Use the original aspect ratio for the destination points
pt1 = np.float32(firstOMRBox)
pt2 = np.float32([[0,0],[width_omr,0],[0,height_omr],[width_omr,height_omr]])
matrix = cv2.getPerspectiveTransform(pt1, pt2)
imgWarpColoured = cv2.warpPerspective(img, matrix, (int(width_omr), int(height_omr)))
# max_side = max(w, h)
# pt1 = np.float32(firstOMRBox)
# pt2 = np.float32([[0,0],[max_side,0],[0,max_side],[max_side,max_side]])
# matrix = cv2.getPerspectiveTransform(pt1, pt2)
# imgWarpColoured = cv2.warpPerspective(img, matrix, (max_side,max_side))
cv2.imwrite('5Wrap_contour.png', imgWarpColoured)
# Apply Threshhold
# imgWarpGray = cv2.cvtColor(imgWarpColoured, cv2.COLOR_BGR2GRAY)
# imgThresh = cv2.threshold(imgWarpGray, 200, 255, cv2.THRESH_BINARY_INV)[1]
# cv2.imwrite('6biggest_thresh.png', imgThresh)
# print(imgThresh.shape)
# x1 = int(w * 0.2) # Start cropping from 70% width
# y1 = 0 # Start from the top
# x2 = w # End at full width (rightmost)
# y2 = h # Full height
# imgThresh = imgThresh[y1:y2, x1:x2]
# cv2.imwrite('7after_crop.png', imgThresh)
# Apply Threshhold
imgWarpGray = cv2.cvtColor(imgWarpColoured, cv2.COLOR_BGR2GRAY)
imgThresh = cv2.threshold(imgWarpGray, 200, 255, cv2.THRESH_BINARY_INV)[1]
cv2.imwrite('6biggest_thresh.png', imgThresh)
print(imgThresh.shape)
afterContourIMage = imgThresh.copy()
grey = cv2.cvtColor(imgWarpColoured, cv2.COLOR_BGR2GRAY)
# Find contours
contours, hierarchy = cv2.findContours(grey, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(len(contours))
cv2.drawContours(afterContourIMage, contours, -1, (0,255,0), 10)
cv2.imwrite('7after_contour.png', afterContourIMage)
# grey_inverted = cv2.bitwise_not(grey)
# cv2.imwrite('7grey_inverted.png', grey_inverted)
cv2.threshold(grey, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU, imgThresh)
cv2.imwrite('7after_thresh.png', imgThresh)
aginAfterContourIMage = imgWarpColoured.copy()
# Find contours
contours, hierarchy = cv2.findContours(imgThresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(len(contours))
cv2.drawContours(aginAfterContourIMage, contours, -1, (0,255,0), 2)
cv2.imwrite('7after_contour2.png', aginAfterContourIMage)
# Get the current dimensions of imgThresh
thresh_h, thresh_w = imgThresh.shape
# Now use the dimensions of imgThresh for cropping
x1 = int(thresh_w * 0.2) # Start cropping from 20% width
y1 = 0 # Start from the top
x2 = thresh_w # End at full width
y2 = thresh_h # Full height
# Make sure our cropping coordinates are valid
if x1 < thresh_w and y2 <= thresh_h:
imgThresh = imgThresh[y1:y2, x1:x2]
cv2.imwrite('7after_crop.png', imgThresh)
else:
print("Cropping coordinates are out of bounds!")
# Use the uncropped version instead
cv2.imwrite('7after_crop.png', imgThresh)
# biggestThreshContoursImage = imgThresh.copy()
# threshContours = cv2.findContours(biggestThreshContoursImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# cv2.drawContours(biggestThreshContoursImage, threshContours[0], -1, (0,255,0), 1)
# cv2.imwrite('biggest_thresh_contours.png', biggestThreshContoursImage)
splitBoxes(imgThresh)
return firstOMRBox
def getCornerPoints(cont):
peri = cv2.arcLength(cont, True)
approx = cv2.approxPolyDP(cont, 0.02*peri, True)
return approx
def reorder(points):
points = points.reshape((4,2))
pointsNew = np.zeros((4,1,2), np.int32)
add = points.sum(1)
# print(points)
# print(add)
pointsNew[0] = points[np.argmin(add)]
pointsNew[3] = points[np.argmax(add)]
diff = np.diff(points, axis=1)
pointsNew[1] = points[np.argmin(diff)]
pointsNew[2] = points[np.argmax(diff)]
# print(pointsNew)
return pointsNew
def splitBoxes(img):
h, w = img.shape[:2]
# Make sure height is divisible by 25
new_h = (h // 25) * 25
img = img[:new_h, :] # Crop height to nearest multiple of 25
# Make sure width is divisible by 5
new_w = (w // 5) * 5
img = img[:, :new_w] # Crop width to nearest multiple of 5
rows = np.vsplit(img, 25) # Split into 25 vertical parts
cv2.imwrite('8Split_image.png', rows[0]) # Save first row for debugging
boxes = []
for i, r in enumerate(rows):
cols = np.hsplit(r, 5) # Now width is divisible by 5
for j, box in enumerate(cols):
boxes.append(box)
# cv2.imwrite(f'Split_image_{i}_{j}.png', box) # Save each box for debugging
return boxes
# Load image
# img = cv2.imread("image.jpg") # Replace with your image path
# splitBoxes(img)
# Example usage
rectContour(contours[0])
# save results
cv2.imwrite('1omr_sheet_thresh2.png',thresh)
cv2.imwrite('2omr_sheet_canny2.png',imgCanny)
cv2.imwrite('3contours2.png',contoursImage)
cv2.imwrite('4biggest_contour2.png',firstOMRBoxImage)
cv2.waitKey(0)
cv2.destroyAllWindows()
This is where I'm currently stuck on
I have an idea of converting them into grid and somehow defining each of the block into binary. 0 -> white, 1-> black like this. but I don't know how to do that as I'm currently learning about all these stuffs.
After contour image:
I've tried to solve it by identifying contours but I didn't succeed as after identifying the contours i don't know how to extract the one's which are rounded and also have fully black color inside the bubble. if i somehow manage to do this I think I can do the rest of the part.
I am currently working on a OMR data extraction project where i have to check students OMR sheets. here is my code snippet:
import cv2
import numpy as np
# read image
path = './data/images/5.jpg'
img = cv2.imread(path)
h, w = img.shape[:2]
# resize image
img = cv2.resize(img, (w//2, h//2))
img = img[0:h-15, 0:w-5]
# threshold on white color
lower=(225,225,225)
upper=(255,255,255)
thresh = cv2.inRange(img, lower, upper)
thresh = 255 - thresh
imgCanny = cv2.Canny(thresh, 10, 50)
# # get contours
contoursImage = img.copy()
firstOMRBoxImage = img.copy()
contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
cv2.drawContours(contoursImage, contours[0], -1, (0,255,0), 2)
def rectContour(contours):
rectContours = []
for i in contours:
area = cv2.contourArea(i)
if area > 50:
peri = cv2.arcLength(i, True)
approx = cv2.approxPolyDP(i, 0.02*peri, True)
if (len(approx) == 4):
rectContours.append(i)
rectContours = sorted(rectContours, key=cv2.contourArea, reverse=True)
firstOMRBox = getCornerPoints(rectContours[0])
secondOMRBox = getCornerPoints(rectContours[1])
thirdOMRBox = getCornerPoints(rectContours[3])
fourthOMRBox = getCornerPoints(rectContours[2])
rollNoPoints = getCornerPoints(rectContours[4])
districtPoints = getCornerPoints(rectContours[5])
nameAndDatePoints = getCornerPoints(rectContours[7])
candidateSign = getCornerPoints(rectContours[8])
invigilatorSign = getCornerPoints(rectContours[9])
groupPoints = getCornerPoints(rectContours[10])
classPoints = getCornerPoints(rectContours[12])
if firstOMRBox.size != 0 and secondOMRBox.size != 0:
cv2.drawContours(firstOMRBoxImage, firstOMRBox, -1, (0,255,0), 30)
cv2.drawContours(firstOMRBoxImage, secondOMRBox, -1, (255,0,0), 30)
cv2.drawContours(firstOMRBoxImage, thirdOMRBox, -1, (0,0,255), 30)
cv2.drawContours(firstOMRBoxImage, fourthOMRBox, -1, (255,255,0), 30)
cv2.drawContours(firstOMRBoxImage, rollNoPoints, -1, (0,255,255), 30)
cv2.drawContours(firstOMRBoxImage, districtPoints, -1, (255,0,255), 30)
cv2.drawContours(firstOMRBoxImage, nameAndDatePoints, -1, (255,255,255), 30)
cv2.drawContours(firstOMRBoxImage, candidateSign, -1, (0,0,0), 30)
cv2.drawContours(firstOMRBoxImage, invigilatorSign, -1, (255,255,255), 30)
cv2.drawContours(firstOMRBoxImage, groupPoints, -1, (0,0,255), 30)
cv2.drawContours(firstOMRBoxImage, classPoints, -1, (255,0,0), 30)
firstOMRBox = reorder(firstOMRBox)
secondOMRBox = reorder(secondOMRBox)
# Get the width and height of the first OMR box
# Calculate the width and height of the first OMR box
width_omr = np.linalg.norm(firstOMRBox[0][0] - firstOMRBox[1][0])
height_omr = np.linalg.norm(firstOMRBox[0][0] - firstOMRBox[2][0])
# Use the original aspect ratio for the destination points
pt1 = np.float32(firstOMRBox)
pt2 = np.float32([[0,0],[width_omr,0],[0,height_omr],[width_omr,height_omr]])
matrix = cv2.getPerspectiveTransform(pt1, pt2)
imgWarpColoured = cv2.warpPerspective(img, matrix, (int(width_omr), int(height_omr)))
# max_side = max(w, h)
# pt1 = np.float32(firstOMRBox)
# pt2 = np.float32([[0,0],[max_side,0],[0,max_side],[max_side,max_side]])
# matrix = cv2.getPerspectiveTransform(pt1, pt2)
# imgWarpColoured = cv2.warpPerspective(img, matrix, (max_side,max_side))
cv2.imwrite('5Wrap_contour.png', imgWarpColoured)
# Apply Threshhold
# imgWarpGray = cv2.cvtColor(imgWarpColoured, cv2.COLOR_BGR2GRAY)
# imgThresh = cv2.threshold(imgWarpGray, 200, 255, cv2.THRESH_BINARY_INV)[1]
# cv2.imwrite('6biggest_thresh.png', imgThresh)
# print(imgThresh.shape)
# x1 = int(w * 0.2) # Start cropping from 70% width
# y1 = 0 # Start from the top
# x2 = w # End at full width (rightmost)
# y2 = h # Full height
# imgThresh = imgThresh[y1:y2, x1:x2]
# cv2.imwrite('7after_crop.png', imgThresh)
# Apply Threshhold
imgWarpGray = cv2.cvtColor(imgWarpColoured, cv2.COLOR_BGR2GRAY)
imgThresh = cv2.threshold(imgWarpGray, 200, 255, cv2.THRESH_BINARY_INV)[1]
cv2.imwrite('6biggest_thresh.png', imgThresh)
print(imgThresh.shape)
afterContourIMage = imgThresh.copy()
grey = cv2.cvtColor(imgWarpColoured, cv2.COLOR_BGR2GRAY)
# Find contours
contours, hierarchy = cv2.findContours(grey, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(len(contours))
cv2.drawContours(afterContourIMage, contours, -1, (0,255,0), 10)
cv2.imwrite('7after_contour.png', afterContourIMage)
# grey_inverted = cv2.bitwise_not(grey)
# cv2.imwrite('7grey_inverted.png', grey_inverted)
cv2.threshold(grey, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU, imgThresh)
cv2.imwrite('7after_thresh.png', imgThresh)
aginAfterContourIMage = imgWarpColoured.copy()
# Find contours
contours, hierarchy = cv2.findContours(imgThresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(len(contours))
cv2.drawContours(aginAfterContourIMage, contours, -1, (0,255,0), 2)
cv2.imwrite('7after_contour2.png', aginAfterContourIMage)
# Get the current dimensions of imgThresh
thresh_h, thresh_w = imgThresh.shape
# Now use the dimensions of imgThresh for cropping
x1 = int(thresh_w * 0.2) # Start cropping from 20% width
y1 = 0 # Start from the top
x2 = thresh_w # End at full width
y2 = thresh_h # Full height
# Make sure our cropping coordinates are valid
if x1 < thresh_w and y2 <= thresh_h:
imgThresh = imgThresh[y1:y2, x1:x2]
cv2.imwrite('7after_crop.png', imgThresh)
else:
print("Cropping coordinates are out of bounds!")
# Use the uncropped version instead
cv2.imwrite('7after_crop.png', imgThresh)
# biggestThreshContoursImage = imgThresh.copy()
# threshContours = cv2.findContours(biggestThreshContoursImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# cv2.drawContours(biggestThreshContoursImage, threshContours[0], -1, (0,255,0), 1)
# cv2.imwrite('biggest_thresh_contours.png', biggestThreshContoursImage)
splitBoxes(imgThresh)
return firstOMRBox
def getCornerPoints(cont):
peri = cv2.arcLength(cont, True)
approx = cv2.approxPolyDP(cont, 0.02*peri, True)
return approx
def reorder(points):
points = points.reshape((4,2))
pointsNew = np.zeros((4,1,2), np.int32)
add = points.sum(1)
# print(points)
# print(add)
pointsNew[0] = points[np.argmin(add)]
pointsNew[3] = points[np.argmax(add)]
diff = np.diff(points, axis=1)
pointsNew[1] = points[np.argmin(diff)]
pointsNew[2] = points[np.argmax(diff)]
# print(pointsNew)
return pointsNew
def splitBoxes(img):
h, w = img.shape[:2]
# Make sure height is divisible by 25
new_h = (h // 25) * 25
img = img[:new_h, :] # Crop height to nearest multiple of 25
# Make sure width is divisible by 5
new_w = (w // 5) * 5
img = img[:, :new_w] # Crop width to nearest multiple of 5
rows = np.vsplit(img, 25) # Split into 25 vertical parts
cv2.imwrite('8Split_image.png', rows[0]) # Save first row for debugging
boxes = []
for i, r in enumerate(rows):
cols = np.hsplit(r, 5) # Now width is divisible by 5
for j, box in enumerate(cols):
boxes.append(box)
# cv2.imwrite(f'Split_image_{i}_{j}.png', box) # Save each box for debugging
return boxes
# Load image
# img = cv2.imread("image.jpg") # Replace with your image path
# splitBoxes(img)
# Example usage
rectContour(contours[0])
# save results
cv2.imwrite('1omr_sheet_thresh2.png',thresh)
cv2.imwrite('2omr_sheet_canny2.png',imgCanny)
cv2.imwrite('3contours2.png',contoursImage)
cv2.imwrite('4biggest_contour2.png',firstOMRBoxImage)
cv2.waitKey(0)
cv2.destroyAllWindows()
This is where I'm currently stuck on
I have an idea of converting them into grid and somehow defining each of the block into binary. 0 -> white, 1-> black like this. but I don't know how to do that as I'm currently learning about all these stuffs.
After contour image:
I've tried to solve it by identifying contours but I didn't succeed as after identifying the contours i don't know how to extract the one's which are rounded and also have fully black color inside the bubble. if i somehow manage to do this I think I can do the rest of the part.
Share Improve this question edited Mar 14 at 14:38 Christoph Rackwitz 15.9k5 gold badges39 silver badges51 bronze badges asked Mar 13 at 22:54 Dipan NamaDipan Nama 11 silver badge2 bronze badges 6- We cannot debug your code without some images? Put imshow commands in your code the make sure your image at each step is as you would expect it. Stop when and image is not looking correct and figure out what went wrong. You may be making assumptions that incorrect. – fmw42 Commented Mar 13 at 23:12
- Welcome to Stack Overflow. Please take the tour (stackoverflow/tour) and read the information guides in the help center (stackoverflow/help), – fmw42 Commented Mar 13 at 23:13
- 1 See stackoverflow/questions/72418797/… and stackoverflow/questions/72465878/…, stackoverflow/questions/77619667/… and stackoverflow/questions/77032956/… – fmw42 Commented Mar 13 at 23:21
- Learn to search this forum and Google to look for similar examples. – fmw42 Commented Mar 13 at 23:22
- the answer below "does something", but it does nothing useful in service of reading the answers on that form. – Christoph Rackwitz Commented Mar 14 at 14:37
1 Answer
Reset to default 0To get the mark locaitons:
- First preprocess the image for contour detection
- Get the contours and their center locaitons as (x,y)
- Using the coordinates decide the selection as A,B,C or D
I can explain how tho filter the image properly and how to get the contours. You first need to apply opening operation which enables you to get rid of small noises while preserving the image.
This is your original image:
This is the opened image:
Finally this is the detected contours(green) and their centers(blue):
This is the complete code, using theese coordinates i believe you can decide the selected option for each marker.
import cv2
import numpy
#Read the image and convert it to black-white the binary image
test_image = cv2.imread('test_result.png')
h,w,c = test_image.shape
gray_image = cv2.cvtColor(test_image,cv2.COLOR_BGR2GRAY)
lvl, thresholded_image = cv2.threshold(gray_image,150,255,cv2.THRESH_BINARY)
#Make opening on the image to remove the samaller white blobs
opened = cv2.morphologyEx(thresholded_image,cv2.MORPH_OPEN,cv2.getStructuringElement(cv2.MORPH_RECT,(11,11)))
#Make the contour detection on the image and get their coordinates
contours,hierarchy = cv2.findContours(opened,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
cv2.drawContours(test_image,[cnt],-1,(0,255,0),2)
# compute the center of the contour
M = cv2.moments(cnt)
cx = int(M["m10"] / M["m00"])
cy = int(M["m01"] / M["m00"])
cv2.circle(test_image,(cx,cy),7,(255,0,0),-1)
#Show the results
cv2.imshow('Thresh',cv2.resize(thresholded_image,(int(w*720/h),720)))
cv2.imshow('Original',cv2.resize(test_image,(int(w*720/h),720)))
cv2.imshow('Opened',cv2.resize(opened,(int(w*720/h),720)))
cv2.waitKey()`