Opencv实现答题卡检测

目录

前言

步骤解析

1、导入必要的模块

2、定义辅助函数

(1)、图像显示函数

(2)、确定答题卡的四个角点坐标,并进行排序函数

(3)、对图像进行透视变换函数

(4)对给定的轮廓列表进行排序函数

3、主程序流程

[(1) 、读取和处理图像](#(1) 、读取和处理图像)

(2)、寻找答题卡轮廓

(3)、处理变换过的区域

(4)、识别答题区域(答题卡的作答区)

(5)、判断答案的正确性

(6)、计算得分并在答题卡上显示结果

完整代码

总结


前言

在图像处理领域,Python 的 OpenCV 库为我们提供了强大的工具。今天我们来探讨一段有趣的代码,它能够实现试卷的自动阅卷功能。

步骤解析

1、导入必要的模块

python 复制代码
import numpy as np
import cv2

2、定义辅助函数

(1)、图像显示函数

用于显示图像,接受图像名称和图像矩阵作为参数,显示图像并等待用户按键

python 复制代码
def cv_show(name, img):
    cv2.imshow(name, img)
    cv2.waitKey(0)
(2)、确定答题卡的四个角点坐标,并进行排序函数

对给定的四个点进行排序,以便确定它们分别对应左上、右上、右下和左下的位置

python 复制代码
def order_points(pts):
    # 一共4个坐标点
    rect = np.zeros((4, 2), dtype="float32")  # 用来存储排序之后的坐标位置
    # 按顺序找到对应坐标0123分别是 左上,右上,右下,左下
    s = pts.sum(axis=1)  # 对pts矩阵的每一行进行求和操作。(x+y)
    rect[0] = pts[np.argmin(s)]
    rect[2] = pts[np.argmax(s)]
    diff = np.diff(pts, axis=1)  # 对pts矩阵的每一行进行求差操作。(y-x)
    rect[1] = pts[np.argmin(diff)]
    rect[3] = pts[np.argmax(diff)]
    return rect
(3)、对图像进行透视变换函数

对图像进行透视变换,将一个包含四个点的不规则四边形变换为规则的矩形

python 复制代码
# 将透视扭曲的矩形变换成一个规则的矩阵
def four_point_transform(image, pts):
    # 获取输入坐标点
    rect = order_points(pts)  # 为上述排序的四个点
    (tl, tr, br, bl) = rect  # 分别返回给四个值,分别表示为左上、右上、右下、左下
    # 计算输入的w和h值
    widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))  # 计算四边形底边的宽度
    widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))  # 计算顶边的宽度
    maxWidth = max(int(widthA), int(widthB))  # 返回最大宽度
    heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))  # 计算左上角到右下角的对角线长度
    heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[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(src,dst[,solveMethod])→ M获得转换之间的关系
    # cv2.warpPerspective(src, Mp, dsizel, dstl, flagsl, borderModel, borderValue]]1])- dst
    # #参数说明:
    # src:变换前图像四边形顶点坐标/第2个是原图
    # MP:透视变换矩阵,3行3列
    # dsize:输出图像的大小,二元元组(width,heiqht)
    M = cv2.getPerspectiveTransform(rect, dst)  # 根据原始点和变换后的点计算透视变换矩阵M
    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))  # 对原始图像,针推变换矩阵和输出图像大小进行透视变换,返回变换后的图片
    # 返回变换后的结果
    return warped
(4)对给定的轮廓列表进行排序函数

此函数可以按照指定的方法(如从左到右、从右到左、从上到下、从下到上)对轮廓进行排序

python 复制代码
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

3、主程序流程

(1) 、读取和处理图像
  • 读取图像test_01.png并复制一份用于后续绘制轮廓。
  • 将图像转换为灰度图像。
  • 使用高斯模糊对灰度图像进行平滑处理。
  • 使用 Canny 边缘检测算法检测图像中的边缘。
  • 绘制检测到的边缘轮廓在复制的图像上并显示。
python 复制代码
ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}  # 定义一个字典,里面存放答题卡正确答案
image = cv2.imread(r'image/test_01.png')
contours_img = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
cv_show('blurred', blurred)
edged = cv2.Canny(blurred, 75, 200)
cv_show('edged', edged)
(2)、寻找答题卡轮廓
  • 使用cv2.findContours函数找到边缘图像中的所有轮廓。
  • 按照轮廓面积从大到小对轮廓进行排序。
  • 遍历轮廓,通过轮廓的周长和近似多边形来寻找近似为四边形的轮廓,认为这个四边形是文档的轮廓。
  • 对找到的文档轮廓进行透视变换,将文档区域变换为规则的矩形并显示。
python 复制代码
# 轮廓检测
cnts = cv2.findContours(edged.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

# 根据轮廓大小进行排序
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
for c in cnts:
    peri = cv2.arcLength(c, True)
    approx = cv2.approxPolyDP(c, 0.002 * peri, True)

    if len(approx) == 4:
        docCnt = approx
        break

# 透视变换
warped_t = four_point_transform(image, docCnt.reshape(4, 2))
warped_new = warped_t.copy()
cv_show('warped', warped_t)
(3)、处理变换过的区域
  • 将透视变换后的图像转换为灰度图像。
  • 使用阈值处理将灰度图像转换为二值图像。
  • 再次找到二值图像中的所有轮廓并在变换后的图像上绘制轮廓并显示。
python 复制代码
warped = cv2.cvtColor(warped_t, cv2.COLOR_BGR2GRAY)
# 阈值处理
thresh = cv2.threshold(warped, 0, 255,
                       cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)
thresh_Contours = thresh.copy()

# 找到每一个轮廓
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
warped_Contours = cv2.drawContours(warped_t, cnts, -1, (0, 255, 0), 1)
cv_show('warped_Contours', warped_Contours)
(4)、识别答题区域(答题卡的作答区)
  • 遍历二值图像中的轮廓,根据轮廓的宽高比、大小等条件筛选出可能的答题区域轮廓。
  • 对每个问题的答题区域轮廓按照从上到下的顺序进行排序。
python 复制代码
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]
(5)、判断答案的正确性
  • 对于每个问题,遍历其五个选项的轮廓,使用掩码和计数非零像素的方法来判断哪个选项被选中。
  • 根据预设的答案键来判断答案是否正确,用不同颜色在变换后的图像上绘制正确和错误的答案轮廓并显示。
python 复制代码
correct = 0
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):
    cnts = sort_contours(questionCnts[i:i + 5])[0]
    bubbled = None
    # 遍历每一个结果
    for (j, c) in enumerate(cnts):
        # 使用mask来判断结果
        mask = np.zeros(thresh.shape, dtype="uint8")
        cv2.drawContours(mask, [c], -1, 255, -1)
        cv_show('mask', mask)

        thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask)
        cv_show('thresh_mask_and', thresh_mask_and)
        total = cv2.countNonZero(thresh_mask_and)
        if bubbled is None or total > bubbled[0]:
            bubbled = (total, j)
    color = (0, 0, 255)
    k = ANSWER_KEY[q]

    if k == bubbled[1]:
        color = (0, 255, 0)
        correct += 1
    cv2.drawContours(warped_new, [cnts[k]], -1, color, 3)
    cv_show("warpeding", warped_new)
(6)、计算得分并在答题卡上显示结果
  • 根据正确答案的数量计算得分。
  • 在变换后的图像上显示得分。
python 复制代码
score = (correct / 5.0) * 100
print("[INFO] score:{:.2f}%".format(score))
cv2.putText(warped_new, "{:.2f}%".format(score), (10, 30),
            cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imshow("Original", image)
cv2.imshow("Exam", warped_new)
cv2.waitKey(0)

完整代码

python 复制代码
import numpy as np
import cv2


def cv_show(name, img):
    cv2.imshow(name, img)
    cv2.waitKey(0)

def order_points(pts):
    # 一共4个坐标点
    rect = np.zeros((4, 2), dtype="float32")  # 用来存储排序之后的坐标位置
    # 按顺序找到对应坐标0123分别是 左上,右上,右下,左下
    s = pts.sum(axis=1)  # 对pts矩阵的每一行进行求和操作。(x+y)
    rect[0] = pts[np.argmin(s)]
    rect[2] = pts[np.argmax(s)]
    diff = np.diff(pts, axis=1)  # 对pts矩阵的每一行进行求差操作。(y-x)
    rect[1] = pts[np.argmin(diff)]
    rect[3] = pts[np.argmax(diff)]
    return rect

# 将透视扭曲的矩形变换成一个规则的矩阵
def four_point_transform(image, pts):
    # 获取输入坐标点
    rect = order_points(pts)  # 为上述排序的四个点
    (tl, tr, br, bl) = rect  # 分别返回给四个值,分别表示为左上、右上、右下、左下
    # 计算输入的w和h值
    widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))  # 计算四边形底边的宽度
    widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))  # 计算顶边的宽度
    maxWidth = max(int(widthA), int(widthB))  # 返回最大宽度
    heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))  # 计算左上角到右下角的对角线长度
    heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[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(src,dst[,solveMethod])→ M获得转换之间的关系
    # cv2.warpPerspective(src, Mp, dsizel, dstl, flagsl, borderModel, borderValue]]1])- dst
    # #参数说明:
    # src:变换前图像四边形顶点坐标/第2个是原图
    # MP:透视变换矩阵,3行3列
    # dsize:输出图像的大小,二元元组(width,heiqht)
    M = cv2.getPerspectiveTransform(rect, dst)  # 根据原始点和变换后的点计算透视变换矩阵M
    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))  # 对原始图像,针推变换矩阵和输出图像大小进行透视变换,返回变换后的图片
    # 返回变换后的结果
    return warped

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

ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}  # 定义一个字典,里面存放答题卡正确答案
image = cv2.imread(r'image/test_01.png')
contours_img = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
cv_show('blurred', blurred)
edged = cv2.Canny(blurred, 75, 200)
cv_show('edged', edged)

# 轮廓检测
cnts = cv2.findContours(edged.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

# 根据轮廓大小进行排序
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
for c in cnts:
    peri = cv2.arcLength(c, True)
    approx = cv2.approxPolyDP(c, 0.002 * peri, True)

    if len(approx) == 4:
        docCnt = approx
        break

# 透视变换
warped_t = four_point_transform(image, docCnt.reshape(4, 2))
warped_new = warped_t.copy()
cv_show('warped', warped_t)

warped = cv2.cvtColor(warped_t, cv2.COLOR_BGR2GRAY)
# 阈值处理
thresh = cv2.threshold(warped, 0, 255,
                       cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)
thresh_Contours = thresh.copy()

# 找到每一个轮廓
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[1]
warped_Contours = cv2.drawContours(warped_t, cnts, -1, (0, 255, 0), 1)
cv_show('warped_Contours', warped_Contours)

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]

correct = 0
for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):
    cnts = sort_contours(questionCnts[i:i + 5])[0]
    bubbled = None
    # 遍历每一个结果
    for (j, c) in enumerate(cnts):
        # 使用mask来判断结果
        mask = np.zeros(thresh.shape, dtype="uint8")
        cv2.drawContours(mask, [c], -1, 255, -1)
        cv_show('mask', mask)

        thresh_mask_and = cv2.bitwise_and(thresh, thresh, mask=mask)
        cv_show('thresh_mask_and', thresh_mask_and)
        total = cv2.countNonZero(thresh_mask_and)
        if bubbled is None or total > bubbled[0]:
            bubbled = (total, j)
    color = (0, 0, 255)
    k = ANSWER_KEY[q]

    if k == bubbled[1]:
        color = (0, 255, 0)
        correct += 1
    cv2.drawContours(warped_new, [cnts[k]], -1, color, 3)
    cv_show("warpeding", warped_new)

score = (correct / 5.0) * 100
print("[INFO] score:{:.2f}%".format(score))
cv2.putText(warped_new, "{:.2f}%".format(score), (10, 30),
            cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imshow("Original", image)
cv2.imshow("Exam", warped_new)
cv2.waitKey(0)

总结

此项目让我们见识了 OpenCV 在图像处理领域的强大功能,包括它对图像的一系列预处理、轮廓检测、透视变换等强大功能。Opencv库的功能和用法远不止于此,还需要我们更加深入的学习和探讨

相关推荐
珠海新立电子科技有限公司3 分钟前
FPC柔性线路板与智能生活的融合
人工智能·生活·制造
IT古董17 分钟前
【机器学习】机器学习中用到的高等数学知识-8. 图论 (Graph Theory)
人工智能·机器学习·图论
曼城周杰伦27 分钟前
自然语言处理:第六十三章 阿里Qwen2 & 2.5系列
人工智能·阿里云·语言模型·自然语言处理·chatgpt·nlp·gpt-3
余炜yw1 小时前
【LSTM实战】跨越千年,赋诗成文:用LSTM重现唐诗的韵律与情感
人工智能·rnn·深度学习
莫叫石榴姐1 小时前
数据科学与SQL:组距分组分析 | 区间分布问题
大数据·人工智能·sql·深度学习·算法·机器学习·数据挖掘
弗锐土豆2 小时前
工业生产安全-安全帽第二篇-用java语言看看opencv实现的目标检测使用过程
java·opencv·安全·检测·面部
如若1232 小时前
利用 `OpenCV` 和 `Matplotlib` 库进行图像读取、颜色空间转换、掩膜创建、颜色替换
人工智能·opencv·matplotlib
YRr YRr2 小时前
深度学习:神经网络中的损失函数的使用
人工智能·深度学习·神经网络
ChaseDreamRunner2 小时前
迁移学习理论与应用
人工智能·机器学习·迁移学习
Guofu_Liao2 小时前
大语言模型---梯度的简单介绍;梯度的定义;梯度计算的方法
人工智能·语言模型·矩阵·llama