如何使用OpenCV-Python实现识别答题卡判卷功能
如何使用OpenCV-Python实现识别答题卡判卷功能
这篇文章主要为大家展示了“如何使用OpenCV-Python实现识别答题卡判卷功能”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“如何使用OpenCV-Python实现识别答题卡判卷功能”这篇文章吧。
任务
识别用相机拍下来的答题卡,并判断最终得分(假设正确答案是B, E, A, D, B)
主要步骤
轮廓识别——答题卡边缘识别
透视变换——提取答题卡主体
轮廓识别——识别出所有圆形选项,剔除无关轮廓
检测每一行选择的是哪一项,并将结果储存起来,记录正确的个数
计算最终得分并在图中标注
分步实现
轮廓识别——答题卡边缘识别
输入图像
importcv2ascvimportnumpyasnp#正确答案right_key={0:1,1:4,2:0,3:3,4:1}#输入图像img=cv.imread('./images/test_01.jpg')img_copy=img.copy()img_gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)cvshow('img-gray',img_gray)
图像预处理
#图像预处理#高斯降噪img_gaussian=cv.GaussianBlur(img_gray,(5,5),1)cvshow('gaussianblur',img_gaussian)#canny边缘检测img_canny=cv.Canny(img_gaussian,80,150)cvshow('canny',img_canny)
轮廓识别——答题卡边缘识别
#轮廓识别——答题卡边缘识别cnts,hierarchy=cv.findContours(img_canny,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)cv.drawContours(img_copy,cnts,-1,(0,0,255),3)cvshow('contours-show',img_copy)
透视变换——提取答题卡主体
对每个轮廓进行拟合,将多边形轮廓变为四边形
docCnt=None#确保检测到了iflen(cnts)>0:#根据轮廓大小进行排序cnts=sorted(cnts,key=cv.contourArea,reverse=True)#遍历每一个轮廓forcincnts:#近似peri=cv.arcLength(c,True)#arclength计算一段曲线的长度或者闭合曲线的周长;#第一个参数输入一个二维向量,第二个参数表示计算曲线是否闭合approx=cv.approxPolyDP(c,0.02*peri,True)#用一条顶点较少的曲线/多边形来近似曲线/多边形,以使它们之间的距离<=指定的精度;#c是需要近似的曲线,0.02*peri是精度的最大值,True表示曲线是闭合的#准备做透视变换iflen(approx)==4:docCnt=approxbreak
透视变换——提取答题卡主体
#透视变换——提取答题卡主体docCnt=docCnt.reshape(4,2)warped=four_point_transform(img_gray,docCnt)cvshow('warped',warped)
deffour_point_transform(img,four_points):rect=order_points(four_points)(tl,tr,br,bl)=rect#计算输入的w和h的值widthA=np.sqrt((tr[0]-tl[0])**2+(tr[1]-tl[1])**2)widthB=np.sqrt((br[0]-bl[0])**2+(br[1]-bl[1])**2)maxWidth=max(int(widthA),int(widthB))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(heightA),int(heightB))#变换后对应的坐标位置dst=np.array([[0,0],[maxWidth-1,0],[maxWidth-1,maxHeight-1],[0,maxHeight-1]],dtype='float32')#最主要的函数就是cv2.getPerspectiveTransform(rect,dst)和cv2.warpPerspective(image,M,(maxWidth,maxHeight))M=cv.getPerspectiveTransform(rect,dst)warped=cv.warpPerspective(img,M,(maxWidth,maxHeight))returnwarpeddeforder_points(points):res=np.zeros((4,2),dtype='float32')#按照从前往后0,1,2,3分别表示左上、右上、右下、左下的顺序将points中的数填入res中#将四个坐标x与y相加,和最大的那个是右下角的坐标,最小的那个是左上角的坐标sum_hang=points.sum(axis=1)res[0]=points[np.argmin(sum_hang)]res[2]=points[np.argmax(sum_hang)]#计算坐标x与y的离散插值np.diff()diff=np.diff(points,axis=1)res[1]=points[np.argmin(diff)]res[3]=points[np.argmax(diff)]#返回resultreturnres
轮廓识别——识别出选项
#轮廓识别——识别出选项thresh=cv.threshold(warped,0,255,cv.THRESH_BINARY_INV|cv.THRESH_OTSU)[1]cvshow('thresh',thresh)thresh_cnts,_=cv.findContours(thresh,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)w_copy=warped.copy()cv.drawContours(w_copy,thresh_cnts,-1,(0,0,255),2)cvshow('warped_contours',w_copy)questionCnts=[]#遍历,挑出选项的cntsforcinthresh_cnts:(x,y,w,h)=cv.boundingRect(c)ar=w/float(h)#根据实际情况指定标准ifw>=20andh>=20andar>=0.9andar<=1.1:questionCnts.append(c)#检查是否挑出了选项w_copy2=warped.copy()cv.drawContours(w_copy2,questionCnts,-1,(0,0,255),2)cvshow('questionCnts',w_copy2)
成功将无关轮廓剔除
检测每一行选择的是哪一项,并将结果储存起来,记录正确的个数
#检测每一行选择的是哪一项,并将结果储存在元组bubble中,记录正确的个数correct#按照从上到下t2b对轮廓进行排序questionCnts=sort_contours(questionCnts,method="t2b")[0]correct=0#每行有5个选项for(i,q)inenumerate(np.arange(0,len(questionCnts),5)):#排序cnts=sort_contours(questionCnts[q:q+5])[0]bubble=None#得到每一个选项的mask并填充,与正确答案进行按位与操作获得重合点数for(j,c)inenumerate(cnts):mask=np.zeros(thresh.shape,dtype='uint8')cv.drawContours(mask,[c],-1,255,-1)#cvshow('mask',mask)#通过按位与操作得到thresh与mask重合部分的像素数量bitand=cv.bitwise_and(thresh,thresh,mask=mask)totalPixel=cv.countNonZero(bitand)ifbubbleisNoneorbubble[0]<totalPixel:bubble=(totalPixel,j)k=bubble[1]color=(0,0,255)ifk==right_key[i]:correct+=1color=(0,255,0)#绘图cv.drawContours(warped,[cnts[right_key[i]]],-1,color,3)cvshow('final',warped)
defsort_contours(contours,method="l2r"):#用于给轮廓排序,l2r,r2l,t2b,b2treverse=Falsei=0ifmethod=="r2l"ormethod=="b2t":reverse=Trueifmethod=="t2b"ormethod=="b2t":i=1boundingBoxes=[cv.boundingRect(c)forcincontours](contours,boundingBoxes)=zip(*sorted(zip(contours,boundingBoxes),key=lambdaa:a[1][i],reverse=reverse))returncontours,boundingBoxes
用透过mask的像素的个数来判断考生选择的是哪个选项
计算最终得分并在图中标注
#计算最终得分并在图中标注score=(correct/5.0)*100print(f"Score:{score}%")cv.putText(warped,f"Score:{score}%",(10,30),cv.FONT_HERSHEY_SIMPLEX,0.9,(0,0,255),2)cv.imshow("Original",img)cv.imshow("Exam",warped)cv.waitKey(0)
完整代码
importcv2ascvimportnumpyasnpdefcvshow(name,img):cv.imshow(name,img)cv.waitKey(0)cv.destroyAllWindows()deffour_point_transform(img,four_points):rect=order_points(four_points)(tl,tr,br,bl)=rect#计算输入的w和h的值widthA=np.sqrt((tr[0]-tl[0])**2+(tr[1]-tl[1])**2)widthB=np.sqrt((br[0]-bl[0])**2+(br[1]-bl[1])**2)maxWidth=max(int(widthA),int(widthB))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(heightA),int(heightB))#变换后对应的坐标位置dst=np.array([[0,0],[maxWidth-1,0],[maxWidth-1,maxHeight-1],[0,maxHeight-1]],dtype='float32')#最主要的函数就是cv2.getPerspectiveTransform(rect,dst)和cv2.warpPerspective(image,M,(maxWidth,maxHeight))M=cv.getPerspectiveTransform(rect,dst)warped=cv.warpPerspective(img,M,(maxWidth,maxHeight))returnwarpeddeforder_points(points):res=np.zeros((4,2),dtype='float32')#按照从前往后0,1,2,3分别表示左上、右上、右下、左下的顺序将points中的数填入res中#将四个坐标x与y相加,和最大的那个是右下角的坐标,最小的那个是左上角的坐标sum_hang=points.sum(axis=1)res[0]=points[np.argmin(sum_hang)]res[2]=points[np.argmax(sum_hang)]#计算坐标x与y的离散插值np.diff()diff=np.diff(points,axis=1)res[1]=points[np.argmin(diff)]res[3]=points[np.argmax(diff)]#返回resultreturnresdefsort_contours(contours,method="l2r"):#用于给轮廓排序,l2r,r2l,t2b,b2treverse=Falsei=0ifmethod=="r2l"ormethod=="b2t":reverse=Trueifmethod=="t2b"ormethod=="b2t":i=1boundingBoxes=[cv.boundingRect(c)forcincontours](contours,boundingBoxes)=zip(*sorted(zip(contours,boundingBoxes),key=lambdaa:a[1][i],reverse=reverse))returncontours,boundingBoxes#正确答案right_key={0:1,1:4,2:0,3:3,4:1}#输入图像img=cv.imread('./images/test_01.jpg')img_copy=img.copy()img_gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)cvshow('img-gray',img_gray)#图像预处理#高斯降噪img_gaussian=cv.GaussianBlur(img_gray,(5,5),1)cvshow('gaussianblur',img_gaussian)#canny边缘检测img_canny=cv.Canny(img_gaussian,80,150)cvshow('canny',img_canny)#轮廓识别——答题卡边缘识别cnts,hierarchy=cv.findContours(img_canny,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)cv.drawContours(img_copy,cnts,-1,(0,0,255),3)cvshow('contours-show',img_copy)docCnt=None#确保检测到了iflen(cnts)>0:#根据轮廓大小进行排序cnts=sorted(cnts,key=cv.contourArea,reverse=True)#遍历每一个轮廓forcincnts:#近似peri=cv.arcLength(c,True)#arclength计算一段曲线的长度或者闭合曲线的周长;#第一个参数输入一个二维向量,第二个参数表示计算曲线是否闭合approx=cv.approxPolyDP(c,0.02*peri,True)#用一条顶点较少的曲线/多边形来近似曲线/多边形,以使它们之间的距离<=指定的精度;#c是需要近似的曲线,0.02*peri是精度的最大值,True表示曲线是闭合的#准备做透视变换iflen(approx)==4:docCnt=approxbreak#透视变换——提取答题卡主体docCnt=docCnt.reshape(4,2)warped=four_point_transform(img_gray,docCnt)cvshow('warped',warped)#轮廓识别——识别出选项thresh=cv.threshold(warped,0,255,cv.THRESH_BINARY_INV|cv.THRESH_OTSU)[1]cvshow('thresh',thresh)thresh_cnts,_=cv.findContours(thresh,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)w_copy=warped.copy()cv.drawContours(w_copy,thresh_cnts,-1,(0,0,255),2)cvshow('warped_contours',w_copy)questionCnts=[]#遍历,挑出选项的cntsforcinthresh_cnts:(x,y,w,h)=cv.boundingRect(c)ar=w/float(h)#根据实际情况指定标准ifw>=20andh>=20andar>=0.9andar<=1.1:questionCnts.append(c)#检查是否挑出了选项w_copy2=warped.copy()cv.drawContours(w_copy2,questionCnts,-1,(0,0,255),2)cvshow('questionCnts',w_copy2)#检测每一行选择的是哪一项,并将结果储存在元组bubble中,记录正确的个数correct#按照从上到下t2b对轮廓进行排序questionCnts=sort_contours(questionCnts,method="t2b")[0]correct=0#每行有5个选项for(i,q)inenumerate(np.arange(0,len(questionCnts),5)):#排序cnts=sort_contours(questionCnts[q:q+5])[0]bubble=None#得到每一个选项的mask并填充,与正确答案进行按位与操作获得重合点数for(j,c)inenumerate(cnts):mask=np.zeros(thresh.shape,dtype='uint8')cv.drawContours(mask,[c],-1,255,-1)cvshow('mask',mask)#通过按位与操作得到thresh与mask重合部分的像素数量bitand=cv.bitwise_and(thresh,thresh,mask=mask)totalPixel=cv.countNonZero(bitand)ifbubbleisNoneorbubble[0]<totalPixel:bubble=(totalPixel,j)k=bubble[1]color=(0,0,255)ifk==right_key[i]:correct+=1color=(0,255,0)#绘图cv.drawContours(warped,[cnts[right_key[i]]],-1,color,3)cvshow('final',warped)#计算最终得分并在图中标注score=(correct/5.0)*100print(f"Score:{score}%")cv.putText(warped,f"Score:{score}%",(10,30),cv.FONT_HERSHEY_SIMPLEX,0.9,(0,0,255),2)cv.imshow("Original",img)cv.imshow("Exam",warped)cv.waitKey(0)
以上是“如何使用OpenCV-Python实现识别答题卡判卷功能”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注恰卡编程网行业资讯频道!
推荐阅读
-
python多行注释符号怎么表示
python多行注释符号怎么表示这篇文章主要介绍“python多行...
-
python支持的操作系统是什么
python支持的操作系统是什么这篇文章主要介绍“python支持...
-
python如何判断列表为空
python如何判断列表为空这篇文章主要介绍“python如何判断...
-
Python如何利用D3Blocks绘制可动态交互的图表
-
2021年度编程语言揭晓
-
PPython:PHP 拥抱 Python 的利器
-
哪种Python IDE最适合你?这里有一份优缺点列表
-
Python分隔字符串函数用法split
aaa,bbb=str.split(‘&&’,2)第一个参数为分隔符第二个参数是要完成的最大拆分数...
-
php安全编程——python测试实例编写
-
神奇的Python模块:pdfkit,将Python抓取的网址内容保存pdf文件