基于计算机视觉OpenCV的答题卡识别系统

本文介绍了一种基于计算机视觉的答题卡识别系统,利用OpenCV进行图像处理,实现自动化批阅和评分,提高教育领域的效率。系统包括图像预处理、图像二值化、图像平移旋转矫正、填涂信息识别等步骤,适用于多种考试场景,减少教师工作负担,提升批阅准确性和效率。核心代码包括图像处理和识别算法,如excel.py、get_answer.py、iidd.py、MainUI.py、ooctest.py和ui.py,实现了答题卡识别、成绩分析和图形用户界面等功能。
摘要由CSDN通过智能技术生成

1.研究背景与意义

项目参考AAAI Association for the Advancement of Artificial Intelligence

研究背景与意义:

随着科技的不断发展,计算机视觉技术在各个领域中的应用越来越广泛。其中,基于计算机视觉的答题卡识别系统在教育领域中具有重要的意义。传统的答题卡批阅方式需要大量的人力和时间,容易出现错误和漏批的情况。而基于计算机视觉的答题卡识别系统可以实现自动化、高效率的批阅,大大提高了批阅的准确性和效率。

在教育领域中,答题卡是一种常见的考试方式。学生通过在答题卡上选择选项来回答问题,然后教师需要将答题卡进行批阅和评分。传统的批阅方式需要教师手动逐一检查每个答题卡,容易出现疲劳和错误。而基于计算机视觉的答题卡识别系统可以通过图像处理和模式识别的技术,自动识别和分析答题卡上的选项,实现自动化的批阅和评分。这不仅可以减轻教师的工作负担,还可以提高批阅的准确性和效率。

另外,基于计算机视觉的答题卡识别系统还可以应用于各种考试场景,包括学校的期中考试、期末考试、高考、托福、雅思等。这些考试通常涉及大量的答题卡,传统的批阅方式需要大量的人力和时间,容易出现错误和漏批的情况。而基于计算机视觉的答题卡识别系统可以实现自动化、高效率的批阅,大大提高了批阅的准确性和效率。同时,该系统还可以提供详细的统计数据和分析报告,帮助教师和学校更好地了解学生的学习情况和能力水平。

此外,基于计算机视觉的答题卡识别系统还可以应用于各种研究领域,包括心理学、教育评估、社会调查等。通过分析答题卡上的选项,可以得到大量的数据和信息,用于研究学生的学习行为、学习能力、心理状态等。这些数据和信息可以帮助研究者更好地了解学生的学习情况和发展趋势,为教育改革和教学改进提供科学依据。

综上所述,基于计算机视觉的答题卡识别系统在教育领域中具有重要的意义。它可以实现自动化、高效率的批阅,减轻教师的工作负担,提高批阅的准确性和效率。同时,它还可以应用于各种考试场景和研究领域,为教育改革和教学改进提供科学依据。因此,研究和开发基于计算机视觉的答题卡识别系统具有重要的实际应用价值和研究意义。

2.图片演示

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3.视频演示

基于计算机视觉OpenCV的答题卡识别系统_哔哩哔哩_bilibili

4.系统流程图

系统实现的整体流程分为用户登录阶段、录入标准答案阶段、答卷识别阶段、成绩分析阶段这四个阶段,主要对以下三个阶段进行介绍。
(1)录入标准答案阶段
首先,教师将标准答题卡用普通打印机打印出来,进行标准答案的填涂;然后通过硬件设备对该答题卡图像进行采集;其次,录入单选多选的题目数和每题分值,并且对采集后的图像进行图像预处理和图像识别;最后,将每一题的识别结果、每题的分值、题号一起存到数据库(本地txt)中。
(2)答卷识别阶段
在答题卡识别阶段,对待阅答题卡进行图像的采集、图像的预处理、图像的识别等处理步骤,在这个过程中,本文将待阅答题卡图像进行缩放,使其和答题卡标准模板图像大小相同,以保证最好的图像识别效果。识别后,得出了该考生的答题卡填涂的识别结果,将该考生的答题卡填涂的识别结果与数据库中的标准答案进行一一对比,然后将该考生的总得分、每题的得分、填涂结果输出并且存入数据库中。
(3)成绩分析阶段
在成绩分析阶段,主要是针对识别后的学生成绩进行得分统计和错题数的分析,从而了解到学生的知识的掌握程度,对教师进行后续的教学起指导和借鉴作用。
综上所述,系统实现流程如图所示。
在这里插入图片描述

5.核心代码讲解

5.1 excel.py

封装为类的代码如下:



class ExcelWriter:
    def __init__(self, data):
        self.data = data

    def set_style(self, name, height, bold=False):
        # 初始化样式
        style = xlwt.XFStyle()
        # 创建字体
        font = xlwt.Font()
        font.bold = bold
        font.colour_index = 4
        font.height = height
        font.name = name
        style.font = font
        return style

    def write_excel(self):
        if os.path.exists("data.xls"):
            os.remove("data.xls")
        book = xlwt.Workbook(encoding='utf-8')  # 创建Workbook,相当于创建Excel
        # 创建sheet,Sheet1为表的名字,cell_overwrite_ok为是否覆盖单元格
        sheet1 = book.add_sheet(u'Sheet1', cell_overwrite_ok=True)
        r = 0
        for i, j in self.data.items():  # i表示data中的key,j表示data中的value
            le = len(j)  # values返回的列表长度
            if r == 0:
                sheet1.write(r, 0, i, self.set_style("Time New Roman", 220, True))  # 添加第 0 行 0 列数据单元格背景设为黄色
            else:
                sheet1.write(r, 0, i, self.set_style("Time New Roman", 220, True))  # 添加第 1 列的数据

            for c in range(1, le + 1):  # values列表中索引
                if r == 0:
                    sheet1.write(r, c, j[c - 1], self.set_style("Time New Roman", 220, True))  # 添加第 0 行,2 列到第 5 列的数据单元格背景设为黄色
                else:
                    sheet1.write(r, c, j[c - 1], self.set_style("Time New Roman", 220, True))

            r += 1  # 行数

        # sheet_merge() 合并单元格

        book.save('data.xls')
        print("已导出至:data.xls")

使用方法:

data1 = {
    "序号": ["姓名", "语文", "数学", "英语"],
    "1": ["张三", 130, 120, 100],
    "2": ["李四", 100, 110, 120],
    "3": ["王五", 125, 135, 135]
}

data2 = {'序号': ['学院', '班级', '学号',  '成绩', '错题'],
          1: [21, 21, 160, 82,
        "{35: 'C', 36: 'B', 37: 'C', 38: 'D', 39: 'D', 46: 'B', 47: 'C', 48: 'D', 49: 'B'}"],
          2: [21, 21, 159, 90, "{46: 'B', 47: 'C', 48: 'D', 49: 'A', 50: 'B'}"]}

excel_writer = ExcelWriter(data1)
excel_writer.write_excel()

该程序文件名为excel.py,主要功能是使用xlwt库将数据写入Excel文件。程序中定义了一个set_style函数,用于设置单元格样式。然后定义了两个数据字典data1和data2,分别表示两个表格的数据。接下来定义了一个write_excel函数,用于将数据写入Excel文件。函数首先判断是否存在名为"data.xls"的文件,如果存在则删除。然后创建一个Workbook对象book,并创建一个名为"Sheet1"的sheet对象sheet1。接着使用循环遍历数据字典,将数据写入Excel文件中。最后调用save方法保存Excel文件,并打印导出成功的提示信息。

5.2 get_answer.py


class AnswerSheetScanner:
    def __init__(self, image_path):
        self.image_path = image_path
        self.ANSWER_KEY = {0: 1, 1: 4, 2: 0, 3: 3, 4: 1}

    def order_points(self, 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

    def four_point_transform(self, image, pts):
        rect = self.order_points(pts)
        (tl, tr, br, bl) = rect
        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")
        M = cv2.getPerspectiveTransform(rect, dst)
        warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))
        return warped

    def sort_contours(self, 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

    def scan(self):
        image = cv2.imread(self.image_path)
        contours_img = image.copy()
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        blurred = cv2.GaussianBlur(gray, (5, 5), 0)
        edged = cv2.Canny(blurred, 75, 200)
        cnts = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL,
                                cv2.CHAIN_APPROX_SIMPLE)[1]
        docCnt = None
        if len(cnts) > 0:
            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
        warped = self.four_point_transform(gray, docCnt.reshape(4, 2))
        thresh = cv2.threshold(warped, 0, 255,
                               cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
        thresh_Contours = thresh.copy()
        cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
                                cv2.CHAIN_APPROX_SIMPLE)[1]
        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 = self.sort_contours(questionCnts,
                                          method="top-to-bottom")[0]
        correct = 0
        for (q, i) in enumerate(np.arange(0, len(questionCnts), 5)):
            cnts = self.sort_contours(questionCnts[i:i + 5])[0]
            bubbled = None
            for (j, c) in enumerate(cnts):
                mask = np.zeros(thresh.shape, dtype="uint8")
                cv2.drawContours(mask, [c], -1, 255, -1)
                mask = cv2.bitwise_and(thresh, thresh, mask=mask)
                total = cv2.countNonZero(mask)
                if bubbled is None or total > bubbled[0]:
                    bubbled = (total, j)
            color = (0, 0, 255)
            k = self.ANSWER_KEY[q]
            if k == bubbled[1]:
                color = (0, 255, 0)
                correct += 1
            cv2.drawContours(warped, [cnts[k]], -1, color, 3)
        score = (correct / 5.0) * 100
      ......

这个程序文件名为get_answer.py,它的功能是读取一张图片,进行图像处理和透视变换,然后根据图片中的选择题答案和正确答案进行对比,计算得分并显示在图片上。

程序首先导入所需的工具包,并设置了一个参数解析器,用于接收输入图片的路径。

接下来定义了一个字典ANSWER_KEY,用于存储正确答案。

然后定义了两个函数,order_points用于按顺序找到坐标点,four_point_transform用于执行透视变换。

接下来定义了一个辅助函数sort_contours,用于对轮廓进行排序,以及一个辅助函数cv_show,用于显示图像。

然后进行预处理,包括读取图片、灰度化、高斯滤波、边缘检测等。

接下来进行轮廓检测,找到最大的轮廓并进行近似处理,得到透视变换的坐标点。

然后执行透视变换,得到变换后的图像。

接下来进行阈值处理,找到每个圆圈轮廓。

然后遍历每一排的选项,对每个选项进行处理,通过计算非零点数量来判断是否选择了该答案。

最后根据正确答案和选择的答案进行对比,计算得分,并在图像上显示得分。

最后显示原始图片和处理后的图片,并等待按键退出程序。

5.3 iidd.py


class ExamPaper:
    def __init__(self, image_name):
        self.image_name = image_name
        self.true_ans = {1:"A",  2:'B',  3:'C',  4:'D',  5:'A',
                         6:"A", 7:'A', 8:'C', 9:'D', 10:'A',
                         11:"B", 12:'C', 13:'D', 14:'A', 15:'B',
                         16:"B", 17:'C',18:'D', 19:'A', 20:'B',
                         21:"C", 22:'D', 23:'A', 24:'B', 25:'C',
                         26:"C", 27:'D', 28:'A', 29:'B', 30:'C',
                         31:"D", 32:'A', 33:'B', 34:'C', 35:'D',
                         36:"D", 37:'A', 38:'B', 39:'C', 40:'D',
                         41:"A", 42:'B', 43:'C', 44:'D', 45:'A',
                         46:"A", 47:'B', 48:'C', 49 :'C', 50:'C' }

    def order_points(self, 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

    def toushi_transform(self, image, pts):
        rect = self.order_points(pts)
        (tl, tr, br, bl) = rect
        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))
        ......

该程序文件名为iidd.py,主要功能是通过图像处理识别答题卡上的选择题答案和学生信息。程序包含以下几个函数:

  1. order_points(pts):根据输入的四个坐标点,按照左上、右上、右下、左下的顺序返回四个点的坐标。

  2. toushi_transform(image, pts):根据输入的图像和四个坐标点,进行透视变换,将图像中的答题卡部分变换为标准大小。

  3. judge(x, y):根据输入的坐标点,判断其所在的题目位置。

  4. judgey0(y):根据输入的y坐标,判断其所在的题目行数。

  5. judgex0(x):根据输入的x坐标,判断其所在的题目列数。

  6. judge0(x, y):根据输入的坐标点,判断其所在的题目位置,并返回行数和列数。

  7. cv_show(name, img):显示图像。

  8. get_postion(image_name):根据输入的图像文件名,读取图像并进行图像处理,找到答题卡的位置和题目的位置。

  9. explain(Info):根据输入的题目位置信息,解析出学生的学号、班级和学号。

  10. calculate(Answer):根据输入的答案位置信息,计算出学生的答案。

  11. sumall(image_name, true_ans):根据输入的图像文件名和正确答案,调用上述函数,计算出学生的得分和错误答案。

该程序主要用于自动批改选择题答题卡,并提取学生的学号、班级和学号信息。

5.4 MainUI.py



class CardRecognitionSystem:
    def __init__(self):
        self.data = {
            "序号": ["学院", "班级", "学号", "成绩", "错题"]
        }
        self.cnt = 0
        self.true_ans = {
            1: "A", 2: 'B', 3: 'C', 4: 'D', 5: 'A',
            6: "A", 7: 'A', 8: 'C', 9: 'D', 10: 'A',
            11: "B", 12: 'C', 13: 'D', 14: 'A', 15: 'B',
            16: "B", 17: 'C', 18: 'D', 19: 'A', 20: 'B',
            21: "C", 22: 'D', 23: 'A', 24: 'B', 25: 'C',
            26: "C", 27: 'D', 28: 'A', 29: 'B', 30: 'C',
            31: "D", 32: 'A', 33: 'B', 34: 'C', 35: 'D',
            36: "D", 37: 'A', 38: 'B', 39: 'C', 40: 'D',
            41: "A", 42: 'B', 43: 'C', 44: 'D', 45: 'A',
            46: "A", 47: 'B', 48: 'C', 49: 'C', 50: 'C'
        }
        self.window = tk.Tk()
        self.window.title('答题卡识别系统')
        self.window.geometry('550x400')
        self.menubar = tk.Menu(self.window)
        self.filemenu = tk.Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label='设置', menu=self.filemenu)
        self.filemenu.add_cascade(label='初始化试卷', command=self.set_ans)
        self.l_score = tk.Label(self.window, text='成绩:')
        self.l_score.place(x=250, y=350)
        self.l_info1 = tk.Label(self.window, text='学院:           班级:          ')
        self.l_info1.place(x=250, y=250)
        self.l_info2 = tk.Label(self.window, text='姓名:           学号:          ')
        self.l_info2.place(x=250, y=300)
        self.l_info3 = tk.Label(self.window, text='错题:    ')
        self.l_info3.place(x=330, y=350)
        self.var_img_name = tk.StringVar()
        self.var_img_name.set("new2.jpg")
        self.l_en = tk.Label(self.window, text='输出图片地址')
        self.l_en.place(x=25, y=225)
        self.entry_img_name = tk.Entry(self.window, textvariable=self.var_img_name)
        self.entry_img_name.place(x=30, y=250)
        self.btn_test = tk.Button(self.window, text='识别', command=self.discern)
        self.btn_test.place(x=40, y=300)
        self.btn_excel = tk.Button(self.window, text='导出', command=self.write)
        self.btn_excel.place(x=40, y=350)
        self.imLabel = tk.Label(self.window, text='结果:')
        self.imLabel.place(x=270, y=20)
        self.tell = tk.Label(self.window, text='欢迎进入机读卡识别系统')
        self.tell.place(x=30, y=70)
        self.window.config(menu=self.menubar)

    def walk(self, path):
        files = []
        if not os.path.exists(path):
            return -1
        for root, dirs, names in os.walk(path):
            for filename in names:
                file = os.path.join(root, filename)
                print(file)  # 路径和文件名连接构成完整路径
                files.append(file)
        return files

    ......

该程序文件名为MainUI.py,主要功能是实现一个答题卡识别系统的图形用户界面。程序使用了tkinter库来创建GUI界面,使用了filedialog库来选择文件和文件夹,使用了PIL库来处理图片,使用了cv2库来进行图像处理,使用了excel库来写入Excel文件。

程序的主要功能包括:

  1. 导入所需的库文件。
  2. 定义了一个数据字典data,用于存储识别结果。
  3. 定义了一个函数walk,用于遍历指定文件夹下的所有文件。
  4. 定义了一个函数discern,用于识别答题卡图片,并将识别结果展示在界面上。
  5. 定义了一个函数set_ans,用于设置正确答案。
  6. 定义了一个函数write,用于将识别结果写入Excel文件。
  7. 创建了一个窗口,并设置了窗口的标题和大小。
  8. 创建了一个菜单栏,并添加了一个菜单项"初始化试卷",点击该菜单项会弹出一个新窗口,用于设置正确答案。
  9. 创建了一些标签和输入框,用于显示和输入相关信息。
  10. 创建了一些按钮,用于触发相应的功能。
  11. 最后,通过调用window.mainloop()来启动窗口的事件循环。

总体来说,该程序实现了一个简单的答题卡识别系统的图形用户界面,用户可以选择单个文件或批量处理文件夹中的答题卡图片,并将识别结果展示在界面上,还可以设置正确答案并将识别结果导出到Excel文件中。

5.5 ooctest.py


class OCR:
    def __init__(self, access_token):
        self.access_token = access_token
        self.headers = {'content-type': 'application/x-www-form-urlencoded'}
        self.base_url = "https://aip.baidubce.com/rest/2.0/ocr/v1/"
    
    def get_text(self, image_path):
        request_url = self.base_url + "handwriting" + "?access_token=" + self.access_token
        image = open(image_path, 'rb')
        image_data = base64.b64encode(image.read())
        params = {"image": image_data}
        response = requests.post(request_url, data=params, headers=self.headers)
        if response:
            return response.json()['words_result'][0]['words']
        else:
            return None
    
    def get_number(self, image_path):
        request_url = self.base_url + "numbers" + "?access_token=" + self.access_token
        image = open(image_path, 'rb')
        image_data = base64.b64encode(image.read())
        params = {"image": image_data}
        response = requests.post(request_url, data=params, headers=self.headers)
        if response:
            return response.json()['words_result'][0]['words']
        else:
            return None


这个程序文件名为ooctest.py,它的功能是通过百度OCR接口识别手写文字。程序首先导入了requests和base64两个模块。然后定义了一个名为getinfo的函数。

在getinfo函数中,首先设置了请求的URL为百度OCR接口的手写文字识别API。然后使用二进制方式打开了两张图片文件,分别是school.jpg和name.jpg,并将它们转换成base64编码的字符串。接下来,将图片的base64编码作为参数,构造了两个请求参数params_s和params_n。

接着,定义了一个access_token变量,该变量是百度OCR接口的访问令牌。然后将access_token拼接到请求URL中。设置了请求头部的content-type为application/x-www-form-urlencoded。

然后,使用requests.post方法发送了两个POST请求,分别是对school_img和name_img进行手写文字识别。将识别结果打印出来,并将结果存入一个名为dict的字典中。

接下来,程序进行了数字识别。同样使用二进制方式打开了两张图片文件,分别是cls.jpg和id.jpg,并将它们转换成base64编码的字符串。构造了两个请求参数params_c和params_i。

然后,定义了一个名为ru的变量,该变量是百度OCR接口的数字识别API的请求URL。将access_token拼接到ru中。

再次使用requests.post方法发送了两个POST请求,分别是对cls_img和id_img进行数字识别。将识别结果打印出来,并将结果存入dict字典中。

最后,函数返回了dict字典。

5.6 ui.py

class SetAnsDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle('初始化试卷')
        self.setGeometry(100, 100, 350, 200)

        self.initUI()

    def initUI(self):
        layout = QVBoxLayout()

        self.new_ans = QLineEdit(self)
        self.new_ans.setText(str(self.parent().true_ans))
        layout.addWidget(self.new_ans)

        btn_confirm = QPushButton('确定', self)
        btn_confirm.clicked.connect(self.confirm)
        layout.addWidget(btn_confirm)

        self.setLayout(layout)

    def confirm(self):
        ans_str = self.new_ans.text()
        self.parent().true_ans = eval(ans_str)
        self.close()

......


这个程序文件是一个使用PyQt5编写的答题卡识别系统。程序主要包括以下几个部分:

  1. 导入必要的模块和库,包括sys、os、PyQt5、PIL、cv2等。
  2. 定义了一个全局变量data,用于存储识别结果。
  3. 定义了一个函数walk,用于遍历指定路径下的所有文件。
  4. 定义了一个SetAnsDialog类,用于初始化试卷答案。
  5. 定义了一个App类,继承自QMainWindow,用于创建主窗口。
  6. 在App类的initUI方法中,创建了主窗口的布局和控件,并设置了背景图片。
  7. 在App类中定义了一些按钮的点击事件,包括识别按钮、导出按钮、初始化试卷按钮、选择文件按钮和批量处理按钮。
  8. 在App类中定义了识别方法discern,用于识别答题卡图片并将结果显示在表格中。
  9. 在App类中定义了选择文件方法choose_file,用于选择要识别的答题卡图片。
  10. 在App类中定义了批量处理方法getall,用于批量处理指定文件夹下的答题卡图片。
  11. 在App类中定义了导出方法write,用于将识别结果写入Excel文件。
  12. 在主程序中创建了一个QApplication对象和一个App对象,并启动了主程序。

总体来说,这个程序是一个简单的答题卡识别系统,可以识别单张答题卡图片或批量处理答题卡图片,并将识别结果显示在表格中,并可以导出到Excel文件中。

6.系统整体结构

整体功能概述:
该答题卡识别系统的整体功能是通过图像处理和OCR技术,实现对答题卡图片的识别和批改,并将识别结果展示在图形用户界面上。系统可以单张识别和批量处理答题卡图片,支持设置正确答案并导出识别结果到Excel文件。

文件功能整理:

文件名功能概述
excel.py将数据写入Excel文件的功能
get_answer.py识别答题卡图片并计算得分的功能
iidd.py通过百度OCR接口识别手写文字和数字的功能
MainUI.py答题卡识别系统的图形用户界面的功能
ooctest.py通过百度OCR接口识别手写文字和数字的功能
ui.py答题卡识别系统的图形用户界面的功能

7.图像的预处理

概述

图像预处理是指在图像分析中,对采集的图像识别之前所进行的一系列处理。由于采集图像环境的不同,例如,光照的明暗、摄像头性能不一、摄像的角度、纸张的薄厚、光源的颜色和打光方式、图像的倾斜等多种环境因素的影响。
因此,为了尽可能减少这些环境因素对图像识别结果的影响,去除一些无关信息,所以,必须对图像进行预处理。其目的是利用计算机信息技术尽可能去掉图像中的一些非紧要信息,恢复并且使紧要信息的可检测性增强,从而提升图像识别的准确度。

图像灰度化

目前,摄像头采集到的答题卡图像是彩色的。其每个像素都是由红®、绿(G)、蓝(B)三个分量组成的,像素点的取值范围为0~16581375l18]。由于存一幅彩色图像需要很大的存储空间,而且增加后续的图像处理的计算量。另一方面,灰度图像和彩色图像可用来反映图像的色度和亮度的特征是一样的。所以,必须对图像进行灰度化处理。完全可以用灰度图像代替彩色图像。
图像灰度化处理就是将待阅答题卡图像转换成灰度图像的过程,在图像处理进程中,这样可以减少的计算量、图像的复杂程度和信息处理速度。常用的图像灰度化的方法有以下几种方式:
(1)单分量法
取灰度图像的灰度值为待阅答题卡图像R、G、B三个分量中的任意一个分量的值,见式。
在这里插入图片描述

(2)平均值法
在待阅答题卡图像中,取某像素点的R、G、B三个分量的值,求这三个值的平均值,即图像的灰度值,见式。
在这里插入图片描述

(3)最大值法
在待阅答题卡图像中,取某像素点的R、G、B三个分量中的最大值,即图像的灰度值,见式。
在这里插入图片描述

(4)加权平均法
加权平均法是根据重要性以及其它指标,将R、G、B三个分量以不相同的权重进行加权平均。根据转换公式把RGB彩色模型转换成YUV彩色模型,可得到各个分量的权重。其中,YUv,即一种颜色编码方法。Y是明亮度:V、U是色度,是影像色彩及饱和度的描述,有确定像素颜色的用途[1]。
人眼对绿色的敏感最高,对蓝色敏感最低,要想得到最符合人眼视觉效果的图像,就得取R、G、B的权重分别约为0.3、0.59、0.11时,这样灰度化后的结果真实合理、符合实际的应用。见式。
在这里插入图片描述

在灰度化处理过程中,由于目前最常用的方法就是加权平均法。因此,本系统采用加权平均法来实现对答题卡图像的灰度化处理效果如图。
在这里插入图片描述

8.图像二值化

在答题卡图像处理过程中,由于考生在填涂答题卡时,填涂的面积和填涂的深浅等都存在一定的差异。因此,必须对灰度化后的答题卡图像进行阈值化处理.
图像的二值化处理就是将灰度图像转换成只有黑白两种取值的图像。先给定一个灰度阈值T,图像中某像素点的灰度值设为f(x, y),当f(x,y)>T时,则令f(x, y)=255:当f(x,y)<=T时,则令f(x,y)=0。这样就得到了只有黑白两色的灰度图像,大大减少了图像的数据量,并且简化了图像的处理过程。阀值化处理的关键在于设定阈值,选取适当的阈值可以尽量的减少图像的干扰信息,突出图像的主要信息,便于进行后续进行图像识别。
在图像处理过程中,选取阈值的方法有很多种,常见的有五种方法:简单阈值法、双峰法、大津法、最佳阈值法。
(1)简单阈值法
简单阈值法是指在图像阈值化处理过程中,通过人眼的识别,判断该图像中的像素点、并且分析该图像,设T是我们人工选择的全局阈值,图像的各个像素点的灰度值g(x,y)与T比较,若g(x,y) >T ,则令g(x. y)=255:若g(x, y)<=T,则令g(x,y)=0:这种阈值选择的方法简单,处理速度快:然而,其使用范围比较窄,结果容易受人为影响[20]。
(2〉双峰法
根据图像的灰度直方图可以用来实现双峰法。在一些不复杂图像中,其灰度分布很有规则,图像背景与前景像素灰度值的分布呈山峰形状,一个波谷由两个波峰形成。因此,确定阈值为两波峰之间波谷的灰度值。有两个波峰的图像适用这种方法[P]。不适用此方法的情况是直方图曲线平缓或只有一个波峰的图像。
(3)大津法
在二十世纪七十年代末,[日本学者大津提出了大津法],此方法可以自适应确定阀值。其原理是基于图像的灰度特征,整个图像分为前景和背景,对这两个部分的像素点总数和灰度平均值分别进行计算,这样就求得其类间方差,类间方差越大时,伴随着前景和背景的差别越大,因此,若类间方差最大时,即是图像的最佳阀值点,二值化效果最好。该方法的主要实现步骤为:
(A)在一个图像中,设w,是前景像素点,u.是前景图像的平均灰度;w是背景像素点,u,是背景图像的平均灰度;g是前景和背景图像的方差,u是图像的平均灰度:
(B)计算图像的平均灰度,见式:
在这里插入图片描述

(C)计算图像的方差g,见式:
在这里插入图片描述

(D)在(3.6)式中把(3.5)式代入得,见式:
在这里插入图片描述

当g最大时,得到最佳阈值T。该方法的优点是算法简单,用时少,当前景与背景的面积相差不大时,能够有效的分割图像。但是,当图像的前景与背景的面积相差很大时,则不能够有效的使图像分割,即其直方图表现为双峰不明显或大小相差很大的双峰,此时分割效果不佳。基于图像的灰度特征的区分来使用的此方法,所以,对噪音和目标图像大小很敏感。因此,在实际应用中,将该方法与其他方法结合起来用。

(4)最佳阈值法(迭代法)

最佳阈值法又称迭代法,它选取阈值的核心是基于逼近的思想[P3l,首先选取灰度图的平均值作为初始阈值,然后进行阙值分割,产生子图像,并根据子图像的特征来选择新的阈值,这样几次循环,分割的图像的像素点可以最大程度减少错误。这样做的成效比用初始阈值直接分割图像好。以下是详细的实现过程:
(A)计算图像最小、最大灰度值分别为Zm和Z.,则初值阈值T=(Zmin+Z.)/2 :
(B)将图像分为目标和背景部分,根据新选取的阈值T,计算出两部分的平均灰度值Z。和Z,,从而得出新阈值T=(Z+Z)/2 :
©如果T=T,则此时的阈值就是最佳阈值:否则将T的值赋予T。,转向步骤(B)。
最佳阈值法可以准确地区分图像的目标和背景,但是细节处区分不细致,总体的效果还是比较好的。
本文经过实验对比了以上方法,发现采用最佳阈值法可以得到更好的答题卡图像二值化的效果如图。
在这里插入图片描述

9.图像平移旋转矫正

在答题卡图像采集过程中,由于答题卡图像放置的位置、支架的搭建、摄像头的固定等人为因素,以及摄像头的性能因素会造成一定角度的倾斜或者形变,采集到的图像如果有一定角度的倾斜或者形变,那么会对识别的结果造成一定的影响,进一步影响学生的成绩的准确性,考试的公正性,以及人才选拔的客观性。因此,在答题卡图像识别之前,判断答题卡图像是否倾斜并做相应平移旋转矫正处理,对后续答题卡识别具有十分重要的意义。
首先需要获取答题卡图像四个角的定位标识即正方形黑块,通常有以下三种方法:一是采用边缘检测或者轮廓区域检测的相关算法,利用答题卡图像边缘的突变性质或者边缘点连接的层次差别来检测并提取其边缘信息,然后对答题卡图像进行相应的矫正,这种方法只能处理没有发生透视情况的图像矫正,处理范围比较局限24。二是采用角点检测的方法,来获取图像的角点特征,然后进行相应的矫正处理,这种方法由于要遍历整个图像的每一个像素点,其需处理的数据量较大、效率较低。三是采用模板匹配的方法,提取答题卡图像四个角的定位标识,然后进行几何变换达到答题卡图像的矫正效果。这种方法可以弥补方法一和方法二的不足,更适用答题卡图像的识别。
根据答题卡图像的矫正需求的分析,对以上几种方法进行分析实验,本系统采用第三种方法即模板匹配的方法,获取答题卡四个角的定位标识,然后根据各个角的定位信息对答题卡图像进行几何变换,来实现对答题卡图像的平移旋转矫正。

模板匹配

模板匹配被认为是在图像识别中的一种最基本的模式识别方法。有以下三种常用的方法:
(1)基于特征的模板匹配算法[5]。其基本思想是提取图像的特征,以生成的特征描述子的相似度为标准,匹配这两幅图像的特征。其特征主要可以分为点、边缘、区域或面等特征。经实验验证,此算法最大的缺陷在于需要在图像中提取出特征,需要比其它两种算法多出一定的计算量和时间,不适用于本系统。
(2)基于形状的模板匹配算法[6],其最佳匹配位置在模板图像内,由像素的梯度向量内积总和,以及最小值决定,其优点就是稳定性和可靠性都比较优越,但是运算量相比较而言较大,处理时间也较长,不适用于本系统,为了提高答题卡识别的效率,需要找到更快速更高效的方法。
(3)基于灰度值的模板匹配算法[7],即相似度是通过计算模板图像与图像之间灰度值来计算的,若以图像的形式表示模板图像块时,根据模板图像块与图像中各个部分的相似度,去判断该模板图像块是否存在于这幅图像中,并求其位置的操作。由于在答题卡设计的时候,制定的答题卡的四个角的黑色方块比较显著,并且进行模板匹配的图像是二值图像,因此答题卡的定位信息的特征变得很明显很简单。经过试验,发现基于灰度值的模板匹配算法配合答题卡图像的定位信息的特征,能够取得相当不错的效果。
在本系统中,答题卡图像四个角的定位标识显著地分布在答题卡的四个边角位置,所以,为了提高模板匹配的效率,先将答题卡图像分为四个部分,分别是左上部分、右上部分、左下部分、右下部分;其次定义一个模板图像块;然后将模板图像块分别在这四个部分的图像上,从上至下,从左至右以像素点为单位进行滑动,将两个图像的像素值进行对比,选择相似度最高的部分标记当遇到相似度更高的部分时,更换标记部分;最后,扫描完毕,将四个部分相似度最高的部分标记出来并进行输出操作。
基于灰度值的模板匹配算法的整个具体流程:
(1)设答题卡图像目标函数为﹐答题卡图像大小为24803508像素,把答题卡图像分成四个部分:①左上部分、②右上部分、③左下部分、④右下部分,每个部分大小为12401754像素如图。
在这里插入图片描述

10.几何变换

图像的几何变换是指一个图像的新坐标位置由另一个图像的坐标位置映射而来的。其特点就是使图像像素的空间位置改变,而不是对其像素的灰度值改变,这可以消除图像采集时出现的几何形变。它是图像处理与分析的基础28]。设标准图像函数f(x,y),实际图像函数g(x,y),把原图像素点(x,y%)的坐标与目标图像新像素点(x ‘, y’)的映射关系可表示为见式:
在这里插入图片描述

其中,f(x。,y,)和g(x,y%)是坐标变换函数,利用标准图像和实际图像中的已知的对应像素点来推算出图像间的几何变换。
考虑到图像可能会在采集时,产生形变或者倾斜。因此,答题卡图像的平移旋转矫正处理必须进行。几何变换主要有坐标映射、仿射变换等。
(1)坐标映射
图像的坐标映射是建立一种映射关系在原图像与目标图像之间,这种映射关系有两种,一种是计算变换后图像像素点反映射在原图像的坐标位置,另一种是计算原图像像素点在映射后图像的坐标位置[2%]。由于这种映射关系并不能达到矫正答题卡图像的效果,所以不适用于本系统的答题卡图像矫正。
(2)透视变换
透视变换就是二维坐标到三维坐标再到另一个二维坐标空间的映射。透视变换过程就是已知标准答题卡和待阅答题卡角定位点坐标,通过透视变换见式,计算出透视变换矩阵,根据映射关系可以得出透视变换后的图像的坐标点,这样就实现对答题卡图像的矫正。
在这里插入图片描述

在公式中,(x ‘, y’,w’)表示原图像,(u, v, w)表示待阅答题卡图像,用于图像透视变换的实现,[as,as]表示平移图像,透视变换矩阵即相对于仿射变换,透视变换具有很高的灵活性。但是由于它是三维坐标之间的变换,故不适合本系统。
(3)仿射变换
图像的仿射变换是空间直角坐标系中二维坐标之间的线性变换[30l。它主要实现平移、缩放、旋转等相关几何操作,操作之后,依然保持其“平直性”和“平行性”的特点。
在保证摄像头的光轴要与答题卡所在平面垂直、光照、摄像头的性能等环境因素达到最佳的情况下,通过摄像头采集图像时,图像绝大部分程度上都有发生一定的平移旋转,这属于平面上的平移旋转,而仿射变换正好是发生在平面内的二维坐标之间的变换,即可以这样理解,图像仿射变换的过程基本就是图像平移旋转矫正的过程;因此,采用仿射变换实现对答题卡图像的平移旋转矫正,更适用于本系统。
要实现答题卡图像的平移旋转矫正,必须先提取答题卡模板图像和待阅答题卡图像四个角定位点坐标信息,这可以通过模板匹配获取;由于答题卡模板图像的每个角的定位点坐标和实际答题卡图像的每个角的定位点坐标一一对应,并且三个点就可确定一个面,所以可以只需要三对角定位点坐标,根据三对角定位点坐标之间的映射关系来实现答题卡图像的矫正。
在这里插入图片描述

11.填涂信息识别

通过对答题卡图像的涂点定位,获取并标记了每个矩形定位点信息的左上角坐标,接下来就可对每个矩形定位点进行填涂信息识别。填涂信息识别就是将考生的准考证号或学号和填涂的答案进行标记并且输出的过程。常用的填涂信息识别算法有以下4种:
(1)基于支持向量机的填涂信息识别
基于支持向量机的填涂信息识别其基本步骤如下,第一,对各个矩形定位信息点与水平定位信息的相对坐标模板进行定义;第二,对水平定位信息区域进行图像分割;第三,提取各个水平定位信息点重心;第四,根据各个矩形定位信息点与水平定位信息的相对坐标模板,提取各个矩形定位信息点初步识别的范围;第五,向量集根据各个矩形定位信息点最初识别的范围及定义的环境因子来构造;第六,输入向量机,采用支持向量机对其进行训练与识别,获得结果。
(2)基于决策树的填涂信息识别
基于决策树的填涂信息识别其基本步骤是:首先采用人工标注的形式从答题卡中抽取矩形定位信息点构建训练集和测试集,在训练集和测试集中,分别有未填涂与填涂矩形定位信息点两类,其次,设定其离散化阈值T,把训练集中答题卡的矩形定位信息点分割成m个大小相同的小矩形,计算所有小矩形的占空比,根据T将其离散成信息特征,然后,将m+1个特征构成特征向量的结构,来构建矩形定位信息点的决策树,最后,使用测试集测试决策树的速度和准确度[1]。
(3)基于模板匹配的填涂信息识别
模板匹配是指将答题卡的矩形定位信息点作为模板图像,将一个题中的一组选项作为实际图像,在实际图像中,从上至下,从左至右以像素点为单位进行滑动,将两幅图像的像素值进行对比,选择相似度最高的部分标记并输出,根据匹配的量化结果得出矩形定位信息点是否被填涂。
(4)基于平均灰度值的填涂信息识别
基于平均灰度值的填涂信息识别的基本思想是通过计算矩形定位信息点的像素的平均灰度值来判断该点是否被填涂。如果一道题目中的某个矩形定位信息点的平均灰度值最大,则表示该点被填涂。因此,设sum为矩形定位信息点的像素点总数,A[i,j]表示坐标[i,j]处的像素点的灰度值,则计算每一道题的矩形定位信息点的像素的平均灰度值 erog。见式:
在这里插入图片描述

12.系统整合

下图完整源码&环境部署视频教程&自定义UI界面

在这里插入图片描述

参考博客《基于计算机视觉OpenCV的答题卡识别系统》

13.参考文献


[1]顾梅花,苏彬彬,王苗苗,等.彩色图像灰度化算法综述[J].计算机应用研究.2019,(5).DOI:10.19734/j.issn.1001-3695.2018.01.0125 .

[2]雷俊杰,张伟池,余荣,等.一种基于决策树的选项识别方法[J].无线互联科技.2018,(1).DOI:10.3969/j.issn.1672-6944.2018.01.047 .

[3]曹仰杰,贾丽丽,陈永霞,等.生成式对抗网络及其计算机视觉应用研究综述[J].中国图象图形学报.2018,(10).

[4]刘方园,王水花,张煜东.支持向量机模型与应用综述[J].计算机系统应用.2018,(4).DOI:10.15888/j.cnki.csa.006273 .

[5]李越.OpenCV应用现状综述[J].工业控制计算机.2017,(7).

[6]文德仲.数字图像处理技术综述[J].科技资讯.2016,(8).DOI:10.16661/j.cnki.1672-3791.2016.08.022 .

[7]李仕伟,周坤,刘新蕊,等.MySQL数据库优化技术[J].信息与电脑.2016,(12).DOI:10.3969/j.issn.1003-9767.2016.12.088 .

[8]张功国.角点检测技术综述[J].数字技术与应用.2013,(4).

[9]谢勰,王辉,张雪锋.图像阈值分割技术中的部分和算法综述[J].西安邮电学院学报.2011,(3).DOI:10.3969/j.issn.1007-3264.2011.03.001 .

[10]翁功平.光标阅读机OMR原理的设计与实现[J].工业控制计算机.2010,(4).DOI:10.3969/j.issn.1001-182X.2010.04.030 .

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值