Skip to content

Commit 00e071f

Browse files
authored
Merge pull request #2 from AeroAndZero/AdvanceOmrScanner
AOS v1
2 parents 0543d0b + 3f2958f commit 00e071f

40 files changed

Lines changed: 1487 additions & 4 deletions

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
1-
# Simple OMR Scanner
2-
An Simple OMR Scanner Made Using OpenCV and Python.
3-
4-
[Click Here](https://youtu.be/rMkkUP3v9h0) For A Youtube Video On Explaining How This Works. (Its an Unlisted Video.. You Can't See It Without Clicking The Link.)
1+
# Development Branch
2+
### This branch is only for in-progress code and not for download ! Switch to Default/master branch for stable application code.

aos.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import cv2
2+
import numpy as np
3+
import circleProcessor
4+
import random
5+
red = [10,10,255]
6+
blue = [255,10,10]
7+
green = [10,255,10]
8+
yellow = [0,255,255]
9+
purple = [255,10,255]
10+
answerKeyTextFile = 0
11+
12+
def mapp(h):
13+
h = h.reshape((4,2))
14+
hnew = np.zeros((4,2),dtype = np.float32)
15+
16+
add = h.sum(1)
17+
hnew[0] = h[np.argmin(add)]
18+
hnew[2] = h[np.argmax(add)]
19+
20+
diff = np.diff(h,axis = 1)
21+
hnew[1] = h[np.argmin(diff)]
22+
hnew[3] = h[np.argmax(diff)]
23+
24+
return hnew
25+
26+
def findCorners(omr0,cp1=70,cp2=20,bp=17):
27+
h = omr0.shape[0]
28+
w = omr0.shape[1]
29+
xdownscale = float(500)/float(w)
30+
ydownscale = float(500)/float(h)
31+
32+
copy = np.copy(omr0)
33+
omr0 = cv2.resize(omr0,(int(w*xdownscale),int(h*ydownscale)))
34+
35+
omr0_blur = cv2.GaussianBlur(omr0,(bp,bp),0)
36+
37+
omr0_canny = cv2.Canny(omr0_blur,cp1,cp2)
38+
39+
contours, hierarchy = cv2.findContours(omr0_canny,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)
40+
41+
cv2.drawContours(omr0, contours, -1, (0,255,0), 3)
42+
maxArea = 0
43+
maxContour = contours[0]
44+
perimulti = 0.01
45+
for i in range(0,len(contours)):
46+
area = cv2.contourArea(contours[i])
47+
if area == (omr0.shape[0]-1)*(omr0.shape[1]-1):
48+
continue
49+
50+
temp_peri = cv2.arcLength(contours[i],True)
51+
temp_approx = cv2.approxPolyDP(contours[i],perimulti*temp_peri,True)
52+
53+
if area > maxArea and len(temp_approx) == 4:
54+
maxArea = area
55+
maxContour = contours[i]
56+
maxContourIndex = i
57+
58+
peri = cv2.arcLength(maxContour,True)
59+
approx = cv2.approxPolyDP(maxContour,perimulti*peri,True)
60+
61+
corners = mapp(approx)
62+
for corner in corners:
63+
corner[0] = int(w*corner[0]/500)
64+
corner[1] = int(h*corner[1]/500)
65+
#cv2.circle(copy,(corner[0],corner[1]),2,(0,0,255),7)
66+
67+
68+
wscale = abs(max(corners[0][0] - corners[1][0],corners[2][0] - corners[3][0]))
69+
hscale = abs(max(corners[0][1] - corners[2][1],corners[1][1] - corners[3][1]))
70+
71+
dst = np.array([
72+
[0, 0],
73+
[wscale - 1, 0],
74+
[wscale - 1, hscale - 1],
75+
[0, hscale - 1]], dtype = "float32")
76+
77+
M = cv2.getPerspectiveTransform(corners,dst)
78+
wrapped = cv2.warpPerspective(copy, M, (wscale, hscale))
79+
return wrapped
80+
81+
def scanOmr(image,actualSize = [],init = [],diff = [],resize = [],totalMCQs = 10,totalOptions = 4,showDots = False, method=0,InkThreshold = 120):
82+
#Debug
83+
print(InkThreshold+10)
84+
#some parameters
85+
safeToScan = False
86+
actualw = actualSize[0]
87+
actualh = actualSize[1]
88+
fx = init[0]
89+
fy = init[1]
90+
dx = diff[0]
91+
dy = diff[1]
92+
currentOption = 1
93+
94+
# 0: total scanned, 1: correct MCQs, 2 : wrong mcqs
95+
optionTicked = []
96+
97+
if (method == 0): #----------------- Simple Scanning Algorithm
98+
resizew = resize[0]
99+
resizeh = resize[1]
100+
101+
dx = int((dx*resizew)/actualw)
102+
dy = int((dy*resizeh)/actualh)
103+
fx = int((fx*resizew)/actualw)
104+
fy = int((fy*resizeh)/actualh)
105+
106+
image= cv2.resize(image,(resizew,resizeh))
107+
108+
for y in range(fy,fy+(dy*totalMCQs),dy):
109+
110+
optionTicked.append(0)
111+
currentOption = 1
112+
113+
for x in range(fx,fx+(dx*totalOptions),dx):
114+
115+
if (image[y,x][0] < InkThreshold) and (image[y,x][1] < InkThreshold) and (image[y,x][2] < InkThreshold):
116+
print("Low inkThreshold found")
117+
118+
#Question Number : int(y/dy)
119+
#Option Number : int(x/dx)+1
120+
121+
cv2.circle(image,(x,y),int(dx*0.1),yellow,int(dx*0.15))
122+
123+
if optionTicked[len(optionTicked)-1] == 0:
124+
optionTicked[len(optionTicked)-1] += currentOption
125+
else :
126+
optionTicked[len(optionTicked)-1] += 69
127+
128+
else:
129+
cv2.circle(image,(x,y),int(dx*0.1),purple,int(dx*0.15))
130+
131+
currentOption += 1
132+
133+
image = cv2.resize(image,(actualSize[0],actualSize[1]))
134+
return image, optionTicked
135+
136+
elif (method == 1): #----------------- Circle Detection and Dynamic Difference Algorithm
137+
image = cv2.resize(image,(actualSize[0],actualSize[1]))
138+
fx,fy = circleProcessor.findClosestCircle(image,fx,fy,int(dx/2))
139+
PointHistoryX = [fx]
140+
PointHistoryY = [fy]
141+
142+
try:
143+
for mcq in range(totalMCQs):
144+
y = circleProcessor.findClosestCircle(image,PointHistoryX[0],PointHistoryY[0]+dy,int(dy/2))[1]
145+
146+
PointHistoryX = [fx]
147+
148+
optionTicked.append(0)
149+
150+
for option in range(totalOptions):
151+
x = circleProcessor.findClosestCircle(image,PointHistoryX[0]+dx,PointHistoryY[0],int(dx/2))[0]
152+
#Scanning Here
153+
xForScan = PointHistoryX[0]
154+
yForScan = PointHistoryY[0]
155+
156+
#----------- Scanning
157+
if (image[yForScan,xForScan][0] < InkThreshold) and (image[yForScan,xForScan][1] < InkThreshold) and (image[yForScan,xForScan][2] < InkThreshold):
158+
print("Low inkThreshold found")
159+
160+
#Question Number : int(y/dy)
161+
#Option Number : int(x/dx)+1
162+
163+
cv2.circle(image,(xForScan,yForScan),1,yellow,2)
164+
165+
if optionTicked[len(optionTicked)-1] == 0:
166+
optionTicked[len(optionTicked)-1] += option+1
167+
else :
168+
optionTicked[len(optionTicked)-1] += 69
169+
170+
else:
171+
cv2.circle(image,(xForScan,yForScan),1,purple,2)
172+
173+
#Keeping Data Short
174+
PointHistoryX.insert(0,x)
175+
dx = abs(PointHistoryX[0] - PointHistoryX[1]) #Dynamic Difference
176+
PointHistoryX.pop()
177+
178+
PointHistoryY.insert(0,y)
179+
dy = abs(PointHistoryY[0] - PointHistoryY[1])
180+
PointHistoryY.pop()
181+
182+
except Exception as e:
183+
print(e)
184+
185+
return image, optionTicked
186+
187+
else:
188+
print("Invalid Method Provided.")
189+
return 0
190+
191+
192+
def main():
193+
omr = cv2.imread("images/img_3.jpg")
194+
found_omr = findCorners(omr)
195+
answers = scanOmr(found_omr,[278,503],[27,24],[32,24],[278,503],20,4,True)
196+
cv2.imshow("AOSv2 1",omr)
197+
cv2.waitKey(0)
198+
cv2.destroyAllWindows()
199+
200+
if __name__ == "__main__":
201+
main()

circleProcessor.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'''
2+
best circle detection parameters found yet :
3+
cv2.HoughCircles(smallImage_blur_gray, cv2.HOUGH_GRADIENT, 1.5, hcDistant,param1=100,param2=20)
4+
'''
5+
import cv2
6+
import numpy as np
7+
import math
8+
show = 0
9+
10+
def cropImage(image,refX,refY):
11+
limitX = int((image.shape[1] * 0.05))
12+
limitY = int((image.shape[0] * 0.05))
13+
rightX = 0
14+
leftX = 0
15+
upY = 0
16+
downY = 0
17+
18+
while (rightX + refX < image.shape[1]) and (rightX < limitX):
19+
rightX += 1
20+
21+
while (refX - leftX > 0) and (leftX < limitX):
22+
leftX += 1
23+
24+
while (refY + downY < image.shape[0]) and (downY < limitY):
25+
downY += 1
26+
27+
while (refY - upY > 0) and (upY < limitY):
28+
upY += 1
29+
30+
croppedImage = image[(refY-upY):(refY+downY),(refX-leftX):(refX+rightX)]
31+
return croppedImage
32+
33+
def distants(x1,x2,y1,y2):
34+
dis = math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1))
35+
return dis
36+
37+
def findClosestCircle(image,refX,refY,hcDistant):
38+
minInit = 0
39+
smallImage = cropImage(image,refX,refY)
40+
#smallImage_blur = cv2.GaussianBlur(smallImage,(1,1),1)
41+
smallImage_blur_gray = cv2.cvtColor(smallImage,cv2.COLOR_BGR2GRAY)
42+
43+
newX = int(smallImage.shape[1]/2)
44+
newY = int(smallImage.shape[0]/2)
45+
diffX = refX - newX
46+
diffY = refY - newY
47+
48+
circles = cv2.HoughCircles(smallImage_blur_gray, cv2.HOUGH_GRADIENT, 1.5, hcDistant,param1=100,param2=20)
49+
50+
if circles is not None:
51+
circles = np.round(circles[0, :]).astype("int")
52+
53+
minDistantXY = [circles[0][0],circles[0][1]]
54+
for (x, y, r) in circles:
55+
if show: #Debugging
56+
cv2.circle(smallImage,(x,y),1,(0,0,255),2)
57+
58+
if distants(x,newX,y,newY) <= distants(minDistantXY[0],newX,minDistantXY[1],newY):
59+
minDistantXY = [x,y]
60+
61+
newX = minDistantXY[0]
62+
newY = minDistantXY[1]
63+
64+
finalX = newX + diffX
65+
finalY = newY + diffY
66+
67+
'''For Debugging '''
68+
if show:
69+
cv2.circle(smallImage,(newX,newY),2,(0,255,0),3)
70+
cv2.imshow("Lo",smallImage)
71+
cv2.waitKey(0)
72+
73+
return [finalX,finalY]
74+
75+
if __name__ == "__main__":
76+
image = cv2.imread("images/croppedOMR7-ticked.jpg")
77+
show = 1
78+
findClosestCircle(image,401,714,1)

images/OMRSheet1.png

11.6 KB
Loading

images/OMRSheet1Filled.png

19.4 KB
Loading

images/OMRSheet2.png

16.7 KB
Loading

images/OMRSheet2Answered.png

17.5 KB
Loading

images/RealLifeOMR1.jpg

751 KB
Loading

images/RealLifeOMR2.jpg

4.83 MB
Loading

images/croppedOMR.jpg

316 KB
Loading

0 commit comments

Comments
 (0)