
做验证码识别这个方向大多数人的第一反应是“这玩意儿早过时了随便一个深度学习模型不都能到99%”。但真正上手干一次就知道尤其是中英文混合、带干扰、字符粘连的验证码想稳定跑在线上的批量流程里坑远比你想象的要多。我最近在做一个发票查验自动化项目时就需要把一批历史票据批量录入查验系统而卡在入口的就是一个4位混合验证码中文、英文、数字混在一起背景还有噪点和干扰线。我把这套识别方案完整做了下来最终整串识别率稳定在95.2%。这篇文章不吹不黑把设计思路、数据准备、模型选型、工程化部署和踩过的坑全部复盘一遍。如果你也在做RPA机器人、批量发票验真或者正好在研究验证码识别技术这篇文章里的经验可以直接拿去做参照。我不建议你照抄代码但思路和避坑点绝对能帮你少走弯路。1. 项目背景与整体设计思路1.1 业务场景驱动的技术选型先说清楚业务背景。发票查验在很多企业里是一项高频、重复、量大的操作尤其是财务共享中心每天要处理几千张报销单据每一张单据都需要核对发票号、金额、开票日期等信息。很多企业会采购OCR识别服务先把纸质发票转成结构化数据但这一步通常只能覆盖票面信息最终的“真伪核验”还是要落到查验平台里。而查验平台在返回结果前几乎都会要求输入验证码。这个环节如果靠人肉输入一是效率低二是容易疲劳出错。我算过一笔账单张验证码人工识别加输入平均需要3到5秒处理1000张就是1小时左右如果遇到验证码渲染得特别模糊重试几次之后时间成本直接翻倍。我接到的需求就是把这个环节自动化让程序自动识别验证码并回填到自动化脚本里。技术上的难点在于目标验证码不是单纯的数字验证码而是中英文混合。常见形态是4个字符可能是数字、小写英文、大写英文、中文字符的组合背景带彩色噪点和干扰线字符之间有时粘连偶尔还有轻微旋转。这样的验证码传统OCR库基本束手无策必须针对目标样式去定制识别方案。1.2 中英文混合验证码的识别难点中英文混合验证码比纯数字或纯英文验证码难在几个地方。第一是字符类别数量暴涨。纯数字只有10个类纯英文大小写加数字也就62个类。但一旦加入中文类别数量可能飙到几百。字符类别越多分类器的最后一层输出维度越大训练难度和所需数据量都会成倍增长。第二是相似字符混淆问题。数字“0”和英文大写“O”、小写“o”在图片上几乎一模一样数字“1”跟小写“l”、大写“I”也高度相似英文“8”和数字“8”更是无法区分。中文里“已/己/巳”、“日/曰”、“人/入”这样的形近字人眼都容易看走眼模型配合后处理规则都未必能完全兜住。第三是字符粘连和形变。验证码为了让OCR失败通常会在字符间距上做文章有的字符贴得很近有的旋转一定角度有的还会叠加干扰线把字符笔画连起来。这种样式对传统字符分割算法非常不友好分割环节一旦出错后面识别再准也没用。我在项目前期用Tesseract做过一个快速测试在30张真实样本上跑了一遍最终只对了9张准确率30%。不是说Tesseract不行而是它本身面向的是自然场景文本或打印体文字对验证码这种刻意“对抗”的场景几乎没有抵抗力。所以从第一版开始我就没有把Tesseract列为可选方案。1.3 两条技术路线怎么选验证码识别的主流技术路线有两条分割单字加分类模型以及端到端序列识别模型。分割加分类的思路比较直观。先把整张验证码图片切分成单个字符的小图然后每个小图送进一个CNN分类器识别出单个字符最后按顺序拼接成完整结果。这条路线的优点是逻辑清晰每步都好调试某个字符识别错了一眼就能看出来。数据也好造只要把字符位置标出来裁剪成小图就能训练分类器。缺点是分割环节太脆弱遇到字符粘连、旋转角度大、中文宽度变化大的情况很容易切错。端到端序列识别常见实现是CRNN加CTC。输入整张验证码图片网络自动学习字符之间的时序关系直接输出整串字符。优点是不需要单独做字符切分对粘连和变形更加鲁棒缺点是需要更多训练数据训练时间更长模型文件也更大。尤其当字符集里包含中文时数据需求量会进一步上升。我最终选择的是分割加分类为主针对难分割样本再做后处理兜底。为什么这么选因为目标验证码虽然带干扰但字符主体并没有粘连到看不清的程度通过连通域分析和垂直投影可以解决八成以上的分割问题。剩下的二成可以用数据增强和识别阶段的反馈校正来兜底。在训练数据只有5万张的情况下这个方案的综合准确率反而更好控制。这里我也想给你一个判断标准如果你要处理的验证码是任意长度、字符随意旋转、背景极度复杂别犹豫直接上CRNN那套端到端方案。如果你的验证码长度基本固定字符间距只是“偶尔贴在一起”那么分割加分类是性价比更高的选择。1.4 为什么说分割分类在大多数情况下够用很多人一说验证码识别就默认要上CRNN、Transformer觉得越“重”越好。但我做了那么多OCR项目之后发现工程上最重要的是在数据量和业务约束下找到最可控的方案。分割加分类这套老派做法优势在于每一步的置信度都可解释、可度量。比如分割结束之后我可以拿到每个字符候选框的宽度、高度、左右间距这些信息都能用来判断当前切分是否可靠。如果某个字符候选框的宽度只有平均宽度的三分之一基本可以断定是误切需要合并相邻候选框重新识别。这样的错误反馈机制在端到端模型里就很难实现。另外分割加分类方案的模型非常轻量CPU上单张图片推理不超过20毫秒模型文件也就几MB。在批量处理场景下这种轻量方案可以直接跑在普通服务器上不需要上GPU部署成本低很多。2. 数据准备与预处理实战2.1 样本怎么来在验证码识别项目里数据问题是先于模型问题的。最理想的情况是直接从目标平台拿到大量历史验证码图片但在实际操作中这种做法既不合规也不现实。平台方通常有访问频率限制短时间内大量抓取很容易触发风控还可能影响正常业务访问。我采取的是“少量真实样本分析加合成数据训练”的组合方案。项目启动时业务方提供了3000张历史真实验证码图片用于分析这些图片是从合作渠道拿到的授权样本。我先用它们做统计分析搞清楚字符集、字符数量、干扰线风格、背景颜色分布等关键信息然后再写一个合成数据生成器把分析出来的特征全部参数化批量生成训练数据。这里必须强调一个原则任何数据使用都要以授权和合规为前提。如果业务场景不允许获得真实样本那就只能通过仔细分析截图人工归纳视觉特征来生成合成数据。但合成数据与实际数据之间的差距最终一定会反映在准确率上所以尽可能争取合法授权的真实样本做验证集是很有价值的。2.2 图像预处理步骤拿到原始验证码图片后我不会直接送进模型而是先做几步标准化预处理。第一步是灰度化。验证码的背景和字符通常颜色差异明显灰度化之后字符和背景的亮度对比就凸显出来了。标准公式是Gray等于0.299乘R加0.587乘G加0.114乘BOpenCV里用cvtColor一行就能搞定。第二步是二值化。我试了固定阈值和大津法最终固定用大津法。大津法会自动计算一个最优阈值让前景和背景的类间方差最大对验证码这种前景背景对比明显的图片非常有效。如果遇到光照不均的情况大津法效果会变差这时候可以改用自适应阈值。第三步是去噪。验证码上经常有孤立的噪点我的做法是先做一次中值滤波把细颗粒噪点抹掉再做一次形态学开运算把干扰线断开成碎段。这一步需要控制强度滤波核太大或开运算迭代次数太多会把字符的细笔画也腐蚀掉。我调参下来中值滤波核大小设为3开运算用3乘3矩形结构元素迭代1次效果最稳。超过1次部分中文字体的横竖笔画就开始断裂反而增加了识别难度。第四步是倾斜校正。如果整张图片存在全局倾斜可以用最小外接矩形或霍夫变换估算倾斜角度再做仿射变换。我遇到的目标验证码整体倾斜不严重更多是单个字符微转所以这一步没有在全局做而是放在切分后的单字上做局部校正。2.3 字符切分经验字符切分是整个项目里最磨人的环节没有之一。我用的是“两步走”策略先用连通域分析和垂直投影做初步切分再对异常候选块做二次精细切分。连通域方法适合字符之间有清晰间隔的情况。用OpenCV的findContours拿到每个连通区域的外接矩形然后根据字符的宽度和高度过滤掉明显太小或太大的块。但中文字符有个麻烦笔画经常断成多个连通域比如“口”字可能被干扰线隔断成几块。所以我会加一个合并逻辑如果两个连通域在垂直方向上有足够大的重叠而且水平距离很近就把它们合并成同一个字符候选块。垂直投影方法则是把二值图按列求和连续的非零段就对应一个字符。这个方法对单个字符投影连续的情况很有效但遇到字符笔画在垂直方向上有断裂或者两个字符贴得太近就会出错。我的做法是把两种方法的结果交叉验证先用连通域找到候选块再用垂直投影做参考如果两者结论不一致就进入二次精细切分流程。二次精细切分主要处理两类情况一是候选块宽度远超平均宽度可能包含了两个字符二是候选块宽度过窄可能是某个字符被拦腰截断。遇到宽度过宽的块我会尝试在垂直投影的“鞍点”位置寻找切割线遇到宽度过窄的块我会去检查它左右相邻的候选块判断是否需要合并。这套逻辑写起来不复杂但非常依赖对目标验证码字符宽度分布的统计所以前期切分参数的统计工作一定要做扎实。2.4 合成数据生成技巧合成数据生成器的核心是“还原真实感”。我一开始生成的合成图太过干净模型在真实样本上表现惨不忍睹后来才意识到问题出在数据分布不匹配上。合成器里的随机项包括背景色块的填充范围、字符旋转角度、字体种类、字符间距、干扰线的数量和颜色、噪点的密度。我参考真实样本统计把旋转角设置为负15度到正15度之间均匀采样字符间距设置成“偶尔贴近、偶尔分开”的分布干扰线控制在1到3条颜色选择与背景相近但又有细微差别。中文字符从一个常用汉字字形库中随机抽取英文和数字按真实样本中的频率分布抽样。另一个关键点是字符位置标注。合成器除了输出图片还输出一个JSON文件记录整串字符结果以及每个字符在像素坐标中的位置框。有了位置框训练分割模型或评估分割效果就非常方便。我可以直接根据位置框把真实字符区域裁剪出来做单字分类器训练不需要额外的人工标注。训练集最终生成了5万张合成图片验证集则混合了真实授权样本和合成样本。这个配比下模型在合成集上的整串准确率能达到95.2%在真实样本上的准确率初期只有92.8%之后我往训练集里加了10%的真实样本做finetune真实样本准确率才提升到95.5%。3. 识别模型实现与调优3.1 字符分类模型设计切分完成后每个字符小图被统一缩放到32乘32像素的灰度图然后送入分类器。字符类别集合包含数字0到9、英文大小写共52类再加上目标验证码中出现过的中文字符最后类别总数是362类。模型输出层是362个神经元的Softmax分布。模型结构走的是轻量路线。第一个卷积层32个3乘3卷积核ReLU激活接2乘2最大池化第二个卷积层64个3乘3卷积核ReLU激活再接2乘2最大池化然后展平接128个神经元的全连接层加了0.5的Dropout防止过拟合最后是362个神经元的输出层。整个模型参数量不大CPU推理速度非常快。训练时用交叉熵损失Adam优化器初始学习率0.001。我设置了一个学习率调度策略每5个epoch如果验证集loss没有下降学习率就乘以0.5。训练到第20轮左右loss基本收敛单字符识别准确率在99%上下。这里有个经验值得说比起模型结构输入字符图的归一化方式对准确率影响更大。如果直接把字符小图拉伸成32乘32会改变原有的长宽比中文和一些窄字符就会变形。更稳的做法是先把字符按原比例缩放到宽高不超过32像素然后填充到32乘32的灰色画布中央。这个细节帮我提升了约1个百分点的整串准确率。3.2 端到端方案的对比实验为了确认分割加分类方案确实是最佳选择我也跑了一版CRNN加CTC作为对照组。CRNN的特征提取部分用了类似VGG16的前几层卷积时序部分接双向LSTM最后用CTC做序列对齐。图片宽度输入固定为160像素高度按比例缩放。在5万张合成数据上训练CRNN的整串准确率到94%左右比分割加分类方案略低一点。原因主要还是中文字符的类别数量多每个中文字符分到的训练样本相对稀缺。如果数据量扩充到20万张CRNN大概率能反超因为端到端模型对字符间时序关系的建模能力更强。但工程上线不只看准确率。CRNN训练时间大约是分割方案的三倍模型文件大一个量级CPU推理速度也慢不少。在批量发票查验的场景里我需要在普通服务器上并发处理图片分割加分类方案的推理延迟优势很明显。所以最终上线用的是分割加分类方案CRNN作为后续数据量充足时的备选升级方案。3.3 打破95%瓶颈的调优点从90%整串准确率提升到95.2%我没有改模型结构而是靠三个细节的叠加。第一是数据增强。合成数据一开始太“干净”模型在含噪真实样本上总是掉点。后来加入了高斯模糊、透视畸变、随机亮度扰动、随机擦除笔画等策略测试集准确率直接涨了2.5个百分点。数据增强对验证码识别项目来说永远是性价比最高的投入。第二是后处理规则。我根据字符混淆矩阵建了一张映射表。模型输出“0”“O”“o”且上下文都是数字时统一映射成“0”。模型输出“1”“l”“I”时如果该位置在目标验证码中只可能出现数字就优先映射成“1”。类似的规则还可以灵活配置每次验证码样式改版更新映射表就行不用重新训练模型。第三是切分置信度反馈。让切分模块输出每个字符候选框的置信度如果某一段置信度很低就把它和相邻段合并重新送进识别器。这一步能让“切分错误”不至于直接“一票否决”显著提升了整串容错率。3.4 测试与评估口径在公布准确率之前必须先把口径说清楚。这个项目里的95.2%指的是整串验证码完全识别正确的比例也就是一张图片全部字符都识别对才算正确任何一个字符错了都算整张失败。如果按单字符识别准确率算362类上的准确率是98.7%。这两个数字差距很大的原因在于整串准确率是单字符准确率的乘积式累积4个字符的整串准确率约等于单字符准确率的四次方所以单字符哪怕到99%整串也只是96%左右。我在测试集上放了5000张合成图片和3000张真实授权样本合成集上整串准确率95.2%真实样本上92.8%。后续用10%真实样本finetune之后真实样本准确率提升到95.5%并保持稳定。建议你在做类似项目时也把合成测试集和真实测试集分开评估毕竟真实场景的数据分布才真正决定上线效果。4. 工程化部署与问题排查4.1 把模型封装成可调用服务模型训好之后要变成稳定的服务才能被自动化流程调用。我用Flask封装了一个HTTP接口输入base64编码的图片输出识别结果字符串。接口内部按顺序执行图片解码、预处理、字符切分、单字识别、后处理、结果返回。服务部署有一个容易踩的坑多个线程共享同一个模型session时会出现推理结果不一致甚至进程崩溃的问题。我采用的方式是服务启动时预加载模型每个worker进程持有独立的session用gunicorn启动4个worker进程。单张图片平均响应时间35毫秒200并发压测下P99在150毫秒以内满足业务需求。如果你不想引入Flask也可以把模型转成ONNX格式在Python脚本里直接用ONNX Runtime推理。ONNX Runtime对CPU推理做了很多优化比PyTorch原生模式快不少。模型转ONNX时要注意固定输入尺寸和动态轴参数否则转换出来的模型在输入尺寸变化时会报错。4.2 关于“按键精灵”类自动化工具的使用边界关键词里提到“按键精灵手机版验证码识别点击”这个我必须专门说几句。按键精灵这类自动化工具本质上是模拟屏幕坐标点击和键盘输入在很多RPA场景里确实方便用来做验证码识别后的自动提交也算常见做法。但在国内做自动化业务合规性永远是第一位的。验证码设置的初衷就是区分人和机器任何绕过验证码的自动化操作都应该先确认自己是否获得了平台方的授权。企业内部的发票查验如果已经和查验服务方建立了合作关系通常可以通过官方接口完成数据交换压根不需要模拟点击。模拟点击的稳定性也很差窗口位置、分辨率、DPI缩放都会导致坐标偏移我见过太多因为分辨率不同导致点击失效的案例。如果确实需要用到验证码识别加自动化点击建议把识别服务化用脚本调用识别接口拿到结果再通过可配置的点击框架去执行而不是把识别逻辑直接写死在按键精灵脚本里。同时一定要控制请求频率设置合理的随机间隔避免对目标服务造成压力。4.3 常见问题排查速查表现象可能原因排查方法整串识别率骤降验证码样式改版重新收集真实样本更新数据生成参数中文汉字频繁识别错该类字符训练样本不足增加该字符在合成数据中的采样权重图片重影、笔画糊掉预处理增强过度调小中值滤波核开运算迭代次数降回1字符切得过多或过少投影/连通域对粘连失效融合两种切分结果增加滑窗重切逻辑模型推理延迟高模型过大或CPU性能不足转ONNX使用4bit量化或改用更轻量网络并发请求时服务崩溃多线程共享模型session改为多进程部署每进程独立session真实样本准确率低于测试合成数据与真实分布偏差过大在训练集中加入10%真实样本finetune4.4 两个容易忽略的细节第一个细节是失败样本的记录。我在上线流程里加了一个日志模块每次识别失败都会把原始图片、识别结果、每个字符的置信度一起存下来。这些失败样本是优化模型最宝贵的生产资料每周做一次错误分类分析往往比盲目调参更快见效。常见的失败模式包括某类中文字符系统性出错、某种干扰线样式导致切分错乱等针对性地补数据比随机加数据高效得多。第二个细节是模型和参数的版本管理。模型文件、预处理参数、字符映射表、后处理规则、合成数据生成器的版本全都要纳入版本管理。每次实验打上标签记录实验目的、数据配比、最终指标。模型迭代时用A/B对比的方式上线不要让新模型直接覆盖旧模型。否则过阵子发现线上效果回退你连“上一版为什么好用”都查不出来。5. 一些真心话如果让我重做一遍这个项目我会把更多时间放在数据生产和切分边界处理上而不是在模型结构上反复折腾。验证码识别的核心不是网络有多深而是数据处理有多贴近目标场景。模型结构即便换到ResNet、EfficientNet在数据分布不匹配的情况下收益也很有限。合规这件事必须拎清。不管技术多有挑战性数据来源和使用方式都必须符合规则。自动化本身没有错但用在哪里、怎么用边界要明白。我这次是从合作渠道拿到授权样本训练和测试都在合规范围内进行。还有一个小技巧测量字符位置时最好把每个字符的最小外接旋转矩形也一并存储下来。后面无论是做旋转校正、画错误分析可视化图还是训练分割网络都会频繁用到。这个细节我是做到项目后期才补上的补数据的过程浪费了不少时间希望你一开始就把这个字段设计进去。验证码识别这个方向说起来不算新鲜但真正把一套方案稳定跑进业务流程考验的是数据、切分、模型、工程的全链路功底。希望这篇文章能帮你少踩几个坑。