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库的功能和用法远不止于此,还需要我们更加深入的学习和探讨

相关推荐
Topstip4 分钟前
Gemini 对话机器人加入开源盲水印技术来检测 AI 生成的内容
人工智能·ai·机器人
Bearnaise7 分钟前
PointMamba: A Simple State Space Model for Point Cloud Analysis——点云论文阅读(10)
论文阅读·笔记·python·深度学习·机器学习·计算机视觉·3d
小嗷犬19 分钟前
【论文笔记】VCoder: Versatile Vision Encoders for Multimodal Large Language Models
论文阅读·人工智能·语言模型·大模型·多模态
Struart_R25 分钟前
LVSM: A LARGE VIEW SYNTHESIS MODEL WITH MINIMAL 3D INDUCTIVE BIAS 论文解读
人工智能·3d·transformer·三维重建
lucy1530275107926 分钟前
【青牛科技】GC5931:工业风扇驱动芯片的卓越替代者
人工智能·科技·单片机·嵌入式硬件·算法·机器学习
jndingxin1 小时前
OpenCV相机标定与3D重建(1)概述
数码相机·opencv·3d
幻风_huanfeng1 小时前
线性代数中的核心数学知识
人工智能·机器学习
volcanical1 小时前
LangGPT结构化提示词编写实践
人工智能
weyson2 小时前
CSharp OpenAI
人工智能·语言模型·chatgpt·openai
RestCloud2 小时前
ETLCloud异常问题分析ai功能
人工智能·ai·数据分析·etl·数据集成工具·数据异常