OpenCV经典案例:01 答题卡识别

🌞欢迎莅临我的个人主页👈🏻这里是我专注于深度学习领域、用心分享知识精粹与智慧火花的独特角落!🍉

🌈如果大家喜欢文章,欢迎:关注🍷+点赞👍🏻+评论✍🏻+收藏🌟,如有错误敬请指正!🪐

🍓“请不要相信胜利就像山坡上的蒲公英一样唾手可得,但是请相信生活中总有美好值得我们全力以赴,哪怕粉身碎骨!”🌹

目录

透视变换矫正

选项识别匹配

QT 界面设计


引言:随着信息化的发展,计算机阅卷已经成为一种常规操作。在大型考试中,客观题基本不再 需要人工阅卷。本项目旨在开发一个基于OpenCV的高效答题卡识别系统,通过先进的图像处理和模式识别技术,实现对答题卡的快速准确分析。

文章所有资源请看文末!

透视变换矫正

假如有一张答题卡平放在地面上,那我们怎样去找到答题卡的边界轮廓呢?

答案是透视变换。首先我们需要找到答题卡的轮廓才能对选项做各种处理呀,接下来就是对透视变换的方法说明了。

假设原始图像中的点为(x,y),目标图像中的对应点为(X,Y)。透视变换可以用一个 3x3 的矩阵M来描述:

\begin{bmatrix} X\\ Y\\ 1 \end{bmatrix}=M\times \begin{bmatrix} x\\ y\\ 1 \end{bmatrix}

其中,矩阵M的元素取决于原始四边形和目标四边形顶点的坐标。其核心原理在于通过建立原始图像和目标图像之间的对应点关系,来计算一个变换矩阵

综上所述,使用透视变换扫描得到答题卡边界具体步骤如下:

  1. 找到原始图像的4个顶点和目标图像的4个顶点
  2. 根据8个顶点构造原始图像到目标图像的变换矩阵
  3. 依据变换矩阵,实现原始图像到目标图像的变换,完成倾斜矫正

注意:用于构造变换矩阵使用的原始图像的4个顶点和目标图像的4个顶点的位置必须是匹配的,也就是说,要将左上、右上、左下、右下4个顶点按照相同的顺序排列。

OK,下面我们直接根据代码来进行说明。

import cv2
import math
import numpy as np

# x坐标
def sortBy_x(pt):
    return pt[0]

# y坐标
def sortBy_y(pt):
    return pt[1]


def correct(path):
    try:
        answerSheet = cv2.imread(path)
        gray = cv2.cvtColor(answerSheet, cv2.COLOR_BGR2GRAY)
        blurred = cv2.GaussianBlur(gray, (3, 3), 0)
        canny = cv2.Canny(blurred, 75, 200)
        contours, Hierarchy = cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        if len(contours) == 1:
            result_contour = contours[0]
        else:
            max_length = -1
            index = -1
            for i, contour in enumerate(contours):
                length = cv2.arcLength(contour, True)
                if length > max_length:
                    max_length = length
                    index = i
            result_contour = contours[index]
        pts = cv2.approxPolyDP(result_contour, 0.02 * cv2.arcLength(result_contour, True), True)
        if len(pts) != 4:
            raise ValueError("透视变换需要四个点,但检测到的点数量为{}".format(len(pts)))

        pts = np.array([pt[0] for pt in pts])  # 提取点坐标
        print(pts)
        pts = sorted(pts, key=sortBy_x)
        print(pts)
        pts = sorted(pts, key=sortBy_y)
        print(pts)
        print(pts[0][0])
        width1 = math.sqrt((pts[0][0] - pts[1][0]) ** 2 + (pts[0][1] - pts[1][1]) ** 2)
        width2 = math.sqrt((pts[2][0] - pts[3][0]) ** 2 + (pts[2][1] - pts[3][1]) ** 2)
        width = int(max(width1, width2))

        height1 = math.sqrt((pts[0][0] - pts[3][0]) ** 2 + (pts[0][1] - pts[3][1]) ** 2)
        height2 = math.sqrt((pts[2][0] - pts[1][0]) ** 2 + (pts[2][1] - pts[1][1]) ** 2)
        height = int(max(height1, height2))

        pts_dst = np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtype="float32")
        pts_src = np.array(pts, dtype="float32")
        M = cv2.getPerspectiveTransform(pts_src, pts_dst)
        birdMat = cv2.warpPerspective(answerSheet, M, (width, height))
        return birdMat
    except Exception as e:
        print(f"Error in correct: {e}")
        return None

1、首先对读取的图像进行一系列预处理操作:灰度转换、滤波、边缘检测等以凸显图像特征

2、使用cv2.findContours查找图像轮廓

当轮廓数量为1时,直接将其结果作为轮廓。

否则通过计算 每个轮廓的弧长,找到弧长最长的轮廓作为结果轮廓。

3、使用cv2.approxPolyDP函数对结果轮廓进行多边形逼近,得到近似的顶点坐标

4、将顶点坐标提取出来,并分别按照x坐标和y坐标进行排序,同时计算相邻两点之间的距离,取最大值作为宽度和高度,并据此计算目标顶点

5、cv2.getPerspectiveTransfor计算变换矩阵Mcv2.warpPerspective根据变换矩阵对原始图像进行透视变换,得到矫正后的图像

效果如下:

选项识别匹配

答题卡轮廓边界得到之后就是对选项的处理了。

import cv2
import numpy as np
import math


def sortBy_x(pt):
    return pt[0]


def sortBy_y(pt):
    return pt[1]


def recognition(path, imageIndex):
    try:
        answerSheet = cv2.imread(path)
        gray = cv2.cvtColor(answerSheet, cv2.COLOR_BGR2GRAY)
        blurred = cv2.GaussianBlur(gray, (3, 3), 0)
        canny = cv2.Canny(blurred, 75, 200)
        contours, _ = cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        if len(contours) == 1:
            result_contour = contours[0]
        else:
            max_length = -1
            index = -1
            for i, contour in enumerate(contours):
                length = cv2.arcLength(contour, True)
                if length > max_length:
                    max_length = length
                    index = i
            result_contour = contours[index]

        pts = cv2.approxPolyDP(result_contour, 0.02 * cv2.arcLength(result_contour, True), True)
        if len(pts) != 4:
            raise ValueError("识别需要四个点,但检测到的点数量为{}".format(len(pts)))

        pts = np.array([pt[0] for pt in pts])
        pts = sorted(pts, key=sortBy_x)
        pts = sorted(pts, key=sortBy_y)

        width1 = math.sqrt((pts[0][0] - pts[1][0]) ** 2 + (pts[0][1] - pts[1][1]) ** 2)
        width2 = math.sqrt((pts[2][0] - pts[3][0]) ** 2 + (pts[2][1] - pts[3][1]) ** 2)
        width = int(max(width1, width2))

        height1 = math.sqrt((pts[0][0] - pts[3][0]) ** 2 + (pts[0][1] - pts[3][1]) ** 2)
        height2 = math.sqrt((pts[2][0] - pts[1][0]) ** 2 + (pts[2][1] - pts[1][1]) ** 2)
        height = int(max(height1, height2))

        pts_dst = np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtype="float32")
        pts_src = np.array(pts, dtype="float32")
        M = cv2.getPerspectiveTransform(pts_src, pts_dst)
        birdMat = cv2.warpPerspective(answerSheet, M, (width, height))
        cv2.imshow("original", birdMat)

        #################   识别   ##############################
        gray_birdMat = cv2.cvtColor(birdMat, cv2.COLOR_BGR2GRAY)
        _, target = cv2.threshold(gray_birdMat, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
        cv2.imshow("Img", target)

        element = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
        target = cv2.dilate(target, element)
        cv2.imshow("image", target)

        # 提取选项
        contours, _ = cv2.findContours(target, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        # print(contours)
        selected_contours = [c for c in contours if cv2.boundingRect(c)[2] > 20 and cv2.boundingRect(c)[3] > 20]

        answerSheet_con = cv2.cvtColor(target, cv2.COLOR_GRAY2BGR)
        cv2.drawContours(answerSheet_con, selected_contours, -1, (0, 0, 255), 2)

        # 选项定位、二维数组存储
        radius = []
        center = []
        for contour in selected_contours:
            (x, y), r = cv2.minEnclosingCircle(contour)
            radius.append(r)
            center.append((int(x), int(y)))

        x_min = min(center, key=lambda x: x[0])[0]
        x_max = max(center, key=lambda x: x[0])[0]
        x_interval = (x_max - x_min) // 4

        y_min = min(center, key=lambda x: x[1])[1]
        y_max = max(center, key=lambda x: x[1])[1]
        y_interval = (y_max - y_min) // 4

        classed_contours = [[[] for _ in range(5)] for _ in range(5)]
        for i, point in enumerate(center):
            index_x = round((point[0] - x_min) / x_interval)
            index_y = round((point[1] - y_min) / y_interval)
            classed_contours[index_y][index_x] = selected_contours[i]

        colors = [(0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 0, 0), (0, 255, 0)]
        test_result = cv2.cvtColor(target, cv2.COLOR_GRAY2BGR)
        for i in range(5):
            for j in range(5):
                if len(classed_contours[i][j]) > 0:
                    cv2.drawContours(test_result, classed_contours[i][j], -1, colors[i], 2)

        # 答案自定义,只有 5个选项
        correct_answers = [0, 4, 4, 2, 1]

        # 定义选项位置
        result_count = np.zeros((5, 5), dtype=int)

        re_rect = [[cv2.boundingRect(contour) for contour in row] for row in classed_contours]
        count_roi = np.zeros((5, 5), dtype=np.float32)
        min_count = 999
        max_count = -1

        for i in range(5):
            for j in range(5):
                if len(classed_contours[i][j]) > 0:
                    rect = re_rect[i][j]
                    tem = target[rect[1]:rect[1] + rect[3], rect[0]:rect[0] + rect[2]]
                    count = cv2.countNonZero(tem)
                    if count > max_count:
                        max_count = count
                    if count < min_count:
                        min_count = count
                    count_roi[i][j] = count

        mean = (max_count - min_count) // 2
        option_diff = np.abs(count_roi - max_count)

        for i in range(5):
            for j in range(5):
                if option_diff[i][j] < mean:
                    result_count[i][j] += 1

        # 进行审阅
        label_answer = birdMat.copy()

        correct_count = 0
        wrong_answers = {}
        for i in range(5):
            selected = []
            for j in range(5):
                if result_count[i][j] == 1:
                    selected.append(j)
                    if j == correct_answers[i]:
                        cv2.drawContours(label_answer, classed_contours[i][j], -1, (255, 0, 0), 2)
                    else:
                        cv2.drawContours(label_answer, classed_contours[i][j], -1, (0, 0, 255), 2)

            # 记录题目数量、正确题数、错题
            if len(selected) == 0:
                continue  # 未作答,不做任何处理
            elif len(selected) == 1:
                if selected[0] == correct_answers[i]:
                    correct_count += 1
                else:
                    wrong_answers[i + 1] = chr(65 + selected[0])  # 错误选项
            else:
                blue_count = sum(1 for j in selected if j == correct_answers[i])
                red_count = len(selected) - blue_count
                if blue_count > 0 and red_count > 0:
                    wrong_answers[i + 1] = '多选'

        total_questions = len(correct_answers)
        score = correct_count / total_questions * 100

        data = {
            "序号": "{:02}".format(imageIndex + 1),
            "成绩": score,
            "题目总数": total_questions,
            "错题": str(wrong_answers),
            "正确题数": correct_count
        }

        return label_answer, data
    except Exception as e:
        print(f"Error in recognition: {e}")
        return None, None

1、首先仍就是图像预处理,这通常会使得我们更易于提取选项,得到其位置。将变换后的图像转为灰度图并进行反二阈值化凸显选项,随后进行膨胀操作以连接断开的部分或填充小的空洞。

2、提取选项轮廓。通过cv2.findContours得到所有轮廓,随后对每个轮廓进行筛选,只有宽度和高度均大于20像素的轮廓才会被保留下来,这样就能够得到选项了。

3、选项定位与分类。计算每个符合条件的轮廓的最小外接圆的圆心和半径。根据圆心坐标,将选项按照水平和垂直方向进行分类并存储到二维数组中。

4、答案识别与审阅。

  • 自定义正确答案,用数字标识答案位置,默认从0开始。
  • 为每个选项区域计算非零像素的数量。
  • 通过计算得到的数量与平均值,确定每个选项的选择情况并存储到二维数组中。

5、审阅结果展示与数据统计。比较二维数组与正确答案,绘制正确和错误选项的轮廓,正确为蓝色,错误为红色;同时统计正确题数、计算分数,并将相关数据存储到字典中。

效果如下:

QT 界面设计

        本次界面设计使用的是pyqt5,我也只是初学,所以做的界面不是很好,但也勉强还算看的过眼吧。这个界面其实就是把变换后的图像和识别检测的结果弄到展示窗口,然后把记录的数据信息这些保存到excel表而已,说实在的还是太简陋了呀。OK,下面我们直接看效果吧。

答题卡识别

        好的,以上就是本次项目的所有内容了,希望对大家有所帮助呀,有疑问的可以评论或私聊我解答哟!

文章所有资源有需要的可自取

百度网盘链接: https://pan.baidu.com/s/1pFeaKRGAwF1zfip_wqt_dQ         提取码: 0bw7

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悠眠小虫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值