一、目标
扫描如图所示的答题卡,找到其中选的答案,与正确答案对比,给出分数。
二、分步实现
1、读取数据,进行透视变换
- 数据预处理:高斯滤波、二值化、边缘检测、识别轮廓
#对输入图像进行预处理
image = cv2.imread(args["image"])
contours_img = image.copy()
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray,(5,5),0)
cv_show('blurred',blurred)
edges = cv2.Canny(blurred,75,200)#边缘检测
cv_show("edges",edges)
#轮廓检测
cnts = cv2.findContours(edges.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[1]
cv2.drawContours(contours_img,cnts,-1,(0,0,255),3)
cv_show("contours_img",contours_img)
docCnt = None
高斯滤波结果
二值化后进行边缘检测
轮廓提取
- 透视变换
通常情况下,我们要在检测出的轮廓中寻找我们想要的部分(这里就是答题卡部分)。检测的方法就是返回周长或者是面积最大的轮廓,然后将轮廓求近似矩形,将矩形的四个点的坐标传入函数中求变换矩阵,进行透视变换。
#对检测到的轮廓进行筛选,为透视变化做准备
if len(cnts) > 0:
cnts = sorted(cnts,key=cv2.contourArea,reverse=True)
for c in cnts:
peri = cv2.arcLength(c,True)
approx = cv2.approxPolyDP(c,0.02*peri,True) #近似轮廓
#透视变换
if len(approx) == 4:
docCnt = approx
break #找到了做透视变换的四个坐标了
#执行透视变换
warped = four_point_transform(gray,docCnt.reshape(4,2))
cv_show("warped",warped)
透视变换函数
def four_point_transform(image,pts):
#获取坐标点
rect = order_points(pts)
(tl,tr,br,bl) = rect
widthA = np.sqrt(((tl[0] - tr[0]) ** 2) + ((tl[1] - tr[1]) ** 2))
widthB = np.sqrt(((bl[0] - br[0]) ** 2) + ((bl[1] - br[1]) ** 2))
maxwidth = max(int(widthB),int(widthA))
heightA = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
heightB = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
maxheight = max(int(heightB),int(heightA))
#确定变换后坐标的位置
dst = np.array([
[0, 0],
[maxwidth - 1, 0],
[maxwidth - 1, maxheight - 1],
[0, maxheight - 1]], dtype="float32")
#计算变换矩阵
M = cv2.getPerspectiveTransform(rect,dst)
warped = cv2.warpPerspective(image,M,(maxwidth,maxheight))
return warped
获取坐标点的函数
def order_points(pts):
#根据位置信息定位四个坐标点的位置
rect = np.zeros((4,2),dtype="float32")
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts,axis=1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect
透视变换结果
- 我们要在透视变换的结果中找到每个选项的位置,从中找到每个题选择的答案。
给透视变化后的图形做自适应阈值的二值化,然后筛选出25个选项的轮廓
方法:检测出轮廓后画出近似矩形,通过判断近似矩形的大小,筛选出选项的轮廓;对选出的轮廓进行从上到下的排序,这样每一道题的五个选项的轮廓就排在一起了。
cnts = cv2.findContours(thresh.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[1]
cv2.drawContours(thresh_counters,cnts,-1,(0,0,255),3)
cv_show('thresh_counters',thresh_counters)
questionCnts = []
for c in cnts:
(x,y,w,h) = cv2.boundingRect(c)
ar = w/float(h)
#设定标准
if w>=20 and h>=20 and ar>=0.9 and ar<=1.1:
questionCnts.append(c)
#将这些轮廓(每一个圆形的轮廓)按照从上到下进行排序
questionCnts = sort_contours(questionCnts,
method="top-to-bottom")[0]
透视变换结果的二值化结果
找到上图中的轮廓,画出
给轮廓排序的函数
def sort_contours(cnts,method="left-to-right"):
reverse = False
i = 0
if method == "right-to-left" or method == "bottom-to-top":
reverse = True
if method == "top-to-bottom" or method == "bottom-to-top":
i = 1
boundingBoxes = [cv2.boundingRect(c) for c in cnts]
(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
key=lambda b: b[1][i], reverse=reverse))
return cnts,boundingBoxes
- 给每个题的五个选项的轮廓从左到右排序,制作每个选项的黑白掩模,与二值化后的图像做与操作,检测每个结果中白色像素点的数量,数量大的就是选择的选项,记录下来与正确选项对比,计算正确的个数。
for (q,i) in enumerate(np.arange(0,len(questionCnts),5)):
cnts = sort_contours(questionCnts[i:i+5])[0]
bubble = None
for (j,c) in enumerate(cnts):
mask = np.zeros(thresh.shape,dtype="uint8")
cv2.drawContours(mask,[c],-1,255,-1)
cv_show('mask',mask)
#通过计算非零像素点的数量来计算是否选择这个答案
mask = cv2.bitwise_and(thresh,thresh,mask=mask)
#通过与操作,将答题卡上的涂答案的那一个圈(图像中这里是全黑的)全部变成白色
cv_show('mask',mask)
total = cv2.countNonZero(mask)
if bubble is None or total>bubble[0]:
bubble = (total,j)
color = (0,0,255)
k = ANSWER_KEY[q] #q代表现在检查的是第q个题
#k是正确答案
if k == bubble[1]:
color = (0,255,0)
correct += 1
cv2.drawContours(warped,[cnts[k]],-1,color,3)
选项掩模的图像
- 计算出结果,可视化输出
score = (correct/5.0) * 100
print("[INFO] score:{:.2f}%".format(score)) #这个%只是为了显示为60%,没有其他意思
cv2.putText(warped,"{:.2f}%".format(score),(10,30),cv2.FONT_HERSHEY_SIMPLEX,0.9,3)
cv_show("Original",image)
cv_show("Exam",warped)
四、收获
- 边缘检测后的图像进行轮廓检测的结果更好。
- zip与zip(星号)
- zip(x,y)是将x中的元素与y中的元素一一对应,形成一个一个的元组(x1,y1),(x2,y2)…
- zip(*)相当于解压,是zip的逆过程
a = [1,3,5,7,9]
b = [11,33,55,77,99]
c = list(zip(a,b))
print(c)
# list
d, e = zip(*c) #zip(*)相当于解压,是zip的逆过程
# print(type(e))
print(d)
print(e)
# 打印结果:
# [(1, 11), (3, 33), (5, 55), (7, 77), (9, 99)]
# <class 'tuple'>
# (1, 3, 5, 7, 9)
# (11, 33, 55, 77, 99)
- np.arange()
- 函数返回一个有终点和起点的固定步长的排列,如[1,2,3,4,5],起点是1,终点是6,步长为1。
- 参数个数情况: np.arange()函数分为一个参数,两个参数,三个参数三种情况
- 1)一个参数时,参数值为终点,起点取默认值0,步长取默认值1。
- 2)两个参数时,第一个参数为起点,第二个参数为终点,步长取默认值1。
- 3)三个参数时,第一个参数为起点,第二个参数为终点,第三个参数为步长。其中步长支持小数
- "{:.2f}”和“%.2f"的区别:
- 一种是表达式:‘%.2f’ % num,在Python2.x和3.x都可用
- 另一种是字符串对象的方法:‘{0:.2f}’.format(num),仅Python3.x可用
correct=3
score = (correct/5.0) * 100
print("[INFO] score:{:.2f}%".format(score))
>>>[INFO] score:60.00%