欢迎光临
我们一直在努力

OpenCV实现答题卡识别与自动判分——从轮廓检测到透视变换前言

本文结合一个基于Python和OpenCV实现的答题卡识别程序,介绍其中涉及的灰度化、高斯模糊、Canny边缘检测、轮廓检测、轮廓近似、透视变换、阈值处理、掩膜、位运算以及像素统计等知识点。整个程序不需要使用深度学习模型,而是通过传统的计算机视觉方法完成基本的答题卡识别。


一、答题卡识别的整体思路

一张普通的答题卡图片,除了真正需要识别的选项之外,还可能包含背景、边框、文字以及其他干扰信息。如果直接统计整张图片中的像素,很难准确判断学生选择了哪个答案。

因此,需要按照一定的流程逐步处理图像。

整个程序的基本流程如下:

读取原始图片
↓
灰度化
↓
高斯模糊
↓
Canny边缘检测
↓
轮廓检测
↓
寻找答题卡四边形
↓
透视变换
↓
二值化
↓
检测选项轮廓
↓
筛选并排序选项
↓
掩膜提取选项区域
↓
统计非零像素
↓
确定学生答案
↓
与标准答案比较
↓
计算正确率

代码首先读取答题卡图片,然后通过灰度化和模糊处理减少图像中的干扰,再利用Canny算法提取边缘。之后通过轮廓检测找到答题卡的外轮廓,并利用四个角点进行透视变换,使答题卡变成比较规整的正视图。

完成透视变换之后,再对答题卡进行二值化处理,通过轮廓筛选找到每道题的选项区域。最后利用掩膜和非零像素统计判断哪个选项被填涂得最多,从而确定学生的答案。


二、图像预处理与答题卡区域检测

1. 灰度化和高斯模糊

程序读取图片后,首先将彩色图像转换成灰度图:

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

彩色图片通常包含B、G、R三个通道,而当前任务主要关注答题卡的边缘和轮廓,因此没有必要一直使用三个颜色通道。转换成灰度图后,图像只剩下一个通道,可以减少后续计算量。

接下来使用高斯模糊:

blurred = cv2.GaussianBlur(gray, (5, 5), 0)

高斯模糊的主要作用是降低图像噪声,使图像更加平滑。对于实际拍摄的答题卡来说,图片中可能存在一些细小的噪声,如果直接进行边缘检测,容易产生很多不需要的边缘,因此在Canny检测之前进行适当的模糊处理是比较常见的做法。

2. Canny边缘检测

经过高斯模糊后,使用Canny算法提取图像边缘:

edged = cv2.Canny(blurred, 75, 200)

Canny边缘检测可以寻找图像中像素变化比较明显的位置。答题卡的外框、选项圆圈等区域都具有比较明显的边缘,因此可以利用Canny结果作为后续轮廓检测的输入。

然后使用:

cnts = cv2.findContours(
edged.copy(),
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE
)[-2]

查找图像中的轮廓。

轮廓可以理解为图像中连续边缘形成的封闭区域。通过轮廓分析,就可以从复杂的图像中寻找具有特定形状的区域。代码还使用 cv2.drawContours() 将检测到的轮廓绘制到原图上,方便观察检测效果。

3. 利用轮廓近似寻找答题卡

得到轮廓以后,程序按照面积从大到小进行排序:

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

这里使用 cv2.approxPolyDP() 对轮廓进行近似。如果近似后的轮廓只有4个顶点,就将其认为是答题卡区域。

这种方法利用了答题卡整体接近矩形这一特点。通过轮廓面积和顶点数量进行筛选,可以比较简单地定位答题卡的外框。


三、四点透视变换与答题区域校正

找到答题卡之后,并不能直接进行选项识别。

这是因为实际拍摄时,摄像头可能并不是正对着答题卡。例如答题卡本身是一个矩形,但由于拍摄角度的原因,在照片中可能变成一个倾斜的四边形。

如果直接在这种图像上识别选项,就会增加判断难度。

因此,程序使用四点透视变换将答题卡调整为规则的矩形。

首先使用 order_points() 对检测到的四个角点进行排序:

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

这里利用坐标的和与差来判断四个点的位置。

对于左上角和右下角:

s = pts.sum(axis=1)

坐标和最小的点作为左上角,坐标和最大的点作为右下角。

对于另外两个点,通过:

diff = np.diff(pts, axis=1)

确定右上角和左下角。

经过排序后,四个点就可以按照左上、右上、右下、左下的顺序保存。

接下来计算变换后的宽度和高度,并构造目标坐标:

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)
)

完成透视变换。

透视变换完成以后,原本倾斜的答题卡就被校正成一个比较规整的矩形。

这个知识点不仅可以用于答题卡识别,还经常用于身份证识别、文档扫描、试卷校正、票据处理等场景。


四、选项检测与自动判分

完成透视变换之后,程序重新将图像转换为灰度图:

warped = cv2.cvtColor(
warped_t,
cv2.COLOR_BGR2GRAY
)

然后进行二值化处理:

thresh = cv2.threshold(
warped,
0,
255,
cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU
)[1]

这里使用了OTSU阈值算法。

OTSU可以根据图像的灰度分布自动寻找一个比较合适的阈值,不需要手动指定具体的阈值大小。同时使用反向二值化,使填涂区域更容易被后面的像素统计检测出来。

接下来再次进行轮廓检测:

cnts = cv2.findContours(
thresh,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE
)[-2]

此时检测到的轮廓主要来自答题卡上的选项区域。

为了排除其他无关轮廓,程序通过外接矩形的宽、高以及宽高比进行筛选:

(x, y, w, h) = cv2.boundingRect(c)

ar = w / float(h)

if w >= 20 and h >= 20 and 0.9 <= ar <= 1.1:
questionCnts.append(c)

这里要求宽度和高度都不能太小,同时宽高比需要接近1。因为答题卡选项通常是圆形或者接近正方形的区域,所以这种方法能够排除大量不符合要求的轮廓。

得到所有选项后,还需要对它们进行排序:

questionCnts = sort_contours(
questionCnts,
method="top-to-bottom"
)[0]

sort_contours()函数可以根据轮廓外接矩形的位置进行排序。程序先从上到下排列所有题目,然后每5个选项作为一道题,再对这一组选项从左到右排序。

确定每道题的5个选项后,就可以判断学生选择了哪个答案。

程序为每一个选项创建一个Mask:

mask = np.zeros(
thresh.shape,
dtype="uint8"
)

cv2.drawContours(
mask,
[c],
-1,
255,
-1
)

Mask的作用可以理解为一个“区域开关”。只让当前选项区域参与后续计算,其他区域全部忽略。

之后使用:

thresh_mask_and = cv2.bitwise_and(
thresh,
thresh,
mask=mask
)

进行位与操作,只保留当前选项内部的像素。

最后通过:

total = cv2.countNonZero(
thresh_mask_and
)

统计当前选项区域中非零像素的数量。

如果某个选项被学生填涂,那么这个区域中的非零像素通常会明显多于其他没有填涂的选项。因此程序只需要比较5个选项的 total 值,就可以找到填涂最多的选项:

if bubbled is None or total > bubbled[0]:
bubbled = (total, j)

这样就完成了学生答案的识别。


五、答案匹配与正确率计算

识别出学生答案以后,就需要和标准答案进行比较。

代码最开始定义了一个答案字典:

ANSWER_KEY = {
0: 1,
1: 4,
2: 0,
3: 3,
4: 1
}

这里的键代表题目编号,值代表正确选项的位置。

在完成每一道题的识别后,程序获取标准答案:

k = ANSWER_KEY[q]

然后将标准答案与学生选择的答案进行比较:

if k == bubbled[1]:
color = (0, 255, 0)
correct = correct + 1

如果答案正确,就增加正确题数量,并使用绿色进行标记;如果答案错误,则使用红色标记。

程序通过:

cv2.drawContours(
warped_new,
[cnts[k]],
-1,
color,
3
)

将对应选项绘制出来,使最终结果更加直观。

最后根据总题数计算正确率:

score = (correct / 5.0) * 100

再通过 cv2.putText() 将正确率写入图片:

cv2.putText(
warped_new,
'{:.2f}%'.format(score),
(10, 30),
cv2.FONT_HERSHEY_SIMPLEX,
0.9,
(0, 0, 255),
2
)

最终可以得到带有判题结果和正确率的答题卡图片,同时在控制台输出:

正确率:xx.xx%

整个程序到这里就完成了从原始答题卡图片到自动判分的完整过程。

赞(0)
未经允许不得转载:171主机测评 » OpenCV实现答题卡识别与自动判分——从轮廓检测到透视变换前言
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址