十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PyTorch点选验证码识别:DBNet+CRNN+CTC完整方案

PyTorch点选验证码识别:DBNet+CRNN+CTC完整方案 简介本资源是一套基于PyTorch实现的中文文字点选、选字及选择式验证码识别完整方案面向深度学习初学者与OCR应用开发者聚焦中文场景下的文字检测与端到端识别难题适用于登录安全验证、自动化测试、爬虫绕过等实际业务需求。压缩包共33个文件涵盖20个核心Python脚本含模型训练、推理服务、数据生成与可视化模块、3个ONNX模型YOLO文字定位、CRNN与CNN识别分支、3张效果演示图及GIF动图、2份字符集与说明文本整体146.78MB目录结构清晰分层含service.py轻量API服务、model子目录封装多模型、src/captcha.py定制化验证码生成逻辑。已有2485人学习下载提供从数据预处理、YOLOCRNN联合建模、CTC解码到Flask/Gunicorn部署的全流程可运行代码附带README.md说明与requirements.txt依赖清单开箱即用便于复现、调试与二次开发。 验证码识别圈子里文字点选一直是个绕不过去的技术坎。和纯数字、滑块拖拽这类验证码不同文字点选需要模型真正理解图片里的汉字是什么、在哪个位置——我经常和做自动化测试的朋友开玩笑这玩意儿考的不是程序是AI的语文能力。这篇文章要分享的是一套用PyTorch实现文字点选/选字类验证码识别的完整方案核心链路是DBNet做中文字检测CRNNCTC做中文识别再串一个文本匹配模块决定点击顺序。项目已经整理成一份可跑的zip包目录、脚本、权重都在里面。如果你正好在研究OCR选字或者在做合规授权范围内的自动化测试、图像文字检测识别这份内容可以直接参考。1. 点选验证码到底在考什么拆成“检测识别匹配”三个子问题1.1 一个容易被低估的问题点选验证码的界面很常见一张自然风景图或插画背景上随机散布着十几个汉字或词语题目区域写着“请依次点击花朵小鸟”。用户需要按顺序点击图中对应位置才能通过。选字版的形态略有不同通常是题目给出单个汉字图片里散布多个文字用户点击目标字。初看好像只是“看懂文字并找到位置”但真正实现自动化后才发现这比识别一行验证码字符串复杂得多。关键区别在于普通字符验证码的答案是“一串文本”而点选验证码的答案是“一组坐标”。模型不仅要识别出图片里的每个文字内容还要给出这些内容在图上的具体位置并且按照题目顺序排列。换句话说这是一个文字检测 文字识别 语义匹配的组合任务。任何一个环节出错最终点击坐标就是错的。1.2 三个核心子任务我把整套系统拆成三个独立但串联的子问题这也是项目里三个训练阶段对应的目标。第一是文字检测。需要从背景复杂的图片中找出所有文字的位置输出文本框。常见的做法是以文本检测模型为核心检测框可以带旋转角度因为验证码里的文字经常不是完全水平的。这一步要解决的核心难点是背景干扰和艺术字体。第二是文字识别。把检测到的每个文本框裁剪出来送进识别模型输出对应的中文字符串。考虑到中文常用字有3500个左右这本质上是一个类别数量很大的序列识别问题。第三是题目匹配与坐标输出。将识别结果与题目给出的词语一一对应输出点击顺序。匹配阶段看似简单实际上因为OCR结果存在错别字、漏字、同音字等情况需要做容错处理。这套拆分的最大好处是每部分都可以独立训练、独立调优、独立替换。检测模型换了不影响识别模型识别模型升级也不必重训检测模型。项目维护起来很省心。1.3 为什么这套链路可行如果你搜过相关方案会发现很多验证码识别项目直接端到端做“图到坐标”的回归这种思路理论上很美实际效果很不稳定。点选文字的分布随时变化一张图上可能有6个字也可能有18个字端到端模型很难输出动态数量的坐标。而“检测 识别 匹配”是业界验证过最稳的组合每一步都有成熟的工具和预训练资源可以辅助。PyTorch在这条链路上的生态很友好。DBNet、CRNN这些经典模型都有开源实现数据加载、分布式训练、ONNX导出全套成熟社区里遇到问题基本都能搜到。这也是我把整个项目整理成zip分享出来的原因——照着目录结构就能把检测、识别、匹配三段分别训练起来最后合成完整推理脚本。2. 训练数据从哪来合成中文点选图的完整流程2.1 先合成再微调数据路径的选择点选验证码识别项目最大的工程量不是模型是数据。真实验证码样本不好大量获取要么找平台购买要么自己写采集脚本但批量标注成本极高。一个更现实的路径是先用合成数据把模型的基础能力训练起来再用少量真实样本微调让模型适配真实分布。合成数据的优势有三个标注精确、数量无限、背景可控。点选验证码的检测框坐标和文字标签本来就需要像素级精确人工标注还得四点多边形费时费力且容易标歪。合成方案里文字渲染到哪个位置、旋转多少度都由代码记录天然就是标准标注。2.2 合成阶段怎么做到“像那么回事”很多人在合成数据上吃过亏生成出来的图太干净模型在合成集上跑得很好一上真实验证码就崩。我的经验是合成不能只追求“能用”而要尽量覆盖真实场景里的各种噪声。背景方面我从网上收集了大量自然风景图、室内图、纯色渐变图还有带噪点的纹理图。字体方面固定的宋体黑体远远不够真实验证码经常用楷体、幼圆、仿宋甚至艺术字体我准备了十几套中文字体在渲染时随机选择。形态方面每个文字随机旋转正负15度随机缩放颜色随机从色板中抽取并且有意加入一部分低对比度字色模拟文字和背景融合的情况。文字数量控制在每张图8到16个字位置在中心区域均匀撒开避免两个文字重叠太严重。最后加上干扰线、高斯噪声、局部模糊和JPEG压缩模拟截图上传后的画质损失。这样合成的图像模型在视觉上很难一眼分辨出真假。2.3 标注格式与读取方式数据集标注格式沿用文本检测常见的JSON结构每张图对应一个记录包含图片路径、尺寸、文本框四点坐标和文本内容。{ image: sample_0001.jpg, height: 640, width: 640, texts: [ { box: [[120, 200], [180, 200], [185, 260], [125, 260]], label: 花朵 }, { box: [[300, 350], [380, 345], [385, 410], [305, 415]], label: 小鸟 } ] }这里有一点要注意四点坐标的顺序必须固定。我习惯按左上、右上、右下、左下的顺序存储。虽然DBNet的后处理对点的顺序不是特别敏感但后续做数据增强、可视化、转旋转框时顺序统一能省掉很多Debug时间。2.4 数据增强的取舍检测任务的数据增强我用了随机透视变换、亮度对比度扰动、高斯模糊、通道偏移和JPEG压缩。识别任务则对裁剪出来的文字块做随机缩放、加窄边、色域扰动。增强强度要克制尤其是中文识别这一路过度扭曲会影响字符结构比如把“口”拉成椭圆模型就会产生大量误识别。提示合成数据时别只渲染题目目标词还要渲染大量干扰词。这样在训练识别模型的同时也等于给匹配模块提供了天然的负样本模型学会了区分“图中有什么”和“题目要什么”。3. 检测端选型为什么是DBNet而不是EAST或PSENet3.1 三种文本检测方案的取舍做中文文本检测绕不开EAST、PSENet、DBNet这三个经典方案。它们各有特点我简单对比一下模型核心思路优势劣势EAST直接回归文本框结构简单推理快长文本和旋转文本表现一般PSENet渐进式扩展分割区域能处理粘连文本后处理复杂速度偏慢DBNet分割 可微分二值化精度速度平衡后处理简单对极端弯曲文本支持一般点选验证码的文字特征是数量不多但分布散、存在旋转和艺术字体很少有粘连成整行的情况。PSENet的强项在这种场景里发挥不出来反而增加了后处理负担EAST对旋转框和低对比度文字的鲁棒性不如分割类模型DBNet的检测精度和推理速度最均衡训练收敛也快。项目里我最终选了DBNet。3.2 DBNet的可微分二值化原理DBNet最核心的贡献是把传统分割网络中的固定阈值二值化操作改成了可微分版本。在训练时网络的输出有三张图概率图、阈值图、近似二值图。概率图负责预测每个像素属于文字区域的概率阈值图负责预测每个位置的最佳二值化阈值近似二值图则用下面这个公式生成B 1 / (1 exp(-k * (P - T)))其中P是概率图预测值T是阈值图预测值k是放大因子一般取50。这样二值化操作参与到了前向传播中梯度可以回传到阈值图的预测分支模型自动学会在不同亮度、不同对比度的区域采用合适的阈值。这个设计在实际验证码场景里很有用。真实点选图的背景常常有渐变色照明不均固定阈值分割容易把文字和背景搅在一起。DBNet的自适应阈值机制让低对比度文字的召回率明显提升。3.3 训练配置与loss设置我的DBNet配置是backbone用ResNet18输入尺寸640x640neck用FPN。选用ResNet18而不是ResNet50是因为验证码文字数量少ResNet18的特征表达能力足够显存占用和推理速度都更好。训练损失由三部分构成概率图的二值交叉熵损失、阈值图的L1损失、近似二值图的二值交叉熵损失。总损失公式是L L_binary 1.0 * L_threshold 1.0 * L_probability优化器用Adam初始学习率1e-4学习率衰减用step decay每20个epoch乘以0.1。合成数据大概训练60个epoch后精度就不再明显提升。模型定义大概是这个结构import torch.nn as nn class DBNet(nn.Module): def __init__(self, backboneresnet18): super().__init__() # 实际项目中会用ResNet FPN结构 self.backbone ResNet(backbone) self.fpn FPN(in_channels[64, 128, 256, 512]) self.head DBHead(inner_channels128) def forward(self, x): features self.backbone(x) f1, f2, f3, f4 self.fpn(features) pred_map, thresh_map, binary_map self.head(f1, f2, f3, f4) return pred_map, thresh_map, binary_map训练时损失函数直接拿这三张输出图和对应的标签计算即可。GT生成方面概率图的标签由原始文本框向内收缩得到收缩比例我习惯设为0.4阈值图的标签则是原始框向外扩张后的区域和收缩后区域的差集。这些细节在DBNet原论文里都有照着实现就行。3.4 后处理从概率图到旋转框推理阶段对概率图用固定阈值做二值化然后提取连通域每个连通域用最小外接矩形来得到文本框。因为验证码的文字通常不是水平排列用旋转框能更准确地圈住文字点击坐标取自旋转框中心。import cv2 import numpy as np def boxes_from_binary_map(binary_map, min_area50): contours, _ cv2.findContours(binary_map, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) results [] for cnt in contours: area cv2.contourArea(cnt) if area min_area: continue rect cv2.minAreaRect(cnt) center, size, angle rect results.append((center, size, angle)) return results这里有一个经验cv2.minAreaRect返回的角度范围是-90到0度如果后面要计算旋转矩形的四个顶点需要自己写转换函数不能直接用返回值里的angle去做仿射变换。很多刚上手的人在这块被绕晕调试半天发现框转歪了。4. 识别端踩坑CRNNCTC处理中文时的调参细节4.1 为什么用CRNN而不是Transformer识别模型我选了经典的CRNN加CTC方案。点选验证码的每个目标是单字或短词长度不固定CRNN天然适合变长序列识别而且对数据量的要求比Transformer低很多。基于Transformer的OCR模型比如ViT加解码器的结构准确率上限确实更高但需要大量数据才能训出来推理占用也更大。在验证码这种字符数量少但样式杂的任务里CRNN已经能把准确率做得很好。4.2 中文字表设计和序列建模CRNN的序列输出和字表设计直接相关。中文字表我没有全部塞进去而是选了常用汉字3500个左右加上数字、英文字母、常见部首和少量标点总共4000多类。这里面有个细节验证码图片里的词有很强的高频倾向比如“花、草、鸟、山、水、人、车、马、云、风”这类名词我把这些字单独加进字表并适当在合成数据里提高出现频率。网络结构是CNN主干提取特征然后接双向LSTM建模序列最后通过全连接层输出每个时间步在每个字符类别上的概率。CRNN的输入高度固定为32宽度按比例缩放最长控制在256以内。对于点选验证码裁剪出来的短文本块宽度经常只有几十像素时间步数少序列建模压力不大。4.3 PyTorch里CTC Loss的用法PyTorch的nn.CTCLoss用起来不太直观特别是targets参数要求把每个样本的标签序列拼成一维数组还必须按batch顺序排列。我写了个工具函数专门做这个转换核心步骤是先生成target_lengths再按顺序拼接最后把输入log_probs换成[时间步数, batch, num_classes]的格式。import torch import torch.nn as nn criterion nn.CTCLoss(blank0, zero_infinityTrue) # log_probs: [T, N, C] # targets: 一维拼起来的标签序列 # input_lengths: [N], 每个样本的时间步数 # target_lengths: [N], 每个样本的标签长度 loss criterion(log_probs, targets, input_lengths, target_lengths)blank默认是0所以字表里第一个位置必须是空白符。zero_infinityTrue很重要遇到长度异常的样本导致loss为无穷时它会自动置零避免训练直接崩掉。推理阶段用贪心解码就够了取每个时间步最大概率的类别然后合并连续重复字符、删除空白字符。def ctc_decode(log_probs): # log_probs: [T, batch, num_classes] preds log_probs.argmax(dim-1) # [T, batch] texts [] for seq in preds.transpose(0, 1): chars [] prev None for idx in seq.tolist(): if idx ! 0 and idx ! prev: chars.append(idx) prev idx texts.append(chars) return texts4.4 中文识别最容易翻车的细节中文识别和英文识别有一个很大的差异形近字太多。“未”和“末”、“己”和“已”、“士”和“土”这些字人眼都容易看混模型更不例外。单纯靠换模型结构很难根治我在训练和推理两个阶段都做了处理。训练阶段我给这类形近字额外增加样本在合成数据里用同一张图片渲染出这两个字强制模型去学细微差别。推理阶段维护了一张形近字映射表匹配题目时如果完全匹配失败就尝试把候选字的形近字替换后再次匹配。还有一个容易被忽视的问题是学习率。CRNN里的LSTM部分对学习率非常敏感学习率超过1e-3时经常出现梯度爆炸loss直接变成NaN。我最终把识别模型的学习率定在2e-4并加了1000步warmup训练过程稳定很多。5. 输出点击坐标的关键一步从文本框到点选顺序5.1 检测框到点击点的转换检测模型输出的是文本框但业务需要的是点击坐标。转换很简单对每个检测框取中心点。如果检测框是水平矩形中心就是宽高的一半如果是旋转矩形中心就是minAreaRect返回的center。这里有一个常见误区四个顶点坐标的平均值不等于旋转框的几何中心尤其是当顶点顺序不是规则环形时直接用平均值算出来的点会有偏移。我统一用cv2.minAreaRect返回的中心稳定可靠。5.2 题目词和候选词的匹配策略匹配阶段不能只做字符串的严格相等判断。OCR识别在低质量验证码图上不可能100%正确可能把“花朵”识别成“花杂”把“小鸟”识别成“小鸟”识别正确但第二个字置信度很低。我按优先级设计了三层匹配第一层是精确匹配候选文本与题目词完全一致就直接命中。第二层是编辑距离匹配编辑距离小于等于1时认为属于同一个词这能救回少量错字的情况。第三层是同音字和形近字替换匹配把识别文本先做一次校正再用校正结果去匹配题目词。实际项目里第三层覆盖率虽然不大但每次命中都能避免一次点击失败价值很高。5.3 干扰文字与重复目标的处理点选图里经常有一些背景文字它们也长得很像“目标词”但不是题目要求的。比如题目要点击“花朵”图上可能同时有“花朵”“花”“草花”这些词。匹配模块必须判断一个候选文本是否在题目集合内而不是简单选“最相似”的候选。我维护一个题目词集合所有候选词先经过校正和过滤只在题目集合中查找映射。如果同一个题目词在图中出现两个候选框比如题目让点“花朵”图里出现了两个“花朵”这种情况无法预判我的策略是都输出坐标并标记为“优先点击置信度更高的框”。在合成数据里我专门渲染了一部分重复目标词的样本让模型有机会学习这种歧义情况实际测试中处理效果明显好过完全没遇到过这类样本的模型。5.4 漏检和误检时的兜底检测模型再好也有漏检的时候。如果题目词在所有候选框中找不到我会按顺序尝试三个兜底方案。第一步降低检测后处理的概率阈值从0.3降到0.1重新对原图跑一遍检测把漏掉的低置信度文字框找回来。第二步如果还是找不到对全图做一次扫描式识别用滑动窗口识别全图再和题目词匹配。第三步如果仍然空缺选择当前置信度最低的几个未匹配框强制OCR并做形近字替换看是否能找回目标词。这套兜底逻辑写成一个递归函数控制在两轮以内防止推理时间过长。实际效果是端到端点选成功率能从80%左右提升到88%以上代价是平均推理时间增加20毫秒可以接受。6. 实测效果与参数备忘调优过程中反复试错的经验6.1 数据量级和真实效果分享一组实测数据方便大家心里有数。我用2万张合成图训练DBNet1万张合成文本块训练CRNN检测F1大约0.93识别准确率约96%。直接扔到真实点选图上端到端点选成功率大约70%说明合成数据和真实数据之间确实存在分布差异。后来我加入5000张更接近真实风格的样本并额外用几百张真实风格抽样图微调整体点选成功率能到88%到92%。这里的关键不是数据量翻倍而是让数据的噪声分布逼近真实场景包括压缩率、字体渲染方式、背景风格等。6.2 一组可以直接抄的数值配置我把常用参数整理成了一张表方便复现时直接对照配置项数值检测输入尺寸640x640检测backboneResNet18DB概率图阈值0.3DB框置信度阈值0.5识别输入高度32识别输入最大宽度256CTC blank0检测优化器Adam, lr1e-4识别优化器Adam, lr2e-4, warmup1000检测batch size16识别batch size64这套配置在单张24G显存的卡上跑起来很轻松如果显存小一些检测batch降到8识别batch降到32也问题不大。6.3 优化方向的取舍检测端的优化点主要是小文字和低对比度文字。可以调整DBNet的shrink ratio从默认的0.4改到0.5让训练时收缩后的文字区域更大一些模型能学到更多文字边缘信息。但也不能调太大收缩不足会导致多个文字互相粘连反而产生错误检测框。识别端的优化重点是难例挖掘。训练过程中可以统计每个字被识别错误的频率把误识别率高的字对收集起来比如“己/已”“未/末”额外生成一批专门针对这些字对的合成样本。这样做的收益比盲目堆模型宽度高得多。6.4 工程实现里的几个坑OpenCV的minAreaRect角度问题前面提过这里再补充一个它返回的矩形尺寸和中心点没有固定顺序关系size里的w和h不是按宽高排序的做可视化时要自己约定。更稳妥的做法是用cv2.boxPoints把旋转矩形转成四个顶点再统一处理。CRNN的输入宽度在ONNX导出时会固定住这也是工程上一个不太起眼但很折腾人的坑。如果训练时宽度范围是32到256导出ONNX时输入张量的shape就不能是动态的否则部分推理框架不支持。我的做法是固定输入宽度为128训练时把样本统一缩放到这个宽度推理速度慢一点但省去了动态shape带来的兼容性问题。7. 复现清单与后续扩展思路7.1 项目包目录与核心文件zip包里的目录结构如下text_click_captcha/ ├── config/ │ ├── det_config.py │ └── rec_config.py ├── data/ │ ├── synth_dataset.py │ └── loaders.py ├── models/ │ ├── dbnet.py │ └── crnn.py ├── train_det.py ├── train_rec.py ├── infer.py ├── utils/ │ ├── box_utils.py │ └── text_matcher.py └── weights/infer.py是主入口传入图片路径和题目词列表输出有序点击坐标。所有训练配置集中在config目录里方便统一修改超参数。7.2 环境安装与依赖环境方面建议Python 3.10PyTorch 1.13或2.x都可以。GPU版PyTorch的安装命令我写在README里了conda create -n captcha python3.10 -y conda activate captcha pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python pillow numpy albumentations onnx onnxruntime如果你机器上CUDA版本不同PyTorch官网有对应的安装命令直接把cu118换成自己的版本即可。CPU环境也能跑推理只是速度慢一些。7.3 训练与推理命令训练和推理的命令很直接# 训练检测模型 python train_det.py --data data/synth_det/ --batch_size 16 --epochs 60 # 训练识别模型 python train_rec.py --data data/synth_rec/ --batch_size 64 --epochs 30 # 推理 python infer.py --image test_01.jpg --questions 花朵,小鸟推理输出会打印每步的点击顺序和坐标[点击顺序 1] 目标: 花朵, 坐标: (213, 184) [点击顺序 2] 目标: 小鸟, 坐标: (402, 357)7.4 评估指标与判定标准评估分三个层面。检测层面计算每个预测框和真实框的IoUIoU大于0.5算命中统计precision、recall、F1。识别层面以整词完全一致作为正确标准统计字准确率和整词准确率。端到端层面直接计算“正确点击点数除以总点击点数”这个指标最贴近业务可用性也是我日常调优最关心的数字。7.5 后续扩展方向这套方案可以沿三个方向扩展。点选验证码还有“选图”变体目标不是文字而是物体相当于通用目标检测加分类只需把文字识别替换为图像分类或向量匹配。如果题目词是图标而不是文字可以接入CLIP或SimCLR做图文匹配。如果遇到大量低分辨率文字可以在识别前加一个轻量级超分模块对模糊文字块做增强。说实话这类项目最值钱的不是模型结构而是你对“合成数据到底要模拟哪些真实噪声”的理解。数据做透了检测和识别的调参都事半功倍。我最终把整个项目整理成zip包也是希望后来者不用再走一遍我合成数据时踩过的那些坑——直接拿着脚本改改字体、调调背景就能生成一套属于自己的训练集把精力花在真正有意思的模型优化上。本文还有配套的精品资源点击获取
返回列表