如何使用OpenCV-Python实现识别答题卡判卷功能

如何使用OpenCV-Python实现识别答题卡判卷功能

这篇文章主要为大家展示了“如何使用OpenCV-Python实现识别答题卡判卷功能”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“如何使用OpenCV-Python实现识别答题卡判卷功能”这篇文章吧。

任务

识别用相机拍下来的答题卡,并判断最终得分(假设正确答案是B, E, A, D, B)

主要步骤

  1. 轮廓识别——答题卡边缘识别

  2. 透视变换——提取答题卡主体

  3. 轮廓识别——识别出所有圆形选项,剔除无关轮廓

  4. 检测每一行选择的是哪一项,并将结果储存起来,记录正确的个数

  5. 计算最终得分并在图中标注

分步实现

轮廓识别——答题卡边缘识别

输入图像

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实现识别答题卡判卷功能”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注恰卡编程网行业资讯频道!

发布于 2022-03-03 21:29:37
收藏
分享
海报
0 条评论
41
上一篇:vue项目中如何使用websocket 下一篇:JavaWeb如何使用mvc模式实现登录功能
目录

    0 条评论

    本站已关闭游客评论,请登录或者注册后再评论吧~

    忘记密码?

    图形验证码