news 2026/9/13 5:28:14

基于Matlab的答题卡识别:图像预处理与Hough变换透视校正

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Matlab的答题卡识别:图像预处理与Hough变换透视校正

简介:基于Matlab实现的答题卡识别系统毕业设计资源,面向计算机、电子信息、数学等专业学生,适用于课程设计、期末大作业或毕业设计参考。项目包含完整GUI界面与图像处理流程,涵盖高斯滤波、图像归一化、中值中心定位、Hough变换、角度计算、旋转校正、平滑处理及识别主程序等多个功能模块,配有9张测试图片与mat数据文件,可帮助读者理解从图像预处理到判分的完整链路。压缩包共24个文件,包含11个m源码、9个jpg图片、2个mat数据、1个asv备份及1个fig界面文件,整体大小28.47MB,结构清晰便于查阅。已有647人学习下载,适合需要快速上手答题卡识别项目、学习Matlab GUI编程与图像处理算法综合应用的读者,作为高分项目参考资料具有较好借鉴价值。

1. 为什么拿 Matlab 做答题卡识别

第一次拆这类答题卡识别系统源码,很多人以为难点在“怎么判断有没有涂黑”,实际接手这套基于 Matlab 实现答题卡识别的毕业设计源码后会发现,真正决定识别率的是一张照片从“拍歪的现场图”变回“规整栅格图”的那段图像处理链路。它解决的是很具体的场景:输入手机拍摄或扫描得到的答题卡图片,完成灰度化、高斯滤波、霍夫变换、透视校正、选项网格定位、涂写判断,再与 answer.mat 或 answer2.mat 中的标准答案比对,输出得分和错题列表,并通过 GUI 界面把整个流程可视化地演示出来。适合正在做 Matlab 课程设计、期末大作业或毕业设计的电子信息、计算机类专业学生。新手可以直接跑通 main.m 到 main3.m 的完整链路,有基础的人则可以基于 findMidCenter.m、Hough_Process.m 这些模块改成自己的版式。

2. 答题卡图像的预处理与透视校正:从灰度化到 Hough 变换

2.1 灰度化、平滑与二值化的参数选择

PicSrc 里的 1.jpg、2.jpg 这类原图,一般是 300dpi 扫描图或者手机平拍摄像头图。彩色通道对后续找边缘和连通域没有额外收益,反而会增加自适应阈值的计算量,所以第一步统一转灰度。gaussianFilter.m 在这里的作用是抑制传感器噪声,直接用imgaussfilt也能达到同样效果。需要说明的是,高斯核不是越大越好,sigma 太大会把铅笔灰的浅痕迹也抹平,导致后面的填充率统计偏低。

img = imread('1.jpg'); if size(img, 3) == 3 gray = rgb2gray(img); else gray = img; end % 高斯平滑,sigma=1.2 对 300dpi 答题卡比较合适 smooth = imgaussfilt(gray, 1.2); % 自适应阈值:前景(涂写区域)比背景暗 bw = imbinarize(smooth, 'adaptive', ... 'ForegroundPolarity', 'dark', ... 'Sensitivity', 0.55); % 答题卡背景是白色,标记和涂写是黑色,取反后方便后续 regionprops BW = ~bw;

imbinarizeadaptive模式会为每个像素计算邻域阈值,适合手机翻拍造成的光照不均。Sensitivity越大越容易把浅灰色铅笔痕迹也判成黑,推荐 0.45~0.6,90% 的涂卡情况都能落在这个区间。ForegroundPolarity设为dark,是告诉函数“我们要检测的是暗色前景”。最后取反是因为答题卡大面积白色、标记是黑色,后续regionpropsbwareaopen和形态学操作都默认前景为 1。如果 9.jpg 这类压缩较重的手机图出现边缘模糊,可以先imsharpen,但强度不要超过 1,否则会在文字边缘产生白色噪点环。

2.2 用 Hough 变换检测答题卡边界与旋转校正

Hough_Process.m 实际就是把“找边缘直线”和“得到旋转角”封装在一起。答题卡通常有两条以上长直线边缘,即使在复杂背景下也会保持高响应;Hough 把边缘点投票到参数空间,输出直线方程,比直接对边缘点做最小二乘拟合更抗孤立点干扰。实现上分四步:Canny 边缘、hough 投票、峰值提取、houghlines 画线。

edges = edge(BW, 'Canny', [0.1 0.25]); [H, T, R] = hough(edges); peaks = houghpeaks(H, 30, 'Threshold', ceil(0.35 * max(H(:)))); lines = houghlines(edges, T, R, peaks, 'FillGap', 80, 'MinLength', 150); % 同一张卡的水平边 theta 基本相同,取中位数抗离群 thetaList = [lines.theta]; angleToRotate = median(thetaList); BWr = imrotate(BW, -angleToRotate, 'bilinear', 'crop');

Canny 阈值[0.1 0.25]比默认偏松,因为答题卡边缘在二值图里非常锐利,阈值太严格会丢失定位块附近的短边。houghpeaks只取投票前 30 的峰,Threshold设为最大投票数的 35%,筛掉由噪点形成的短线。FillGap=80允许同一直线上的缺口最长为 80 像素,避免边缘被印刷的定位标记截断成多段。角度取中位数而不是均值,原因是偶然检测到桌面边缘或纸阴影时会产生明显偏角,中位数能容忍约 1/3 的异常线。

注意:Hough 返回的theta是直线法向与 x 轴的夹角,不是纸张长边与水平线的夹角,所以旋转符号容易写反。我一般先打印angleToRotate,如果原图右倾但旋转后更歪,就把-angleToRotate改成+median(thetaList)+90。另外,imrotatecrop参数会裁掉旋转后露出的边角,后续定位标记时不要依赖图像边缘。

2.3 透视校正:把斜视角拍摄的答题卡拉正

旋转校正只解决平面内旋转。手机不是正上方拍摄时,试卷会呈现梯形或四边形透视变形,这时必须做透视校正。源码里的 Image_Rotate.m 覆盖了旋转部分,更完整的识别系统还会保留一个透视入口。常见做法是用fitgeotrans把四个定位角标映射到标准矩形。

% 已通过 regionprops 得到角标中心 corners = [x1 y1; x2 y2; x3 y3; x4 y4] % 标准答题卡对应坐标,单位像素 fixed = [0 0; 2100 0; 2100 2800; 0 2800]; tform = fitgeotrans(corners, fixed, 'projective'); Icorrect = imwarp(BWr, tform, ... 'OutputView', imref2d([2800 2100]));

fitgeotrans至少需要 4 对匹配点,projective变换能描述任意四边形到矩形的映射,比affine只能处理平行四边形的能力更强。检测角标时,我会用regionprops(BWr, 'Area', 'Centroid')筛选面积在 800~5000 像素内的连通域,再利用它们到图像质心的距离排序。如果答题卡有黑色边框,角标会和边框连成一个连通域,需要先imerode一次,或者在先验位置裁剪固定区域后再做检测。完成这一步后,后续网格坐标可以直接按固定像素步长切分,不必每张图重新学习版式。

3. 选项区域定位与分割:从定位标记到网格坐标

3.1 用 findMidCenter 语义理解定位标记

findMidCenter.m 名字很直白:找到图形区域的中心。在答题卡识别中,这个中心是建立坐标系的原点,而不是简单的几何质心。我读源码时看到它内部使用了regionpropsCentroid,这与系统需要把物理坐标映射到答题卡版式的需求一致。大致等价于以下代码:

stats = regionprops(BW, 'Area', 'Centroid', 'BoundingBox'); candidate = stats([stats.Area] > 500 & [stats.Area] < 5000); centers = reshape([candidate.Centroid], 2, [])'; % 调试时打印面积分布,确认定位块和涂写块的面积区间 areas = [candidate.Area]; disp(areas);

面积区间是关键。扫描后涂写黑块面积一般在 300~600 像素,定位块会设计得更大;如果区间下限设太小,会把噪点也算进来。中心点找到后,建议按“左上、右上、左下、右下”重新排序,否则fitgeotrans的坐标对应关系会错乱。我常用sortrows(centers, [2 1])先粗排,再通过质心到整体包围盒四角距离做精确匹配。

3.2 行列切割:题号步长与选项宽高映射

定位完成后,识别系统进入最关键的一步:网格坐标生成。这里不能直接固定行列等分,因为印刷和扫描过程会产生 3~5 个像素的偏移,直接等分会导致选项框切偏。我会先建一张参数表,把版式信息写清楚,再用两个定位标记之间的距离计算步长。

参数推荐值说明
rows20题号总行数,来自印刷版式
cols4A~D 选项数,5 个时改为 A~E
cellW60~80 px单个选项区域宽度
cellH55~75 px单个选项区域高度
stepX(xRight - xLeft) / cols水平基准步长
stepY(yBottom - yTop) / rows垂直基准步长

切割代码:

% 以左上定位块中心 (x0,y0) 为起点 for q = 1:rows for opt = 1:cols x1 = round(x0 + (opt-1) * stepX + padX); y1 = round(y0 + (q-1) * stepY + padY); x2 = round(x1 + cellW); y2 = round(y1 + cellH); roi{q, opt} = BW(y1:y2, x1:x2); end end

padX/padY用于去掉印刷框线本身的干扰,建议设成round(cellW*0.15)round(cellH*0.12)。切割出的 ROI 直接保存为元胞数组,后续涂写判断就不需要再在全图中做运算。这里一个常见误用是直接imcrop后尺寸不一致,导致cellfun报尺寸不匹配;我习惯把所有 ROI 统一插值到 64×64,即roi{q, opt} = imresize(roi{q, opt}, [64 64]),这样后续特征长度固定,也方便批量处理。

3.3 当定位标记缺失时怎么办

PicSrc 里部分图片存在裁切或阴影,定位标记可能被截掉半个,regionprops会漏检。常用补救方式有两种:第一种是检测整张图中最长的水平线和竖直线,用交点代替缺失点;第二种是利用 Hough 直线与图像边界的交点估算出扩展角。推荐优先补点而不是放弃,因为毕业设计答辩时经常用缺角图片测试鲁棒性。如果某个角标缺失,我会把该角坐标设置为相邻两条边缘直线在 200 像素范围内的像素坐标最大值,并记录一个warning,在 GUI 状态栏提示“已自动补全定位点,精度可能下降”。

4. 涂写识别与评分算法:像素密度、粘连处理和答案比对

4.1 用填充率而不是“是否全黑”判断涂写

拿到选项 ROI 后,直接统计黑色像素占比是最稳定的方法。常见阈值:涂写区域内黑色像素占比超过 35% 判为已涂,低于 15% 判定为空白,15%~35% 视为可疑。大多数 2B 铅笔在 300dpi 下填充率在 0.5 以上,HB 铅笔或擦除不干净会在 0.2~0.3 之间。实现如下:

filled = zeros(rows, cols); for q = 1:rows for opt = 1:cols cell = roi{q, opt}; ratio = sum(cell(:) > 0) / numel(cell); filled(q, opt) = ratio; end end mark = (filled > 0.35);

sum(cell(:) > 0)统计 ROI 中前景像素总数,numel(cell)是区域面积。这里不推荐对每个 ROI 分别graythresh,因为全空白的 ROI 用大津法会把噪声阈值抬得很高,导致误判。如果需要自适应,我一般只对整张图算一个 Otsu 阈值,然后每个 ROI 用它的 0.8 倍作为判断阈值。

4.2 单选、多选、未涂和模糊涂写分别处理

答题卡批改最容易出逻辑问题的是把单选、多选混合在一起处理。单选应该取每行填充率最高的选项;多选则要把所有超过阈值的选项都选中;如果出现填充率在 0.15 到 0.35 之间的选项,应该标记为“疑题”而不是直接扣分。代码如下:

singleChoice = zeros(rows, 1); multiChoice = cell(rows, 1); for q = 1:rows rowMark = filled(q, :); over35 = find(rowMark > 0.35); over15 = find(rowMark > 0.15 & rowMark <= 0.35); if ~isempty(over35) [~, idxMax] = max(rowMark); singleChoice(q) = idxMax; multiChoice{q} = over35; elseif isempty(over15) singleChoice(q) = 0; % 未涂 multiChoice{q} = []; else % 介于 15% 和 35% 之间:交给人工复核 singleChoice(q) = NaN; multiChoice{q} = over15; end end

0.350.15是我基于 PicSrc 里多张图统计出来的经验值。如果切换低分辨率扫描,可以先把每行filled矩阵打印到 Excel,观察涂写与空白的分布区间再微调。NaN在后续比对中会作为“疑题”处理,在 GUI 中显示为黄色,不参与得分,也不判错。

4.3 与 answer.mat 比对,得到得分和错题列表

answer.mat 和 answer2.mat 存放标准答案,常见格式是 20×1 的数值数组,也可能包含学号和得分字段。这里给出兼容单选比对的代码:

load('answer.mat'); % 假设变量名为 sd,类型为 20x1 数值数组 score = 0; errorList = []; for q = 1:rows if isnan(singleChoice(q)) errorList(end+1) = q; % 疑题不计分不扣分 continue; end if singleChoice(q) == sd(q) score = score + 5; % 每题 5 分 else errorList(end+1) = q; end end fprintf('得分:%d / %d\n', score, rows*5);

与答案比对前先排除NaN,避免isequal(NaN, 3)永远返回 0。错题通过errorList存题号,便于后续生成 Excel 错题报告。如果标准答案本身是多选结构,可以改用cellfun(@(x,y) isequal(x,y), multiChoice, answerList)逐题比对。

下面是一张我调试时经常对照的故障表:

现象原因处理
某一行全部误判网格偏移累计误差用相邻定位标记重新计算 stepY
所有选项都偏黑背景阈值取了噪点把 Sensitivity 降低 0.05
双选题漏选涂写接近阈值下界对 ROI 做 imdilate 再统计
印刷字符被当涂黑字符面积接近涂写块按连通域面积过滤,设置最大面积上限

5. 把识别流程包装成 GUI 以及真实试卷上的容错技巧

5.1 MYGUI.fig 的回调设计

MYGUI.fig 是界面文件,MYGUI.m 是主程序。典型布局为:左边坐标轴显示原图,右边坐标轴显示识别结果,中间一排按钮“读入图像、开始识别、重置”,下方文本框显示得分和疑题。按钮回调用guidata保存数据,避免每次点击都重新读取图片。

function btnLoad_Callback(hObject, eventdata, handles) [filename, pathname] = uigetfile({'*.jpg;*.png', '图片文件'}); if isequal(filename, 0), return; end handles.origin = imread(fullfile(pathname, filename)); axes(handles.axesOrigin); imshow(handles.origin); guidata(hObject, handles); end function btnRun_Callback(hObject, eventdata, handles) if ~isfield(handles, 'origin'), return; end handles.result = main3(handles.origin); axes(handles.axesResult); imshow(handles.result.outputImg); set(handles.textScore, 'String', sprintf('得分:%.1f', handles.result.score)); guidata(hObject, handles); end

main3.m在本资源里相当于合并后的处理入口,接收图像矩阵,返回包含outputImgscore的结果结构体。按钮回调里不要堆大量图像处理代码,否则 GUIDE 生成的 m 文件会很乱,后续定位问题也不方便。

5.2 四个提升“答辩通过率”的小技巧

第一,形态学开运算消除孤立噪点。BW2 = imopen(BW, strel('disk', 2))只去掉 1~2 像素的噪声,不会碰涂写区域,对扫描产生的碎点特别有效。

第二,涂写不饱满时先膨胀再统计。roi_dil = imdilate(roi, strel('disk', 3))会让浅涂区域的填充率提升 5~10 个百分点。具体做法是同时计算原始 ROI 和膨胀后 ROI 的填充率,只要膨胀后填充率超过 0.5 就判为已涂,这样可以兼容擦除不干净的情况。

第三,倾斜角度校验放在 GUI 状态栏。处理前打印Compute_Angle.m得到的角度,如果绝对值大于 5 度,就提示用户重新拍照,而不是强行校正。大角度透视会直接切断选项框,校正后依然会漏选。

第四,把识别结果写成 Excel 表格。使用writetable将题号、标准答案、学生答案、是否匹配四列导出,这是毕业设计答辩时最直观的产品化证明。调整阈值时,我会把“疑题”数量作为指标,若疑题超过总题数 3%,优先检查网格步长而不是继续调阈值。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 5:27:31

GPT Image 2实战评测:文本渲染、API调用与工作流集成指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 5:27:05

CAN总线G代码传输实战:用C语言和SocketCAN实现稳定运动控制

简介&#xff1a;这是一份基于C语言的CAN通信协议实现源码包&#xff0c;适合嵌入式开发者、汽车电子或工业控制领域的初学者学习参考。资源围绕DSP2833x平台搭建&#xff0c;覆盖ECan模块驱动、报文收发、中断服务、过滤器配置及错误处理等关键环节&#xff0c;并附带完整工程…

作者头像 李华
网站建设 2026/9/13 5:24:20

Shottr:macOS原生截图标注与OCR一体化工具

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 5:24:18

达梦数据库与MySQL的MERGE语法对比及迁移踩坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 5:23:20

GPT Image 2 实测:从提示词工程到业务工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华