尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

基于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 % 高斯平滑sigma1.2 对 300dpi 答题卡比较合适 smooth imgaussfilt(gray, 1.2); % 自适应阈值前景(涂写区域)比背景暗 bw imbinarize(smooth, adaptive, ... ForegroundPolarity, dark, ... Sensitivity, 0.55); % 答题卡背景是白色标记和涂写是黑色取反后方便后续 regionprops BW ~bw;imbinarize的adaptive模式会为每个像素计算邻域阈值适合手机翻拍造成的光照不均。Sensitivity越大越容易把浅灰色铅笔痕迹也判成黑推荐 0.45~0.690% 的涂卡情况都能落在这个区间。ForegroundPolarity设为dark是告诉函数“我们要检测的是暗色前景”。最后取反是因为答题卡大面积白色、标记是黑色后续regionprops、bwareaopen和形态学操作都默认前景为 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%筛掉由噪点形成的短线。FillGap80允许同一直线上的缺口最长为 80 像素避免边缘被印刷的定位标记截断成多段。角度取中位数而不是均值原因是偶然检测到桌面边缘或纸阴影时会产生明显偏角中位数能容忍约 1/3 的异常线。注意Hough 返回的theta是直线法向与 x 轴的夹角不是纸张长边与水平线的夹角所以旋转符号容易写反。我一般先打印angleToRotate如果原图右倾但旋转后更歪就把-angleToRotate改成median(thetaList)90。另外imrotate的crop参数会裁掉旋转后露出的边角后续定位标记时不要依赖图像边缘。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 名字很直白找到图形区域的中心。在答题卡识别中这个中心是建立坐标系的原点而不是简单的几何质心。我读源码时看到它内部使用了regionprops和Centroid这与系统需要把物理坐标映射到答题卡版式的需求一致。大致等价于以下代码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~EcellW60~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 endpadX/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 end0.35和0.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(end1) q; % 疑题不计分不扣分 continue; end if singleChoice(q) sd(q) score score 5; % 每题 5 分 else errorList(end1) 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); endmain3.m在本资源里相当于合并后的处理入口接收图像矩阵返回包含outputImg和score的结果结构体。按钮回调里不要堆大量图像处理代码否则 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%优先检查网格步长而不是继续调阈值。本文还有配套的精品资源点击获取
返回列表