-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathscanner.py
236 lines (205 loc) · 9.16 KB
/
scanner.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import cv2,os, glob
from imutils.perspective import four_point_transform
from imutils import contours
import numpy as np
from answerkey import *
from settings import *
sections = {1: "Reading", 2: "Writing", 3: "Math-No-Calc", 4: "Math-Calc"}
folder_dir = os.getcwd()
files_list = glob.glob(folder_dir + "/imgs/*")
img_list = []
for file in files_list:
if (file.lower().endswith(".jpg") | file.lower().endswith(".png") | file.lower().endswith(".jpeg")):
img_list.append(file)
imgpath = max(img_list, key=os.path.getctime) #most recently modified image, assuming this is the onen you used
def runAllTests():
files_list.sort(key=os.path.getctime, reverse=True)
for file in files_list:
if (file.lower().endswith(".jpg") | file.lower().endswith(".png") | file.lower().endswith(".jpeg")):
filepath = os.path.join(folder_dir, file)
checkAns(filepath)
def findRectContour(img, cntrs):
areas = []
ma = 0
cntr = None
for c in cntrs:
area = cv2.contourArea(c)
areas.append(area)
(x, y, w, h) = cv2.boundingRect(c)
cntrImg = cv2.drawContours(img.copy(), c, -1, (255, 0, 0), 1)
cv2.rectangle(cntrImg, (x, y), (x+w, y+h), (0, 0, 255), 2)
if (area >= ma):
ma = area
cntr = c
return cntr
def removeRectContour(img, pw, ph):
form_area = pw * ph
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 61, 10)
cntrs, hiearchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
largest_areas = sorted(cntrs, key=cv2.contourArea, reverse=True)
for c in largest_areas:
area = cv2.contourArea(c)
print(area)
if (area >= form_area/2):
mask = np.ones(thresh.shape[:2], dtype="uint8") * 255
cv2.drawContours(mask, c, -1, (0,0,0,0), 30)
cv2.drawContours(thresh, c, -1, (0,0,0), 30)
thresh = cv2.bitwise_and(thresh, thresh, mask=mask)
else:
break
cv2.rectangle(thresh, (0,0), (pw,ph), (0,0,0), 40) #safety measure in case curved border
return thresh
def checkAns(imgpath):
global section
img = cv2.imread(imgpath)
gray = cv2.cvtColor(img.copy(), cv2.COLOR_BGR2GRAY) #converting image to grayscale format
blur = cv2.GaussianBlur(gray, (5, 5), 0)
thresh = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 31, 10)
cntrs, hiearchy = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) #simple because we only need a rectangle
cntrsImg = cv2.drawContours(img.copy(), cntrs, -1, (255, 0, 0), 1)
cntr = findRectContour(img, cntrs)
peri = cv2.arcLength(cntr, True)
approx = cv2.approxPolyDP(cntr, 0.02 * peri, True) #approximate rectangular contour
cntrImg = cv2.drawContours(img.copy(), cntr, -1, (255, 0, 0), 3)
shape = cv2.drawContours(img.copy(), approx, -1, (255, 0, 0), 5)
cv2.imshow("Rectangular Region", cntrImg)
cntrImg = cv2.drawContours(img.copy(), cntr, -1, (255, 0, 0), 3)
#an easy method from imutils lib that converts image to birds-eye view
corners = approx.reshape(4, 2)
paper = four_point_transform(img, corners)
papergray = cv2.cvtColor(paper.copy(), cv2.COLOR_BGR2GRAY)
bounds = paper.shape
pw = bounds[1]
ph = bounds[0]
form_area = pw * ph
#binary image conversion
thresh = cv2.adaptiveThreshold(papergray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 61, 10)
thresh = removeRectContour(paper, pw, ph)
#open cv find contours aims to find white objects, not black
cntrs, hiearchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) #sometimes external works better
papercntrs = cv2.drawContours(paper.copy(), cntrs, -1, (0, 255, 0), 1)
bubbles = []
widths = []
heights = []
bubble_areas = []
rect_areas = []
filtered_rect = paper.copy()
filtered_cntrs = []
maxw = 0
maxh = 0
if (section == 1): #the bubble size cannot exceed these safety bounds
maxw = pw/20
maxh = ph/13
elif (section == 2):
maxw = pw/20
maxh = ph/9
for c in cntrs:
(x, y, w, h) = cv2.boundingRect(c)
ar = h / float (w) #either vertical rectangle or quasi square
if w <=maxw and h<=maxh and w >= pw/100 and h>= pw/100 : #no straight lines with no shape allowed or big blobs no wide rectangles (which are normally 2-digit question numbers )
widths.append(w)
heights.append(h)
cv2.rectangle(filtered_rect, (x, y), (x+w, y+h), (0, 0, 255), 2)
filtered_cntrs.append(c)
bubble_areas.append(cv2.contourArea(c))
rect_areas.append(w*h)
cv2.imshow("filtered", filtered_rect)
widths.sort(reverse=True)
heights.sort(reverse=True)
bubble_areas.sort(reverse=True)
rect_areas.sort(reverse=True)
detected_section = None
if (len(filtered_cntrs) <= (52*4*2 + 52)):
detected_section = 2
else:
detected_section = 1
if (detected_section == section):
print("Expected Seciton")
else:
print("Did Not Expect Section" + str(section))
section = detected_section
totalq = 0
if (section == 1):
totalq = 52
elif (section == 2):
totalq = 44
widthbound = widths[totalq*4-1] #from testing normally 15-25 pixels
heightbound = heights[totalq*4-1] #from testing normally 15-25 pixels + there are exactly 208 bubbles on the reading portion
bubbleareabound = bubble_areas[totalq*4-1]
rectareabound = rect_areas[totalq*4-1]
print(widthbound)
print(heightbound)
print(rectareabound)
print(bubbleareabound)
print(heights)
print(len(heights))
bubbles_cntr = paper.copy()
bubbles_rect = paper.copy()
answers = allTests[test + sections[section]]
for c in filtered_cntrs:
(x, y, w, h) = cv2.boundingRect(c)
area = cv2.contourArea(c)
if area >= bubbleareabound : #filter out circles by size // w>= widthbound and h >= heightbound and w <=pw/20 and h<=ph/13 and ar >= 0.7 and #w>= widthbound and h >= heightbound and
bubbles.append(c)
cv2.rectangle(bubbles_cntr, (x, y), (x+w, y+h), (0, 0, 255), 2)
cv2.imshow("bubbles",bubbles_cntr)
for c in filtered_cntrs:
(x, y, w, h) = cv2.boundingRect(c)
betterarea = w*h #imo more reliable than contourArea()
if w>= widthbound and h >= heightbound and betterarea >= rectareabound : #filter out circles by size // w>= widthbound and h >= heightbound and w <=pw/20 and h<=ph/13 and ar >= 0.7 and
#bubbles.append(c)
cv2.rectangle(bubbles_rect, (x, y), (x+w, y+h), (0, 0, 255), 2)
bubbles = contours.sort_contours(bubbles, method="left-to-right")[0]
qpercol = 0
if (section == 1):
qpercol = 13
elif (section == 2):
qpercol = 9
print(qpercol)
correct = 0
wrong = ""
qnum = 0
sum = 0.0
print(len(bubbles))
debugstring = "" #each qnum and the 4 bubble pixel value ratios printed out at end
for (row, r) in enumerate(np.arange(0, len(bubbles), qpercol*4)): #iterative every column or every 13 questions
#sorting the first 13 questions (4 choices each)
col = contours.sort_contours(bubbles[r:r + qpercol*4], "top-to-bottom")[0]
for (q, i) in enumerate(np.arange(0, len(col), 4)): #then find the first four bubbles ( for one qustion)
cnts = contours.sort_contours(col[i:i + 4],"left-to-right")[0] #left to right sort
qnum += 1
bubbled = None
values = []
for (j, c) in enumerate(cnts):
(x, y, w, h) = cv2.boundingRect(c)
mask = np.zeros(thresh.shape, dtype="uint8") #mask a question turning black and white
cv2.drawContours(mask, [c], -1, 255, -1)
mask = cv2.bitwise_and(thresh, thresh, mask=mask)
total = cv2.countNonZero(mask) #count largest amount of white pixels
sum += total
ratio = total/float(w*h)
values.append(ratio)
if bubbled is None or ratio > bubbled[0]:
bubbled = (ratio, j)
ans = ord(answers[qnum-1])-ord('A')
values.sort()
avg = sum / (float(4*qnum))
# check to see if the bubbled answer is correct
color = (0, 0, 255)
if ans == bubbled[1]:
color = (0, 255, 0)
correct += 1
else:
wrong += (str(qnum) + " ")
if (ShowAnswer == True): # draw the outline of the correct answer on the test
cv2.drawContours(paper, [cnts[ans]], -1, color, 2)
else: #alternatively simply mark whether the user's answer is right or wrong
cv2.drawContours(paper, [cnts[bubbled[1]]], -1, color, 2)
fontScale = 0.8
cv2.putText(paper, f"{correct}/{totalq} | Wrong answers: {wrong}", (10, 30),cv2.FONT_HERSHEY_SIMPLEX, fontScale, (0, 0, 255), 2)
cv2.imshow("Final Corrected Paper",paper)
cv2.waitKey(0)
cv2.destroyAllWindows()
#runAllTests()
checkAns(imgpath)