简介这是一套基于Python实现的电脑摄像头答题卡识别项目源码与配套文档面向计算机相关专业的毕业设计、课程设计及项目开发学习者也适合想入门图像处理与OpenCV实战的开发者。项目通过摄像头拍摄铺放在桌面、椅子甚至键盘上的答题卡即可完成单选答题卡的自动识别与判分在灵活性与准确度上表现突出能有效解决传统扫描设备依赖固定位置、部署成本高的问题。资源包共25个文件约2.95MB包含9个Python源码文件涵盖主程序、答题卡处理、评分与测试模块、11张png示例图片、1份pdf答题卡模板以及README、requirements、LICENSE等说明文件目录结构清晰便于快速理解与二次开发。目前已有130人学习关注。读者可获得一套经过严格测试的完整项目代码结合md文档与示例图片能快速跑通识别流程并在此基础上扩展题型、优化算法或迁移到其他视觉识别场景。1. 电脑摄像头识别答题卡为什么它比扫描仪方案更灵活答题卡识别这件事很多人第一反应是买台专用扫描仪走馈纸式批量扫描。但如果你做过学校阅卷、培训机构随堂测、或者小团队内部考试就会发现扫描仪方案有两个绕不开的硬伤一是设备成本和场地固定二是学生涂卡不规范时扫描仪出来的图照样要人工兜底。而用电脑摄像头做答题卡识别本质上是把「采集端」从专用硬件换成了普通 USB 摄像头或笔记本自带摄像头配合 Python 做图像处理和识别整套流程可以跑在一台普通电脑上。这个方向适合谁一是做毕业设计、课程设计的学生需要一个有完整技术链路、能演示、能写进论文的项目二是做教育培训、企业内部考核的开发者想低成本搭一套答题卡批改工具三是想练手 OpenCV 和图像处理的一线工程师。它的核心链路是摄像头采集 → 图像预处理 → 答题卡定位与透视校正 → 填涂区域分割 → 选项判定 → 输出成绩。整条链路用 Python OpenCV NumPy 就能跑通不依赖深度学习也能做到可用精度这也是它比很多「上来就上模型」的方案更适合落地的原因。2. 摄像头答题卡识别的技术链路拆解2.1 从一帧画面到一张标准答题卡四个关键阶段摄像头采集到的原始画面和扫描仪出来的图完全是两回事。扫描仪是均匀光照、固定分辨率、无透视畸变摄像头则受环境光、拍摄角度、镜头畸变影响。所以整条链路里图像预处理和透视校正占了一半以上的工作量。第一个阶段是采集与降噪。摄像头帧率一般 30fps但识别不需要每帧都处理常见做法是隔几帧取一帧或者按键触发采集。采集到的 BGR 图像先转灰度再做高斯模糊去噪。高斯核大小一般取 5×5 或 7×7太大糊掉边缘太小去噪不干净。第二个阶段是答题卡定位。答题卡通常有黑色边框或定位黑块用 Canny 边缘检测 轮廓查找找到面积最大的四边形轮廓就是答题卡区域。这里有个关键点轮廓必须是凸四边形且面积占画面比例要设阈值否则容易把桌面边缘、书本误判成答题卡。第三个阶段是透视校正。找到四个角点后用cv2.getPerspectiveTransform计算变换矩阵再用cv2.warpPerspective把倾斜的答题卡拉成正视图。校正后的图答题卡边框应该接近水平垂直这是后续分割的前提。第四个阶段是填涂区域分割与判定。校正后的图按预设的行列坐标切出每个题目的选项区域统计每个区域内黑色像素占比占比超过阈值的判定为「涂了」。阈值一般取 0.30.5具体要看涂卡笔的深浅和光照。2.2 环境准备与依赖安装把 OpenCV 跑起来动手之前先把环境搭好。Python 建议 3.8 以上OpenCV 用opencv-python包即可。如果你用的是笔记本自带摄像头cv2.VideoCapture(0)就能打开如果是外接 USB 摄像头索引可能是 1 或 2需要试。# 创建虚拟环境避免污染全局包 python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate # 安装核心依赖 pip install opencv-python numpy这里说明一下参数opencv-python是主包包含图像处理和摄像头接口numpy用于矩阵运算OpenCV 的很多返回值本身就是 NumPy 数组。如果你后续要做更复杂的形态学操作这两个包足够。不建议一上来装opencv-contrib-python除非你要用 SIFT 这类专利算法普通答题卡识别用不上。提示如果cv2.VideoCapture(0)打不开摄像头先确认摄像头没被其他程序占用Windows 下可以在「相机」应用里测试一下。Linux 下检查/dev/video*设备是否存在。2.3 答题卡定位与透视校正的完整代码下面这段代码是整条链路里最核心的部分从摄像头读一帧找到答题卡轮廓做透视校正输出一张正视图。import cv2 import numpy as np def order_points(pts): 把四个角点按 左上、右上、右下、左下 排序 rect np.zeros((4, 2), dtypefloat32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] # 左上xy 最小 rect[2] pts[np.argmax(s)] # 右下xy 最大 diff np.diff(pts, axis1) rect[1] pts[np.argmin(diff)] # 右上x-y 最小 rect[3] pts[np.argmax(diff)] # 左下x-y 最大 return rect def four_point_transform(image, 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) warped cv2.warpPerspective(image, M, (maxWidth, maxHeight)) return warped def find_answer_sheet(frame): 从一帧画面里找到答题卡并校正 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) edged cv2.Canny(blurred, 50, 150) # 膨胀一下让断裂的边缘连起来 edged cv2.dilate(edged, np.ones((3, 3), np.uint8), iterations1) cnts, _ cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts sorted(cnts, keycv2.contourArea, reverseTrue)[:5] sheet_cnt None for c in cnts: peri cv2.arcLength(c, True) approx cv2.approxPolyDP(c, 0.02 * peri, True) # 近似成四边形且面积足够大 if len(approx) 4 and cv2.contourArea(approx) 50000: sheet_cnt approx break if sheet_cnt is None: return None return four_point_transform(frame, sheet_cnt.reshape(4, 2)) # 主循环 cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break warped find_answer_sheet(frame) if warped is not None: cv2.imshow(Answer Sheet, warped) cv2.imshow(Original, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明order_points解决的是「找到的四个角点顺序是乱的」这个问题透视变换必须知道哪个点对应左上、哪个对应右下否则变换出来是翻转或扭曲的。four_point_transform里用边长最大值作为目标图尺寸保证校正后不丢信息。find_answer_sheet里 Canny 的双阈值 50/150 是经验值光照均匀可以收紧到 75/200光照差就放宽。面积阈值 50000 是针对 640×480 以上分辨率设的如果你用 1080p这个值要相应调大。参数说明cv2.approxPolyDP的0.02 * peri是近似精度值越小越贴近原始轮廓但可能近似不出四边形值越大越容易近似成四边形但角点位置会偏。答题卡这种规则矩形0.02 是比较稳的。2.4 填涂区域判定从校正图到选项结果校正后的答题卡是正视图接下来按预设坐标切出每个选项区域。这里有两种做法一种是硬编码坐标适合固定版式的答题卡另一种是先用形态学找定位黑块再按相对位置推算。毕设和课程设计里硬编码坐标更快出结果但换一张答题卡就要改代码。def judge_filled(warped, x, y, w, h, threshold0.35): 判断某个区域内是否被涂黑 roi warped[y:yh, x:xw] gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) # 二值化黑色像素为 255 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) black_ratio np.count_nonzero(binary) / (w * h) return black_ratio threshold, black_ratio # 假设校正后答题卡宽 800 高 1000第一题四个选项坐标如下 options [ (100, 200, 40, 40), # A (160, 200, 40, 40), # B (220, 200, 40, 40), # C (280, 200, 40, 40), # D ] for idx, (x, y, w, h) in enumerate(options): filled, ratio judge_filled(warped, x, y, w, h) print(f选项 {chr(65idx)}: 涂了{filled}, 黑色占比{ratio:.2f})逻辑说明THRESH_BINARY_INV THRESH_OTSU是自动阈值二值化Otsu 会根据图像直方图自动找分割点比手动设 127 更稳。black_ratio是黑色像素占区域总面积的比例涂满的选项一般在 0.5 以上没涂的在 0.05 以下0.35 这个阈值留了余量。参数说明ROI 的坐标和尺寸必须和你的答题卡版式对齐。建议先用一张标准答题卡校正后用画图工具量出每个选项的像素坐标再填进代码。如果答题卡是 A4 纸校正后宽度建议统一缩放到 800 或 1000 像素这样坐标可以固定下来。3. 摄像头采集的工程化处理与稳定性优化3.1 光照不均和阴影摄像头方案最大的敌人扫描仪有均匀光源摄像头没有。同一张答题卡上午拍和下午拍窗边拍和灯下拍二值化结果可能完全不同。这是摄像头方案翻车最多的地方也是很多人做完 demo 就放弃的原因。常见做法有三种。第一种是自适应阈值用cv2.adaptiveThreshold替代全局阈值它对局部光照变化更鲁棒。第二种是形态学开闭运算先开运算去噪点再闭运算填小孔。第三种是限制采集条件比如固定一个补光灯、固定拍摄距离和角度把变量控制住。def preprocess_roi(roi): 对选项区域做自适应二值化抗光照不均 gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) # 自适应阈值邻域 15x15常数 C10 binary cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 15, 10) # 开运算去小噪点 kernel np.ones((2, 2), np.uint8) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) return binary参数说明blockSize15是邻域大小必须是奇数太小会保留噪声太大会失去局部适应性。C10是从均值里减去的常数值越大二值化越保守黑色区域越少。这两个参数要根据实际光照调没有万能值。注意自适应阈值不是银弹。如果答题卡上有大面积阴影局部阈值也会失效。最稳的办法还是控制拍摄环境补光灯 固定支架比任何算法都管用。3.2 摄像头参数怎么调分辨率、曝光、白平衡cv2.VideoCapture打开摄像头后可以设置一系列参数。很多人不知道这些参数可以调结果采集到的图要么太暗要么太糊。cap cv2.VideoCapture(0) # 设置分辨率很多摄像头默认 640x480调到 1280x720 更清晰 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) # 关闭自动曝光手动设一个值避免画面忽明忽暗 cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) cap.set(cv2.CAP_PROP_EXPOSURE, -6) # 关闭自动白平衡避免颜色漂移 cap.set(cv2.CAP_PROP_AUTO_WB, 0) cap.set(cv2.CAP_PROP_WB_TEMPERATURE, 4500)参数说明CAP_PROP_AUTO_EXPOSURE在不同平台上的取值含义不一样Windows 下 0.25 表示手动模式Linux 下可能是 1 或 3需要实测。CAP_PROP_EXPOSURE是曝光值负值表示更短的曝光时间画面更暗但更不容易糊。CAP_PROP_WB_TEMPERATURE是色温4500 接近中性白具体看你的灯光。这里有个血泪经验不是所有摄像头都支持这些参数设置cap.set返回 False 就说明不支持。遇到这种情况只能靠外部光源和拍摄距离来补偿。3.3 多题批量识别与结果输出单题识别跑通后要扩展到整张答题卡。做法是把所有题目的选项坐标存成一个列表循环判定最后输出每题的答案。# 题目版式每题 4 个选项横向排列行间距 60 像素 questions [] start_x, start_y 100, 200 for q in range(20): # 20 道题 opts [] for o in range(4): x start_x o * 60 y start_y q * 60 opts.append((x, y, 40, 40)) questions.append(opts) answers [] for q_idx, opts in enumerate(questions): filled_opts [] for o_idx, (x, y, w, h) in enumerate(opts): filled, ratio judge_filled(warped, x, y, w, h) if filled: filled_opts.append(chr(65 o_idx)) # 处理多涂和漏涂 if len(filled_opts) 1: answers.append(filled_opts[0]) elif len(filled_opts) 0: answers.append(未涂) else: answers.append(多涂: .join(filled_opts)) print(识别结果, answers)逻辑说明这段代码把版式抽象成「起始坐标 行列间距」换答题卡时只改这几个参数。多涂和漏涂是实际阅卷里必须处理的异常直接输出「多涂」或「未涂」比强行猜一个答案更负责。参数说明start_x、start_y是第一个选项的左上角坐标60是选项间距和行间距。这些值必须和你的答题卡版式严格对应建议先用一张标准卡校正后量取。4. 避坑与常见问题排查4.1 轮廓找不准把桌子边缘当成答题卡现象程序框出来的区域不是答题卡而是桌面边缘或背景里的矩形物体。原因findContours找的是所有轮廓按面积排序后取最大的但桌面、显示器边框面积可能比答题卡还大。解决加两个约束。一是轮廓近似后必须是四边形二是四边形的宽高比要在合理范围比如 0.61.5 之间三是面积占画面比例要设上下限。另外答题卡如果有明显的黑色边框可以先用颜色阈值把黑色区域提取出来再找轮廓比直接 Canny 更稳。4.2 透视校正后图像扭曲或翻转现象校正后的答题卡是斜的、翻转的或者比例严重失真。原因order_points排序错误或者四个角点的顺序和实际不对应。解决检查order_points里的 sum 和 diff 逻辑。sum 最小是左上、最大是右下这个没问题diff 是 x-y最小是右上、最大是左下。如果答题卡旋转超过 45 度这个排序会失效需要先判断答题卡的大致朝向。另外目标图的宽高要用边长最大值不要用固定值否则比例会失真。4.3 涂卡判定忽高忽低同一张卡两次结果不一样现象同一张答题卡两次识别结果不同有的选项一次判涂了一次判没涂。原因光照变化导致二值化阈值漂移或者 ROI 坐标有 12 像素偏移。解决第一改用自适应阈值减少全局光照影响。第二ROI 区域适当放大几个像素给坐标误差留余量。第三判定阈值不要卡在 0.35 这种临界值涂了的选项黑色占比通常远高于 0.5没涂的远低于 0.1把阈值设到 0.3 以下或 0.5 以上都能拉开差距。第四如果还是不稳对同一帧做多次判定取众数。4.4 摄像头帧率低识别卡顿现象cap.read()很慢画面卡顿识别延迟明显。原因分辨率设太高或者每帧都在做完整识别流程。解决识别不需要每帧都做。可以隔 5 帧取一帧或者按键触发。分辨率 1280×720 足够答题卡识别没必要上 1080p 或 4K。另外Canny 和轮廓查找比较耗时可以先把图像缩放到 640×480 做定位找到角点后再用原图做透视变换这样精度和速度都能兼顾。4.5 答题卡版式换了代码全要改现象换一张不同版式的答题卡坐标全部对不上识别结果全错。原因硬编码坐标只适合固定版式。解决如果要做通用方案需要在答题卡上印定位黑块比如四个角各一个实心方块先检测黑块位置再按黑块之间的相对比例推算每个选项的坐标。这样换版式只需要改比例参数不用改绝对坐标。这是从 demo 走向可用工具的关键一步。5. 从能跑到好用精度验证与版式自适应技巧5.1 用混淆矩阵验证识别精度而不是靠眼睛看很多人做完识别随便拿几张卡跑一下看着差不多就认为成了。这是毕业设计答辩时最容易被问倒的地方。正确的做法是准备一批已人工批改的答题卡把程序结果和人工结果做对比算准确率、漏检率、误检率。指标含义计算方式准确率判定正确的选项占比正确数 / 总选项数漏检率涂了但没识别出来的比例漏检数 / 实际涂卡数误检率没涂但识别成涂了的比例误检数 / 实际未涂数多涂检出率多涂异常被正确标记的比例正确标记多涂数 / 实际多涂数测试集至少准备 30 张覆盖不同光照、不同拍摄角度、不同涂卡深浅。如果准确率低于 95%先查光照和 ROI 对齐再查阈值。漏检率高说明阈值太高误检率高说明阈值太低这两个指标要一起看。5.2 版式自适应的最小实现定位黑块 相对坐标硬编码坐标的方案换一张卡就废。加四个定位黑块就能做到版式自适应。思路是在答题卡四角印 10×10 毫米的实心黑块校正后检测这四个黑块的中心坐标然后按「黑块间距的百分比」来定位每个选项。def find_anchor_blocks(warped): 在矫正后的答题卡上找四个定位黑块 gray cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 80, 255, cv2.THRESH_BINARY_INV) cnts, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) blocks [] for c in cnts: x, y, w, h cv2.boundingRect(c) area w * h # 黑块面积在合理范围且接近正方形 if 50 area 2000 and 0.7 w / h 1.4: blocks.append((x w // 2, y h // 2)) return blocks # 假设四个黑块中心为 tl, tr, br, bl # 选项坐标 黑块坐标 相对比例 * 黑块间距 def get_option_roi(anchors, rel_x, rel_y, size40): tl, tr, br, bl anchors width tr[0] - tl[0] height bl[1] - tl[1] cx int(tl[0] rel_x * width) cy int(tl[1] rel_y * height) return (cx - size // 2, cy - size // 2, size, size)逻辑说明find_anchor_blocks用固定阈值 80 提取黑色区域因为定位黑块是纯黑印刷阈值可以设低。面积和宽高比约束用来排除文字和噪点。get_option_roi把选项位置表示成相对黑块间距的比例这样答题卡在画面里大小变化、轻微旋转都不影响定位。参数说明rel_x和rel_y是 01 之间的小数表示选项中心相对于左上黑块的位置比例。比如第一题 A 选项在横向 20%、纵向 30% 的位置就传 0.2 和 0.3。这些比例需要根据你的答题卡版式量一次之后固定。5.3 一个我踩过的坑别在识别环节做太多事我最早做这个方案时想在识别环节同时做「手写学号识别」「主观题区域检测」结果代码越写越复杂答题卡识别本身的稳定性反而下降了。后来我把职责拆开摄像头识别只负责「定位答题卡 判定客观题填涂」学号识别、主观题切图这些交给后续独立模块。每个模块只做一件事出了问题好定位也好替换。这个习惯一直保留到现在。做视觉项目最怕的就是一条链路里塞太多目标最后哪个都不准。先把一个环节做到 95% 以上再往上叠比一开始就追求大而全靠谱得多。希望帮到你。本文还有配套的精品资源点击获取