
简介这是一份面向Python毕业设计/课程设计的答题卡识别判分系统完整源码包适合计算机视觉、图像处理方向学生参考。系统基于OpenCV实现答题卡图像预处理、选择题选项识别、答案比对与成绩计算并将结果写入MySQL数据库及Excel文件覆盖了从图像输入到成绩导出的完整流程。压缩包共293个文件体积15.36MB核心为30个py源码文件辅以HTML/CSS/JS前端页面、SQL数据库脚本、pyc编译文件及项目说明文档便于直接运行与二次开发。项目采用模块化设计环境基于Python 3.6.8与PyCharm已集成前端展示与后端逻辑适合学习Canny边缘检测、轮廓提取、模板匹配等OpenCV核心操作。目前已有92人学习下载对于需要快速搭建完整课设项目并理解计算机视觉应用流程的读者极具参考价值。1. 答题卡识别与判分这套Python源码的价值和边界答题卡识别并不是一个黑匣子项目。完整流程拆开看只有四步图像预处理、选项区域定位、答案比对、成绩落库真正属于自己的核心逻辑代码可能还不到200行难点全在预处理参数和判分边界上。这套基于计算机视觉的Python毕业设计源码用OpenCV把“答题卡照片”一路处理成“Excel成绩单”还带了MySQL 5.7存储和基于bootstrap.css、layui.css的管理界面对正在做毕业设计、课程设计或者想低成本搭一个轻量级阅卷工具的人都是一个值得完整复现的样本。我把它从环境到代码到坑位逐层拆开读完你大概就知道该不该下以及下回来第一步该干什么。2. 识别流程拆解图像预处理、选项定位与答案比对的完整闭环2.1 模块化程序设计把识别任务拆成五个可维护环节我拿到这种项目有一个习惯先不急着跑而是把源码的文件结构扫一遍确认模块边界。这套源码的模块化做得比较规矩按职责分成五个环节图像读取与缩放、预处理、选项提取、答案比对、成绩存储。每个环节是一个独立函数或独立文件输入输出都是标准数据结构——numpy数组、字典、列表调试时可以单独跑任意一环不会牵一发动全身。主控逻辑大致长这样def main(): # 步骤1读取答题卡图片统一尺寸 img load_image(sheet_001.jpg) # 步骤2预处理返回二值化图像和答题卡外轮廓 binary, contours preprocess(img) # 步骤3定位并提取所有选项区域返回填涂状态字典 options extract_options(binary, contours) # 步骤4与标准答案比对逐题计分 score, detail compare(options, answer_key) # 步骤5写入Excel MySQL save_result(score, detail) return score每个函数职责很单一。preprocess只做图像层面的处理不关心业务逻辑extract_options只返回“题号 是否填涂”的字典compare拿字典和标准答案比对返回一个百分制分数和逐题明细。参数说明里有两点值得注意extract_options返回的是以(题号, 选项)为 key 的字典而不是列表这样后续比对时能直接按题号取学生涂写结果避免列表顺序错位带来的隐性bugcompare的answer_key也是字典题号和标准答案一一对应不在answer_key里的题不会参与计分。这种结构对课程设计非常友好。没有复杂框架没有难以追踪的全局变量每个环节都能单步调试。如果你要换成另一套答题卡模板只需要改extract_options里的行列坐标配置其余环节基本不用动。这也正是“模块化设计”在答辩时候的核心卖点。2.2 答题卡图像预处理灰度化、高斯滤波、二值化与边缘检测的参数选择预处理是整个系统里最影响识别率的一环。它的目标不是让图片变好看而是把“答题卡长什么样”这件事变成计算机能算的纯几何信息。具体做三件事灰度化、去噪、二值化顺序不能乱。灰度化把三通道BGR图压缩成单通道亮度图计算量直接降到原来的三分之一左右。去噪多用高斯模糊因为高斯滤波是按邻域加权平均能在保边缘细节的同时去掉传感器噪声。这个项目里用高斯滤波而不是均值滤波是合理的均值滤波会把边缘抹平后续轮廓检测容易丢失选项边界。二值化把图像变成黑白两色填涂区域和印刷底色彻底分离。def preprocess(image_path): # 读取并缩放成标准宽度统一尺寸后坐标网格才好计算 img cv2.imread(image_path) img cv2.resize(img, (1200, 1600), interpolationcv2.INTER_AREA) # 灰度化三通道变单通道亮度信息 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊去噪核大小(5,5)sigmaY自动按核计算 blurred cv2.GaussianBlur(gray, (5, 5), 0) # Otsu二值化阈值传0表示自动计算THRESH_BINARY_INV让填涂区域变白色 _, binary cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 取最外层轮廓RETR_EXTERNAL避免把选项内部的空心圆也带进来 contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) return binary, contours这里有两个细节要展开说。第一cv2.threshold返回两个值第一个是“实际使用的阈值”Otsu模式下由算法自动算出来代码里用_直接丢弃二值化结果binary才是真正要用的。INTER_AREA缩放适合含有固定图案的图片它能保留较清晰的边缘如果你换成长方形的答题卡宽度按 1200 固定、高度按比例缩放即可。第二cv2.findContours在 OpenCV 3 和 OpenCV 4 里的返回结构不同这里解包写成(contours, _)意味着你安装的必须是 OpenCV 4.x这个坑我会在第四章详细说。2.3 选项轮廓检测与答案比对从像素密度到分数的核心逻辑很多第一次接触答题卡识别的人会以为选项定位需要训练一个YOLO模型其实完全不用。标准答题卡是规则印刷的每个选项的位置理论上固定只要对答题卡做一次透视校正就能按行列坐标精确切出每一个选项区域。识别“学生涂了哪个选项”的核心手段是像素密度统计——把选项矩形从二值图里切出来看白色像素占比。def extract_options(binary, contours): # 从所有外轮廓里找面积最大的那个认为是答题卡本体 card_contour max(contours, keycv2.contourArea) # approxPolyDP把任意轮廓拟合为四边形得到答题卡四个顶点 epsilon 0.02 * cv2.arcLength(card_contour, True) approx cv2.approxPolyDP(card_contour, epsilon, True) # 坐标配置6行×每行5题每题A/B/C/D四个选项 rows 6 cols 5 options {} for idx in range(rows * cols): # 按行列均分得到第idx题的矩形区域 x, y, w, h get_option_rect(approx, idx, rows, cols) roi binary[int(y):int(y h), int(x):int(x w)] # 计算白色像素占比超过阈值认为已涂选 density np.sum(roi 0) / (w * h) if density 0.4: options[idx] 已涂选 return optionsget_option_rect是坐标网格生成函数核心思路是把答题卡的四个顶点围出的矩形横向均分成cols列、纵向均分成rows行第idx题的矩形就能通过行列索引反算出来。density 0.4是一个经验阈值低于它会把浅涂卡误判为未填高于它会把残留笔迹误判成选项。真正生产环境里很少用固定阈值我在第五章会给出更稳的“相对密度”方案。取到学生涂写的选项之后比对逻辑就很朴素与answer_key逐题比对答对加一分最后换算百分制。这一层代码简单到没有太多可说的但注意一个细节——比对时建议用student_answers.get(q_no, None)而不是直接索引这样没填涂的题在成绩明细里会显示为“未作答”而不是直接抛KeyError打断主流程。答题卡识别最怕在数据层崩溃这种防御式写法值得保留。3. 环境搭建与源码落地Python 3.6.8 OpenCV 4.x MySQL 5.7 的坑位对齐3.1 开发环境版本组合与依赖安装这套源码指定的是 Python 3.6.8 MySQL 5.7 Navicat 11 PyCharm。这套组合看起来“复古”其实是刻意选稳定的。Python 3.6.8 时代OpenCV 和 numpy 都有现成的预编译轮子下载即装不用碰源码编译MySQL 5.7 和 Navicat 11 的组合在课程设计环境里非常成熟网上能搜到的同类项目基本都是这套配置。依赖清单建议直接锁版本opencv-python4.5.4.60 numpy1.19.5 pymysql1.0.2 openpyxl3.0.10 pillow8.1.0pip install -r requirements.txt如果网络源慢换国内镜像源一条命令解决pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple版本锁死是有原因的。numpy1.19.5是最后一个完整支持 Python 3.6 的版本装更新的 numpy 会在导入时报错opencv-python4.5.4.60不仅保证findContours返回值是双元组而且这个版本对 Python 3.6 提供了完整 wheel 包。pymysql用它来连 MySQL轻量且不需要装 MySQL 官方 C 客户端openpyxl写 Excel注意 2.x 和 3.x 的 API 有差异本项目的代码按 3.x 写锁定版本就不会踩差异坑。3.2 源码结构定位判分核心函数与主流程整套源码的文件结构是标准的毕业设计形态。核心目录大致如下answer_sheet/ ├── config.py # 答题卡模板参数、标准答案、数据库连接配置 ├── image_utils.py # 图像预处理与透视变换 ├── option_detect.py # 选项区域定位与填涂判断 ├── score_engine.py # 答案比对与成绩计算 ├── excel_writer.py # 成绩写入Excel ├── mysql_writer.py # 成绩写入MySQL └── main.py # 主入口串起整个流程定位判分函数有个技巧不用从main.py一行行追直接在score_engine.py里搜def compare或def calc_score。这个函数入参是student_answers和answer_key返回百分制分数和逐题明细。典型实现def calc_score(student_answers, answer_key): # student_answers: {题号: A} 学生实际涂选结果 # answer_key: {题号: A} 标准答案 correct_count 0 detail {} for q_no, correct in answer_key.items(): ans student_answers.get(q_no, None) detail[q_no] (ans, correct, 1 if ans correct else 0) if ans correct: correct_count 1 total len(answer_key) score round(correct_count / total * 100, 2) return score, detail这里用student_answers.get(q_no, None)代替方括号索引是一个很实用的防御习惯。答题卡如果存在漏填student_answers里就没有对应的题号直接索引会抛KeyError整个判分流程当场中断用.get则会把漏填的题记为 None最终在 Excel 里显示“未作答”。score保留两位小数避免浮点精度带来 84.9999 这种难看的成绩。3.3 成绩存储Excel导出与MySQL写入的双通道配置源码同时落 Excel 和 MySQL很符合课程设计的评分点——既展示了文件输出能力又展示了数据库操作能力。Excel 写入基于openpyxlimport openpyxl def write_excel(scores, outputscores.xlsx): # scores: [{学号: 001, 姓名: 张三, 成绩: 85, 详情: ...}] wb openpyxl.Workbook() ws wb.active ws.title 答题卡成绩 ws.append([学号, 姓名, 成绩, 答题详情]) for row in scores: ws.append([row[学号], row[姓名], row[成绩], row[详情]]) wb.save(output) return outputMySQL 写入用pymysqlimport pymysql def write_mysql(scores): conn pymysql.connect( host127.0.0.1, port3306, userroot, password123456, databaseanswer_sheet, charsetutf8mb4, autocommitTrue ) try: with conn.cursor() as cursor: sql INSERT INTO score_record (student_no, student_name, score, detail) VALUES (%s, %s, %s, %s) data [(s[学号], s[姓名], s[成绩], s[详情]) for s in scores] cursor.executemany(sql, data) finally: conn.close()注意charsetutf8mb4必须显式指定。Navicat 11 建库默认字符集是 utf8但 utf8 在 MySQL 里是 utf8mb3 的别名存不了生僻字和 emoji姓名里有“”这类字的考生写入时会直接报Incorrect string value。executemany是批量插入比循环单条 insert 快一个数量级这个项目数据量不大但代码习惯值得保留下来。4. 避坑指南答题卡识别实战中的五类典型问题与排查方案4.1 环境与路径类findContours返回值、中文路径、MySQL认证插件现象1在preprocess函数里按源码解包运行时报TypeError: not enough values to unpack (expected 3, got 2)。原因OpenCV 版本差异。OpenCV 3.x 的cv2.findContours返回三个值OpenCV 4.x 返回两个值源码是按 4.x 写的你的环境里装成了 3.x。解决锁定安装opencv-python4.5.4.60。如果你确实只能待在 OpenCV 3 环境解包改成_, contours, _ cv2.findContours(...)也能跑但后续依赖 4.x 行为的代码可能还会冒出来建议直接换成 4.x。现象2图片放在纯英文路径下一切正常一放到“桌面/张三/答题卡照片/”这类带中文的目录cv2.imread返回值是 None。原因OpenCV 的imread底层走 C 的 fopen对中文路径和部分 Unicode 字符支持不好这在 Windows 中文系统上是必踩的坑。解决不要用imread用 numpy 二进制读取加imdecode组合def imread_unicode(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR)把项目里所有读图入口都换成这个封装函数中文路径问题当场消失。现象3Navicat 11 能正常连接 MySQL但pymysql连接时报Authentication plugin caching_sha2_password cannot be loaded或 1251 错误。原因MySQL 的默认认证插件从 5.7 到 8.0 发生了变化。如果你本机的 MySQL 不是 5.7 而是 8.0或者用户密码被重置过加密方式老版本pymysql不认caching_sha2_password。解决对这个项目最省事的是让 MySQL 用户使用mysql_native_password。先查当前插件SELECT user, host, plugin FROM mysql.user WHERE user root;如果是caching_sha2_password执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;然后重启服务再连。4.2 识别与判分类透视变形、固定阈值漏判、涂改残留误判现象4答题卡放在桌面上用手机斜着拍识别结果全是“未作答”或者选项区域完全错位。原因手机拍摄角度不是正上方垂直视角答题卡在图像里是倾斜甚至梯形的直接按原始坐标切选项区域切到的是斜矩形像素统计完全失真。解决在extract_options前强制加透视校正。用approxPolyDP拿到答题卡四个顶点再做透视变换def four_point_transform(image, pts): # pts是答题卡外轮廓的四个顶点按左上、右上、右下、左下排列 rect order_points(pts) (tl, tr, br, bl) rect widthA np.linalg.norm(br - bl) widthB np.linalg.norm(tr - tl) maxWidth max(int(widthA), int(widthB)) heightA np.linalg.norm(tr - br) heightB np.linalg.norm(tl - bl) maxHeight max(int(heightA), int(heightB)) dst np.array([[0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtypefloat32) M cv2.getPerspectiveTransform(rect, dst) return cv2.warpPerspective(image, M, (maxWidth, maxHeight))这段代码里order_points是最容易出错的地方它负责把四个顶点按“左上、右上、右下、左下”的顺序排好顺序错了透视矩阵直接算反。网上很多现成实现是拿坐标总和与差值排序建议用np.argsort加坐标判断写别直接用 x 排序。现象5密度阈值设 0.4浅涂的答题卡大量漏判调低到 0.25又出现涂改残留误判。原因固定阈值在面对不同批次、不同深浅的碳素笔时没有自适应能力。涂得太浅像素占比达不到 0.4橡皮擦没擦干净背景密度本身就高阈值低了就把残留笔迹算成涂选。解决放弃单阈值改用“相对密度 领先倍数”的双重判断def detect_choice(densities): # densities: {A: 0.3, B: 0.05, C: 0.02, D: 0.01} max_option max(densities, keydensities.get) max_density densities[max_option] sorted_densities sorted(densities.values(), reverseTrue) # 最大密度超过0.25且明显领先第二候选才认为是有效涂选 if max_density 0.25 and max_density 2 * sorted_densities[1]: return max_option return None同一道题的四个选项先分别算密度取最大者对应的选项再检查它是否比第二高的选项有明显优势。这样即使整体偏浅只要填涂区域确实比同题其他选项都重就能正确识别。这是我用多批样本试出来的方案比单纯调阈值稳得多。5. 参数调优与质量验证从“能识别”到“高准确率”的调试路径5.1 阈值、轮廓面积与透视参数矩阵整个答题卡识别项目里真正要调的参数不超过六个。我把它们列成一张矩阵方便你对照排查参数位置作用常用范围影响cv2.resize目标宽度统一处理尺寸1000~1500px太小选项区域不清晰太大计算慢高斯模糊核大小去噪强度(3,3)~(7,7)核越大边缘越模糊选项边界越容易丢Otsu二值化黑白分界自动基本不手调但要保证光照相对均匀密度阈值填涂判断0.2~0.5越低越容易误判残留越高越容易漏判浅涂轮廓面积过滤排除干扰卡片面积的1/10以上过滤掉桌边、杂物等其他轮廓透视变换边长校正后图像尺寸由顶点距离自动算影响后续坐标网格的对齐精度其中轮廓面积过滤是最容易被忽略的。如果不加限制findContours可能把桌面边缘、手指阴影、抽屉线都当成候选轮廓一旦max(contours, keycv2.contourArea)选错后面透视变换和坐标网格全错。加一个过滤条件能规避大半问题def is_card_like(c): area cv2.contourArea(c) if area 10000: # 按1200x1600的输入尺寸答题卡面积远大于这个值 return False x, y, w, h cv2.boundingRect(c) aspect w / h return 0.6 aspect 1.5 # 答题卡长宽比一般在0.7~1.3之间实际使用中我会把所有候选轮廓按面积排序取前三个再依次过is_card_like这样比单纯取面积最大更稳因为答题卡本体有时候不是面积最大的那个轮廓。5.2 可视化调试把判定结果画回原图上找问题调识别系统最有效的招不是打印日志而是画图。把每道题的判定结果直接画在原图上一眼就能看出问题出在哪一层。常见做法是写一个调试函数def visualize_result(img, options, answers, output_pathresult.jpg): result img.copy() for q_no, option in options.items(): if option answers.get(q_no): color (0, 255, 0) # 绿色为正确 else: color (0, 0, 255) # 红色为错误或未作答 x, y, w, h option_rects[q_no] cv2.rectangle(result, (x, y), (x w, y h), color, 2) cv2.imwrite(output_path, result)调参时把output_path指到一个临时目录跑完一组样本直接打开图片看颜色分布。绿色多说明识别正常红色集中出现在某一行基本就是坐标网格的行高没对齐红色随机散布多半是阈值和预处理的问题。这种方法比看数字更直观也比逐行 print 高效得多是我做这类项目时离不开的手段。5.3 真实考场差异倾斜、残留笔迹、光线不足的应对策略这套源码的目标场景是手机拍摄或扫描仪扫描。真实考试环境里会有三种典型扰动。倾斜四点透视变换能修正大部分问题但超过 15 度的倾斜不建议硬靠算法修。透视校正插值会损失边缘清晰度选项目区域被插值涂抹反而影响密度统计。拍摄时尽量让镜头正对答题卡这个物理层面的改进比任何算法都有效。残留笔迹橡皮擦没擦干净导致的背景密度偏高是阈值误判的重灾区。相对密度法可以缓解但更稳妥的做法是加“空白卡基线校准”——先用一张空白答题卡算出每个选项区域的基准背景密度识别时减去基线再算增量密度。实现上就是初始化时准备一份baseline_density字典后续每个选项的密度都减去对应基线值。光线不足整体偏暗时 Otsu 二值化容易把填涂区域和背景糊成一片。优先做亮度归一化用自适应直方图均衡clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) normalized clahe.apply(gray)clipLimit2.0限制对比度放大幅度防止背景噪声被过度增强tileGridSize是分块大小越大对局部光照的适应能力越强但计算也越慢。在基本光照均匀的场景下不均衡直接走 Otsu 性能更好所以这段代码通常放在一条“光照不足才启用”的分支里。6. 进阶技巧批量判分、动态答案与二次开发落地点把源码跑通单张图之后不要停在原地。真正能让这个项目变成毕业设计亮点或者工作里可用工具的是把它扩成批量判分脚本。实现思路很简单遍历目录下所有图片文件名里带学号和姓名逐张处理结果汇总写一次 Excel 和 MySQL。import os, glob def batch_score(folder_path, answer_key): results [] for img_path in glob.glob(os.path.join(folder_path, *.jpg)): # 文件名格式2023001_张三.jpg student_no, name os.path.splitext(os.path.basename(img_path))[0].split(_) binary, contours preprocess(img_path) options extract_options(binary, contours) score, detail calc_score(options, answer_key) results.append({学号: student_no, 姓名: name, 成绩: score, 详情: detail}) write_excel(results) write_mysql(results) return results原来的顶层界面基于 bootstrap.css、layui.css 封装跑通batch_score之后你可以把返回的results直接接到 Web 层的路由上做一个“上传图片就能看成绩”的小应用。三个二次开发落地点供参考。第一个把answer_key从config.py的硬编码改成 MySQL 表驱动。不同班级、不同试卷不需要改代码在管理界面里导入答案即可这比在源码里改字典更贴近真实使用。第二个增加“待人工复核”机制——对得分异常低、空白题过多、或者检测到两道题同时被涂的试卷在 Excel 里单独标记一列。这是我在实际使用中最有价值的改动能有效兜住算法误判。第三个成绩明细落库时保存完整的答题详情 JSON比如{1: A, 2: B, 3: 未填}方便后续做知识点错题分析比只存一个总分有挖掘价值得多。这些扩展都不需要动核心识别流程因为它们本来就建立在源码的模块化边界之上。从第一次跑通到改造成批处理工具我翻过最大的一个车就是盲目调阈值。当时觉得浅涂漏判是系统有 bug盯着阈值调了整整一晚上后来才意识到问题出在固定阈值的统计方式上换成“相对密度 领先倍数”之后一下就稳了。从那以后我每次调识别参数都强制走一遍固定流程先用 5 张不同光照的样本图跑一轮把判定结果画出来统一看错在哪一层再决定是改预处理还是改阈值绝不盯着一个指标死磕。这个习惯帮我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取