
简介基于ResNet与DenseNet深度学习算法的验证码光学字符识别OCRPython源码包适合计算机相关专业的在校学生、算法初学者以及有课程设计或毕业设计需求的开发者使用。资源内包含完整项目代码与训练/测试脚本并配有1084个文件其中以1071张验证码样本图片为主辅以5个XML标注或配置文件、4个Python核心脚本、1个字体文件及说明文档能够支撑从数据准备、模型训练到预测识别的完整流程。压缩包整体为8.84MB体量较轻、易于部署。目前已有423人学习下载验证了其实用性和参考价值。项目代码经过运行测试目录结构清晰可直接在现有基础上调整网络结构或新增字符集用于复现实验、课程设计、大作业甚至初期项目演示是入门深度学习图像识别与OCR任务的不错参考。1. 验证码识别从传统OCR到深度学习模型ResNetDenseNet定长分类思路很多人拿着验证码图片第一反应是装一个 Tesseract 来跑但结果往往惨不忍睹。原因很简单验证码本身就是反 OCR 设计的扭曲、旋转、干扰线、粘连让传统特征工程难以招架。基于深度学习的验证码识别模型把这个任务重新定义为“图像分类”给定一张定长验证码用卷积网络提取字符级特征输出每一位字符的概率分布。这篇博客围绕的正是这样一份课程设计 python 源码它采用 ResNet 与 DenseNet 两种算法结构构建 OCR 识别模型把 45 位字符的验证码 png 转成可直接训练的监督数据训练和部署链路完整适合计科、人工智能、大数据方向的课程设计、毕设项目或初期项目演示。2. ResNet与DenseNet结构差异及验证码特征提取中的网络设计验证码识别模型不能简单地把 ImageNet 预训练模型搬过来改最后一层。验证码字符尺度小、纹理集中网络太深容易丢失细节太浅又学不到语义特征。本项目把 ResNet 和 DenseNet 两个结构特征组合使用ResNet 负责稳定的梯度传递DenseNet 负责特征复用两者共用同一个分类头。这一章先讲清任务建模是“定长分类”还是“序列识别”再对比两种结构在验证码场景下的利弊最后给出可改写的 PyTorch 网络骨架。2.1 验证码识别任务建模定长分类与CTC两套路线深度验证码识别通常有两种建模方式。第一种是定长分类假设验证码固定为 4 位或 5 位例如项目里的 wmpmp.png、pe4xn.png、24f6w.png 这类样本网络输出多组概率组数等于字符长度每一组对应字符集上的一个分布。第二种是序列识别不限制长度用 CNN 提取特征序列后接 BiLSTM用 CTC 损失进行对齐这也是 CRNN 的经典方案。这份源码选择的是定长分类路线从课程设计角度看足够合理训练稳定、收敛快、显存压力小而且评估指标就是简单的字符级和整图准确率答辩时解释起来很清晰。如果是长度变化的验证码则需要在预处理阶段把定长假设换成 CTC模型结构和损失函数都要调整。2.2 ResNet残差连接在字符纹理提取中的作用ResNet 的核心思想是残差学习让卷积层去拟合输入 x 与目标输出之间的残差 F(x)而不是完整映射 H(x)。对验证码这类前景与背景对比强烈的图像残差结构给了梯度一条“高速公路”网络堆到 18 层以上也不会出现梯度消失。具体到字符识别浅层卷积学习笔画边缘和角点深层卷积组合出“横折”“弧线”这类更高层特征残差连接保证深层特征图里依然保留原始字符的形状轮廓。实际工程中ResNet18 这种轻量级主干加一个全局池化和全连接输出头就能在 CPU 上完成推理课程设计阶段完全没有必要上 ResNet50 以上的大网络。2.3 DenseNet稠密连接的特征复用价值DenseNet 比 ResNet 更激进每一层都把之前所有层的输出在通道维度拼接后作为输入。带来的直接收益是特征复用比如字符“8”的圆弧边缘特征在第一层就出现后面的 DenseBlock 可以反复读取它而不用重新学习。对尺寸较小的验证码图像DenseNet 的参数效率更高在相同精度下用更少的参数量。代价是内存占用大因为需要保存大量中间拼接特征图。所以常见做法是用 ResNet 作为骨干提取初始特征在网络后段替换成 DenseBlock让稠密连接去强化字符细粒度的纹理表达避免内存爆炸也能兼顾两种结构的优势。2.4 本资源所用主干网络的常见组织方式以 PyTorch 为例类似源码里最常见的实现方式是借助 torchvision 里的预训练模型把后面的分类头去掉再接一个自定义输出层。下面的代码演示了如何用 ResNet18 的骨架改造成一个定长验证码分类器import torch import torch.nn as nn from torchvision import models class CaptchaCNN(nn.Module): def __init__(self, num_classes36, captcha_len4): super().__init__() # 去掉 ResNet18 最后的 avgpool 和 fc只保留卷积特征层 resnet models.resnet18(pretrainedFalse) self.backbone nn.Sequential(*list(resnet.children())[:-2]) # 输出 4 位验证码每一位在 36 个字符上做分类 self.head nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(512, captcha_len * num_classes) ) self.captcha_len captcha_len self.num_classes num_classes def forward(self, x): feat self.backbone(x) out self.head(feat) # 拆成 [batch, captcha_len, num_classes] 方便计算交叉熵 return out.view(-1, self.captcha_len, self.num_classes)这段代码的逻辑很直接backbone 输出空间特征图head 里的 AdaptiveAvgPool2d 把任意尺寸压到 1x1Flatten 后用线性层一次性输出captcha_len * num_classes个值再 reshape 成三维张量。其中pretrainedFalse表示随机初始化如果数据集只有几千张这比加载 ImageNet 权重更省事num_classes36对应 10 个数字加 26 个大写字母如果包含小写字母就改成 62。这里的captcha_len必须和预处理后每张图片的字符数量一致否则训练时张量形状对不上。为了理清选择依据可以用下面这张表对比两种主干在验证码任务上的表现维度ResNetDenseNet梯度流动残差加和路径短而稳定每层直连前序路径丰富特征复用隐式复用显式拼接复用内存占用低高训练收敛快适合小样本慢一些细节特征更充分推荐场景主干 浅层特征提取深层细粒度笔画识别这张表在课程设计报告里可以作为“网络选型依据”直接引用比单纯贴网络结构图更有说服力。3. 验证码png样本预处理与数据增强从文件到可训练张量验证码项目第一个坑往往不是网络结构而是数据没有处理干净。项目给出的样本如 24f6w.png、gc83b.png、m4g8g.png 都是带明显干扰的验证码截图尺寸通常在 160x60 到 120x40 之间字符位置会有偏移。如果直接把原图喂给 ResNet网络会学到“字符在图片哪个区域”的位置记忆换一张新验证码就失效。因此预处理阶段要完成灰度化、去噪、二值化和统一尺寸。3.1 样本形态分析与预处理流程拿到一批 png 后我一般会先写一段脚本统计图像尺寸和灰度直方图。常见流程是转灰度高斯滤波去掉细颗粒噪点用 Otsu 自动阈值做二值化再按比例缩放并居中对齐到统一画布。下面是比较通用的实现import cv2 import numpy as np def preprocess(image_path, target_size(32, 32)): # 以灰度模式读入 img cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) # 3x3 高斯滤波去掉单像素噪点 img cv2.GaussianBlur(img, (3, 3), 0) # Otsu 自动阈值二值化反相让字符变成白色前景 _, binary cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 保持宽高比缩放避免字符被拉扁 h, w binary.shape scale min(target_size[0] / w, target_size[1] / h) binary cv2.resize(binary, (int(w * scale), int(h * scale))) # 放到 32x32 黑色画布中央 canvas np.zeros(target_size, dtypenp.uint8) bh, bw binary.shape x_off (target_size[1] - bw) // 2 y_off (target_size[0] - bh) // 2 canvas[y_off:y_off bh, x_off:x_off bw] binary # 归一化到 0~1 区间 return canvas.astype(np.float32) / 255.0这里的参数选择遵循一个原则GaussianBlur 的核用 3x3太小去不掉噪声太大把字符笔画边缘也磨掉了。Otsu 会自动计算阈值适合背景色变化不大的验证码。scale min(32/w, 32/h)保证字符完整显示且不拉伸多出来的区域留黑边。归一化到 0~1 之后模型输入分布稳定配合 BatchNorm 层收敛更快。如果验证码带彩色干扰线需要在灰度化之前加一步颜色通道过滤这里不展开。3.2 文件名标签编码与字符集映射预处理搞定后要把文件名转成标签。以 24f6w.png 为例真实标签是“24f6w”但例子里的字符集通常只需要大写字母加数字因此统一用upper()把小写转大写。下面的代码构造字符到索引的映射并遍历目录生成训练样本import os charset 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ char2idx {c: i for i, c in enumerate(charset)} def label_to_tensor(label): return [char2idx[c] for c in label] def build_dataset(root): samples [] for name in os.listdir(root): if not name.endswith(.png): continue label os.path.splitext(name)[0].upper() if len(label) 0 and all(c in char2idx for c in label): samples.append((os.path.join(root, name), label_to_tensor(label))) return samples这段代码的关键点在于字符表顺序charset里第 i 个字符对应模型输出的第 i 个类别训练和推理必须使用同一个字符表否则会出现预测结果永远对不上的情况。label_to_tensor返回一个与字符长度等长的 list后续在 Dataset 中转换成torch.long张量。一个容易忽略的细节是取文件名时用os.path.splitext(name)[0]它会安全去掉扩展名比name[:-4]更不容易踩到不可见字符的坑。3.3 数据增强策略与参数配置验证码图像的增强和自然图像不同不能做任意角度旋转和水平翻转因为字符颠倒或镜像后含义已经改变。课程设计场景里我通常只用轻微的亮度抖动、像素级平移和噪声注入。下表是可抄的配置增强方式参数范围使用说明亮度/对比度抖动0.8 ~ 1.2模拟不同显示设备截图上下左右平移±2 像素打破位置记忆弹性形变alpha0.5提高对字符扭曲的鲁棒性椒盐噪声概率 0.02模拟压缩噪声水平翻转关闭会破坏字符方向信息增强可以用 imgaug 或 albumentations 一行接入。需要特别提醒的是增强不是越重越好。验证码字符数量和空间结构一旦被随机裁剪破坏模型学到的特征立刻退化成“数笔画”。样本量只有几千张时过重的增强反而会引入噪声导致验证集准确率止步在某个低值。我一般只保留亮度抖动和微小平移把更多精力放在把预处理流程调对上。提示如果训练多个 epoch 后验证集准确率一直在 40% 以下先去看预处理输出的图片字符是否完整、是否反色不要急着换网络结构。4. 训练验证码识别模型数据加载、损失函数与调参细节到了训练环节容易出问题的反而是数据读取和损失函数形状的对齐。很多人把训练脚本跑起来就盯着 loss 看其实更关键的是把每步的输入输出维度搞清楚。下面从 Dataset 开始到损失计算再到调参和断点续训把整条链路串起来。4.1 Dataset与训练循环骨架PyTorch 下需要自己定义一个 Dataset把前面生成的 samples 封装起来__getitem__里返回预处理后的图像张量和标签张量。训练循环里模型输出是[batch, captcha_len, num_classes]标签是[batch, captcha_len]两者展平对齐后计算交叉熵。示例如下import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader class CaptchaDataset(Dataset): def __init__(self, samples, transformNone): self.samples samples self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img preprocess(path) if self.transform: img self.transform(img) return torch.from_numpy(img).unsqueeze(0), torch.tensor(label, dtypetorch.long) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 correct_chars 0 total_chars 0 for x, y in loader: x, y x.to(device), y.to(device) out model(x) b, L, C out.shape loss criterion(out.view(b * L, C), y.view(b * L)) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() pred out.argmax(dim-1) correct_chars (pred y).sum().item() total_chars y.numel() return total_loss / len(loader), correct_chars / total_chars这段代码的要点有三个unsqueeze(0)把灰度图从[32,32]变成[1,32,32]表示单通道out.view(b * L, C)把 4 位字符的所有预测合并起来统一算损失argmax(dim-1)在最后一维取数值最大的类别索引得到整张图片每个位置的预测字符。返回值同时计算了 loss 和字符级准确率但课程设计最终汇报时建议再统计一个整图准确率也就是一张验证码的所有字符全部预测正确的比例这个指标更贴近真实登录场景。4.2 损失函数选择交叉熵还是CTC定长验证码场景优先用nn.CrossEntropyLoss不要顺手用 CTC。CTC 是为长度不定的序列设计的例如 CRNN 中接 BiLSTM 后做对齐训练需要额外的 blank 机制收敛慢且调参复杂。本项目输出长度固定每一位都是独立分类事件交叉熵能提供更强的梯度信号少样本下也更容易收敛。如果后续要处理长度变化的验证码才需要把模型输出改成序列特征并在解码阶段做去重合并那已经是另一个复杂度层级了。4.3 超参数配置与收敛判断基于这类源码的常见实验给出一份可以直接使用的初始超参数超参数建议值说明输入尺寸32 x 32太大浪费计算太小丢失笔画主干ResNet18 DenseBlock参数量约 12MBatch Size32 ~ 64根据显存调整学习率1e-3Adam 优化器适用Epoch30 ~ 50小数据集 20 轮可收敛优化器Adam ReduceLROnPlateauloss 平台期自动降学习率实际调试时发现一个高频问题训练准确率很高但验证准确率上不去多半是预处理阶段字符没有对齐或是数据增强里的平移和弹性形变过量。这时候先把增强关掉只保留亮度抖动重新训练几个 epoch 看趋势。如果验证准确率开始回升说明是增强策略的问题如果仍然停滞再去检查标签文件是否有多余字符或空格。4.4 模型保存与断点续训课程设计验收时可能要求中途换机器继续训练所以保存 checkpoint 要把模型、优化器、当前 epoch 和字符表全部带上。下面是一个常见做法def save_checkpoint(model, optimizer, epoch, best_acc, path): torch.save({ epoch: epoch, model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), best_acc: best_acc, charset: charset }, path)这样保存的好处是恢复训练时只需一个文件就能完整还原状态。charset字段尤其关键推理服务启动时直接读取模型文件里的字符表可以避免训练脚本和部署脚本之间字符集不一致的问题。best_acc用来记录当前最优整图准确率训练脚本在每一轮结束时比较并覆盖最优权重保证最后留下的是效果最好的模型而不是最后一个 epoch 的模型。5. 推理部署与课程设计验收批量识别、模型导出与常见坑模型训练完项目还差最后一步把它变成可以演示和交付的东西。这一章给出完整的推理代码、导出的快速方案以及几个这个场景里出现频率最高的坑。5.1 批量推理与置信度输出推理脚本的核心是关闭梯度并把模型切到 eval 模式避免 dropout 和 BatchNorm 行为不一致。下面的函数接收一张图片路径返回预测字符串和平均置信度def predict(model, image_path, devicecpu): model.eval() img preprocess(image_path) x torch.from_numpy(img).unsqueeze(0).unsqueeze(0).to(device) with torch.no_grad(): logits model(x) probs torch.softmax(logits, dim-1) conf, pred probs.max(dim-1) label .join(charset[p] for p in pred[0].tolist()) return label, conf[0].mean().item()这里先unsqueeze(0)增加 batch 维再unsqueeze(0)增加通道维顺序不能反。softmax在最后一维上计算得到每个字符位上所有类别的概率probs.max(dim-1)返回最大值和对应索引。平均置信度可以展示成“预测为 24f6w置信度 0.98”比只输出字符串更有说服力答辩演示时能直接证明模型不是瞎猜。5.2 导出为 TorchScript 与 ONNX如果要把模型集成到 Java 或 C 服务里可以转换为 TorchScripttraced torch.jit.trace(model, torch.randn(1, 1, 32, 32)) traced.save(captcha_jit.pt)trace会把前向过程固化成静态图之后用torch.jit.load加载即可摆脱训练环境。更通用的做法是导出 ONNX再用 onnxruntime 做推理后端语言不限。有一点要明确这套流程和 Tesseract 这类通用 OCR 是两条完全不同的技术路线验证码图片的目的就是对抗普通 OCR直接用 Tesseract 识别率通常很低这也是深度学习落在这个场景的核心原因。5.3 三个容易踩的坑第一个坑是尺寸不一致。不同来源的验证码分辨率可能不同直接resize(32,32)会把字符拉扁正确做法是按比例缩放再居中也就是预处理里画布方案。第二个坑是深浅色反转。白底黑字和黑底白字的图片混合在一起时Otsu 二值化会得到相反的前景同一个字符笔画特征完全翻转训练会很不稳定。第三个坑是标签大小写混用文件名用小写采集代码里转成大写后字符表对不上推理结果全是错的。把这三个坑写进课程设计报告的“问题与解决”一节能明显提升项目的完整感和可信度。本文还有配套的精品资源点击获取