简介:这是一套基于MATLAB实现的数字验证码识别系统项目源码,面向计算机、人工智能、通信工程、自动化等专业的在校学生与教师,也适合作为课程设计、毕业设计或项目初期立项的演示案例。资源包共306个文件,以298张jpg验证码样本图片为核心数据,配合4个m脚本文件、2张png、1个fig界面文件与1个mat数据文件,整体压缩包约458KB,体积轻量便于快速部署与调试。项目代码均经过实际运行测试,功能完整可用,读者可借助样本图片与脚本理解验证码预处理、字符分割与识别的基本流程,并在此基础上修改扩展,实现其他识别功能。目前已有59人学习关注,适合作为入门图像处理与模式识别的实践参考,也可用于课设作业的二次开发。
1. 数字验证码识别系统:从一张噪点图到可复现的 MATLAB 流水线
手里拿到一张 120×40 的验证码截图,四位数字,背景有随机噪点、干扰线和轻微旋转,人眼一眼能读,但要让程序稳定读出来,很多人第一反应是上深度学习。可真到落地时你会发现,样本量不够、标注成本高、训练环境还未必顺手。这时候一套纯 MATLAB 的数字验证码识别系统反而更实在:它不依赖 GPU,不依赖大规模数据集,靠图像预处理加模板匹配或简单分类器就能跑通,适合做课程设计、图像处理大作业,也适合当作 MATLAB 图像处理能力的练手项目。标题里的「数字验证码识别系统」本质是一条流水线:灰度化、二值化、去噪、字符分割、特征提取、分类输出。它解决的是「固定长度纯数字验证码的自动读取」,适合样本可控、字体变化不大的场景。如果你手里正好有 MATLAB,想用一套能讲清楚每一步在干什么的方案把这件事做出来,下面这条路径可以直接照着走。
2. 预处理流水线:把彩色噪点图变成干净的二值矩阵
验证码识别的第一道坎从来不是分类器,而是图像本身太脏。彩色背景、随机噪点、干扰线、字符粘连,这些都会让后续分割直接崩掉。预处理的目标只有一个:让每个数字变成一个独立、干净、位置明确的二值区域。这一步做不好,后面换什么分类器都是白搭。
2.1 灰度化与二值化的参数选择
MATLAB 里读进来的验证码通常是 RGB 三通道,先转灰度。常见做法是rgb2gray,它按人眼亮度感知加权,比简单取平均更合理。转完灰度后,背景和字符的灰度差往往不够明显,需要二值化。imbinarize默认用 Otsu 方法自动找阈值,对背景相对均匀的验证码效果不错;但如果背景有渐变或强噪点,Otsu 会把一部分噪点也当成前景。
% 读取验证码图像并转灰度 img = imread('captcha_001.png'); gray = rgb2gray(img); % 用 Otsu 自动阈值二值化,前景为白 bw = imbinarize(gray); bw = ~bw; % 反色,让字符为白色前景 % 如果 Otsu 效果差,改用固定阈值观察 % bw2 = gray < 120;逻辑说明:imbinarize返回的是 logical 矩阵,默认前景为黑(0)、背景为白(1),所以用~bw反色,让字符变成 1,方便后续bwlabel和区域统计。参数上,Otsu 不需要手动设阈值,适合快速验证;固定阈值适合你已知背景灰度范围的情况,比如背景集中在 200 以上、字符在 100 以下,那gray < 120会比 Otsu 更稳。判断标准很简单:二值化后字符笔画连续、没有大面积噪点块,就算合格。
2.2 去噪与干扰线抑制的实操顺序
二值化之后通常还有两类脏东西:孤立噪点和小连通域,以及细的干扰线。去噪顺序建议先做连通域面积过滤,再做形态学开运算。面积过滤能干掉大部分孤立噪点,开运算能断开细线但保留字符主体。
% 去除小于 30 像素的连通域 bw_clean = bwareaopen(bw, 30); % 用 2x2 矩形结构做开运算,断开细干扰线 se = strel('rectangle', [2 2]); bw_open = imopen(bw_clean, se); % 如果干扰线较粗,可改用腐蚀一次再膨胀一次 % bw_erode = imerode(bw_clean, strel('square', 2)); % bw_open = imdilate(bw_erode, strel('square', 2));逻辑说明:bwareaopen的第二个参数是面积阈值,设太小去不干净,设太大会把数字的「1」这种细笔画也删掉。经验值是字符笔画宽度的 1 到 2 倍面积,120×40 的验证码里数字笔画大概 20 到 40 像素,所以 30 是个安全起点。strel('rectangle', [2 2])比方形结构更温和,横向和纵向腐蚀程度一致,不容易把数字的拐角吃掉。开运算后如果字符出现断裂,说明结构元素太大,换回 2×2 或改用先腐蚀后膨胀各一次。
2.3 字符分割:连通域标记与投影法怎么选
分割是验证码识别里最容易翻车的一步。常见做法有两种:连通域标记和垂直投影。连通域标记适合字符之间不粘连的情况,bwlabel直接给每个字符区域编号,再按外接矩形裁剪;垂直投影适合字符宽度固定、间距均匀的验证码,对轻微粘连更鲁棒。
% 方法一:连通域标记 [L, num] = bwlabel(bw_open); stats = regionprops(L, 'BoundingBox', 'Area'); boxes = cat(1, stats.BoundingBox); % 按左边界排序,保证从左到右 [~, idx] = sort(boxes(:,1)); boxes = boxes(idx,:); % 方法二:垂直投影 col_sum = sum(bw_open, 1); threshold = max(col_sum) * 0.1; is_char = col_sum > threshold; % 找连续为 true 的区间作为字符列范围逻辑说明:bwlabel的num是连通域个数,如果它不等于验证码位数(比如四位数字却得到 6 个区域),说明有噪点没去干净或者字符断裂。regionprops拿到的BoundingBox是[x, y, width, height],按 x 排序后依次裁剪即可。垂直投影法里threshold取最大列和的 10% 到 20%,太低会把噪点列算进去,太高会漏掉笔画细的列。两种方法可以结合:先用连通域标记,如果区域数不对,再切到投影法兜底。
3. 特征提取与分类:模板匹配、SVM 和轻量 CNN 的取舍
分割出单个数字后,下一步是让机器判断它是 0 到 9 中的哪一个。这一步的选择直接决定系统复杂度和准确率。模板匹配最简单,SVM 居中,轻量 CNN 最重但上限最高。选哪个取决于你的样本量、字体一致性和对准确率的容忍度。
3.1 模板匹配:归一化互相关与像素差法
模板匹配的核心是拿每个分割出来的字符和 0 到 9 十个模板逐一比对,取最相似的那个作为结果。MATLAB 里可以用normxcorr2做归一化互相关,也可以直接算像素差的绝对值之和。前者对亮度变化更鲁棒,后者实现更简单。
% 假设 templates 是 1x10 的 cell,每个存一个 20x14 的二值模板 % char_img 是当前分割出的字符,统一 resize 到 20x14 char_norm = imresize(char_img, [20 14]); scores = zeros(1, 10); for k = 1:10 tmpl = templates{k}; % 归一化互相关,取最大值 cc = normxcorr2(tmpl, char_norm); scores(k) = max(cc(:)); end [~, pred] = max(scores); pred_label = pred - 1; % 映射回 0-9逻辑说明:normxcorr2返回的相关系数在 -1 到 1 之间,越接近 1 越相似。模板必须和待识别字符做同样的归一化尺寸,否则相关计算没有意义。imresize到固定尺寸这一步很关键,它把字符的绝对位置和大小差异抹掉,只保留形状信息。模板可以从训练样本里手工挑,也可以用同一字体渲染生成。如果验证码字体固定,模板匹配准确率能到 90% 以上;字体一变,掉到 60% 以下很正常。
3.2 SVM 分类:特征向量怎么构造
SVM 比模板匹配灵活,因为它不要求字符严格对齐,而是靠特征向量区分。对二值字符图,常用的特征有:HOG 方向梯度直方图、投影直方图、笔画密度。MATLAB 的fitcecoc可以直接做多分类 SVM,不需要自己写一对多逻辑。
% 提取 HOG 特征 features = extractHOGFeatures(char_norm, 'CellSize', [4 4]); % 训练多分类 SVM(假设 trainFeatures 是 N x D,trainLabels 是 N x 1) svmModel = fitcecoc(trainFeatures, trainLabels); % 预测 pred = predict(svmModel, features);逻辑说明:extractHOGFeatures的CellSize决定特征维度,[4 4]对 20×14 的小图来说比较合适,再小会过拟合,再大会丢细节。fitcecoc默认用一对一策略,10 个类别需要训练 45 个二分类器,样本少的时候训练很快。SVM 的优势是样本需求比 CNN 小,几百张图就能出效果;劣势是特征工程靠经验,HOG 参数调不好准确率上不去。
3.3 轻量 CNN:什么时候值得上深度学习
如果你的验证码字体变化大、有旋转和扭曲,模板匹配和 SVM 都会吃力,这时候可以考虑轻量 CNN。MATLAB 的 Deep Learning Toolbox 支持从零搭一个小网络,不需要预训练模型。网络结构可以很简单:两个卷积层加两个全连接层,输入 20×14 灰度图。
layers = [ imageInputLayer([20 14 1]) convolution2dLayer(3, 16, 'Padding', 'same') reluLayer maxPooling2dLayer(2, 'Stride', 2) convolution2dLayer(3, 32, 'Padding', 'same') reluLayer fullyConnectedLayer(10) softmaxLayer classificationLayer];逻辑说明:输入层尺寸必须和归一化后的字符图一致。两个卷积层分别用 16 和 32 个 3×3 卷积核,参数量很小,CPU 上也能训练。maxPooling2dLayer(2, 'Stride', 2)把特征图缩小一半,减少计算量。输出层 10 个神经元对应 0 到 9。这个网络在几千张样本上训练几十轮就能收敛,但样本少于 500 张时容易过拟合,需要加数据增强或者退回 SVM。
4. 避坑与排查:验证码识别里最容易翻车的五个地方
这一章记录的是我在实际跑验证码识别时反复遇到的坑,每一个都对应具体的现象、原因和解决方式。如果你按前面的流水线跑下来发现准确率不对,先对照这里排查。
4.1 二值化后字符断裂或消失
现象:二值化结果里数字笔画断成几截,或者干脆整块消失。原因通常是 Otsu 阈值被背景噪点拉偏,或者反色逻辑搞反了。解决:先imshow看二值图,确认前景是白色。如果阈值不对,改用固定阈值并手动扫一遍 80 到 180 的范围,找到字符最连续的那个值。如果字符本身灰度就接近背景,先做一次imadjust增强对比度再二值化。
4.2 连通域数量对不上验证码位数
现象:四位验证码分割出 3 个或 6 个区域。原因有三种:噪点没去干净、字符粘连、字符断裂。解决:先看bwareaopen的面积阈值是不是太小,调到 50 再试;如果还有粘连,用垂直投影法替代连通域标记;如果字符断裂,把开运算的结构元素从 2×2 降到 1×1,或者跳过开运算只做面积过滤。
4.3 模板匹配把 6 和 8、0 和 8 搞混
现象:识别结果里 6 和 8 频繁互换,0 和 8 也容易错。原因:这几个数字在低分辨率二值图里形状差异很小,模板匹配的像素级相似度区分不开。解决:把字符归一化尺寸从 20×14 提高到 28×20,保留更多形状细节;或者在特征里加入孔洞数量(8 有两个孔,6 和 0 各一个),用bweuler算欧拉数作为辅助判据。
4.4 SVM 训练准确率高但测试准确率低
现象:训练集上 98%,测试集上 70%。原因:样本量太少或者特征维度太高导致过拟合。解决:先减特征,把 HOG 的CellSize从[4 4]调到[8 8],特征维度降一半;再扩样本,对训练图做轻微平移和旋转生成额外样本;最后检查训练集和测试集是不是来自同一批验证码,如果字体不同,SVM 泛化不了是正常的。
4.5 批量处理时内存溢出或速度骤降
现象:单张图跑得很快,几百张一起跑就卡死或报内存不足。原因:每次循环都重新读模板或者没有释放中间变量。解决:把模板在循环外加载一次,循环内只做匹配;用clear清理大矩阵;如果图像数量上千,改成parfor并行,但注意并行里不要写文件冲突。
5. 从单张调试到批量验证:一套可复用的评估脚本
前面四章把流水线搭起来了,但真正决定这套系统能不能用的,是批量跑一遍之后的统计结果。单张图调通不代表整体可靠,你需要一个评估脚本告诉你准确率、错误分布和失败样本长什么样。这一章给出一套可以直接改的评估流程,以及我在长期跑验证码时养成的两个习惯。
5.1 批量测试与混淆矩阵输出
评估脚本的核心逻辑是:遍历测试集,对每张图跑完整流水线,记录预测值和真实标签,最后算准确率并画混淆矩阵。MATLAB 的confusionmat和confusionchart可以直接用。
% 假设 testImages 是文件路径列表,testLabels 是对应真实数字 numTest = length(testImages); predLabels = zeros(numTest, 1); trueLabels = zeros(numTest, 1); for i = 1:numTest img = imread(testImages{i}); % 调用前面封装好的识别函数 predLabels(i) = recognize_captcha(img, templates, svmModel); trueLabels(i) = testLabels(i); end % 准确率 acc = sum(predLabels == trueLabels) / numTest; fprintf('整体准确率: %.2f%%\n', acc * 100); % 混淆矩阵 cm = confusionmat(trueLabels, predLabels); confusionchart(cm);逻辑说明:recognize_captcha是你自己封装的函数,内部走完灰度化、二值化、去噪、分割、分类全流程。confusionmat的第一个参数是真实标签,第二个是预测标签,顺序不能反。confusionchart画出来的热力图能一眼看出哪两个数字最容易混。如果某个数字的召回率明显低,回去看它的分割结果,大概率是分割阶段就错了,而不是分类器的问题。
5.2 失败样本的归因方法
准确率数字本身不说明问题,关键是知道错在哪。我一般会把预测错误的样本单独存到一个文件夹,按「真实标签_预测标签」命名,比如3_8.png。跑完一批后打开这个文件夹,快速扫一眼就能归类:是二值化把字符吃掉了,还是分割多切了一块,还是分类器把 3 认成 8。归因之后针对性调参,比盲目换分类器有效得多。
% 保存失败样本 failDir = 'fail_cases'; if ~exist(failDir, 'dir') mkdir(failDir); end for i = 1:numTest if predLabels(i) ~= trueLabels(i) img = imread(testImages{i}); fname = fullfile(failDir, sprintf('%d_%d.png', trueLabels(i), predLabels(i))); imwrite(img, fname); end end逻辑说明:文件名用真实标签在前、预测标签在后,方便排序和统计。mkdir前先判断目录是否存在,避免重复创建报错。这个习惯看起来简单,但能省掉大量「到底哪里错了」的猜测时间。
5.3 一个提高鲁棒性的小技巧:多模板投票
如果你的验证码字体有轻微变化,单一模板容易在边界样本上翻车。我常用的补救办法是每个数字准备 3 到 5 个模板,分别来自不同样本或不同渲染参数,识别时让所有模板投票,得票最多的作为最终结果。这个改动代码量很小,但对手写体或字体混用的验证码,准确率能提 5 到 10 个百分点。
% templates 是 10xK 的 cell,每行对应一个数字的 K 个模板 votes = zeros(1, 10); for d = 0:9 for k = 1:size(templates, 2) tmpl = templates{d+1, k}; cc = normxcorr2(tmpl, char_norm); if max(cc(:)) > 0.6 % 相似度阈值 votes(d+1) = votes(d+1) + 1; end end end [~, pred] = max(votes); pred_label = pred - 1;逻辑说明:templates从一维 cell 变成二维,每行是一个数字的多个模板。相似度阈值 0.6 是经验值,低于这个值说明模板和字符形状差太远,不投票。投票法比取最大相似度更稳,因为它不依赖单个模板的绝对分数,而是看哪个数字被更多模板认可。代价是计算量翻几倍,但验证码识别本身不追求实时,这点开销可以接受。
这套系统我从单张调试跑到批量评估,最大的教训是:预处理阶段多花一小时,分类阶段就能少调一天。很多人一上来就纠结用 SVM 还是 CNN,结果二值化都没做干净,换什么分类器都救不回来。先把二值化和分割的中间结果imshow出来看,确认每一步都符合预期,再往下走。希望帮到你。
本文还有配套的精品资源,点击获取