验证码识别脚本实战:OpenCV预处理+CNN训练全流程解析
发布时间:2026/9/26 16:36:42来源:尧图网络
简介面向计算机相关专业学习者与机器学习初学者的实战项目基于机器学习算法实现验证码识别包含可直接运行测试的完整源码与说明文档适用于课程设计、毕业设计或企业初期项目演示具有较高的学习借鉴价值。压缩包共2000个文件以1995张验证码图片为主要数据集附4个Python脚本和1个说明文档整体仅12.42MB轻量紧凑便于快速下载与部署。目前已有163人学习下载代码经测试运行成功功能稳定。四个Python脚本分别负责数据整理、图像切割、模型训练与识别调用形成清晰可复用的代码结构结合说明文档读者能完整经历验证码图像预处理、特征提取、模型训练与识别的流程理解从样本整理到算法调用的实现思路为后续改进或迁移到其他图像识别任务提供参照也可作为图像识别入门项目的模板。1. 验证码识别脚本当自动化流程被一张图片卡住做自动化测试或者批量数据处理的人多半都遇到过这个场景前面的登录脚本写得很顺一到验证码图片就卡住。手动输入一次可以几百次坚持不住。我第一次查“验证码识别”时发现这词几乎总是和机器学习算法出现在一起容易让人以为上来就要搞大模型。实际上标题里这个基于机器学习算法的验证码识别脚本核心不是模型有多深而是把图像预处理、字符分割、模型训练和预测脚本串成一条完整链路。这条链路的典型形态是OpenCV 做图像清洗和字符切分CNN 做单字符分类Python 脚本把结果按顺序拼起来。它擅长处理的是不断变化的静态验证码不是滑块或点选这类行为验证适合自动化测试工程师、批量数据处理开发者以及想练手图像分类的机器学习新手。本文按实际落地顺序把这条链路从头到尾拆开讲。2. 验证码识别到底在做什么处理管线和算法选型验证码识别看起来是个模型问题实际上一开始就决定成败的是管线设计。最常用的一条完整管线是读图 → 灰度化 → 二值化 → 去噪 → 字符分割 → 归一化 → 分类 → 后处理。前四步在 OpenCV 里完成分类交给训练好的模型最后用规则做纠错。下面把每个环节的原理和选型理由说清楚。2.1 图像预处理灰度、二值化和去噪的先后顺序不能乱验证码图片天然带着大量干扰背景渐变、噪点、干扰线、字符旋转。第一步是灰度化。虽然颜色本身也是特征但大多数验证码的字符颜色并不稳定依赖颜色反而会把问题复杂化所以先转成单通道。用 cv2.imread 时可以直接指定 IMREAD_GRAYSCALE也可以在彩色读入后用 cvtColor 转灰度两者结果略有差异建议全项目统一用一种方式避免训练和预测时数据处理不一致。然后是二值化这是预处理里最值得花时间调的一步。常见做法是用大津法OTSU自动算阈值不用手动猜。特别要留意阈值方向如果背景亮、字符深要用 THRESH_BINARY_INV 把字符翻成白色、背景翻成黑色这样后面统计投影时才是“白色像素代表前景”。方向搞反是最常见的低级翻车表现是二值图里字符区域一片黑分割直接失败。去噪放在二值化之后做。中值滤波对椒盐噪声很有效卷积核一般取 3太大容易把细笔画抹掉。如果验证码里有很多细短的干扰线可以再加一次形态学开运算用 2×2 的核对白色前景做腐蚀再膨胀把孤立的细线断掉。这一步要不要用取决于干扰线密集程度建议把开关做成参数而不是写死在代码里。2.2 字符分割投影法比固定宽度切割更可靠字符分割是验证码识别里最容易被低估的一环。最直接的做法是固定宽度切割比如 4 位验证码就把图片宽度四等分。这种方法在字符规整、等宽的验证码上能跑通但只要字体不是等宽、字符位置有随机偏移切出来的边界就会错位后续识别率断崖式下跌。所以做这类识别脚本我一般优先用垂直投影法。把二值图每一列白色像素数累加字符所在的列区间会出现波峰字符之间的空隙会出现接近 0 的波谷按波谷作为切分线。代码量不大效果比固定宽度稳定很多。投影法对“字符不粘连”的验证码非常可靠配合一个 min_width 参数过滤掉杂点和短干扰线产生的零碎列区段基本能覆盖大部分场景。如果字符之间发生粘连投影波谷会消失一个区段里包含两个字符。这种场景有两条路一是改用连通域分析把连在一起的字体块按连通区域拆开但粘连处的笔画本来就连在一起拆不干净二是承认显式分割这条路到头了改用目标检测或序列识别模型直接整图输出。就工程投入来说前 80% 的验证码用投影法就能解决剩下 20% 的粘连场景才需要换方案。2.3 模型选型SVM 是基线CNN 是主力分割之后的任务是单字符分类。类别数一般是 36数字加小写字母或 62大小写加数字纯数字只有 10 类。传统做法是提取 HOG 特征扔给 SVM 训练。SVM 的好处是数据量需求小每类 200 张可能就够训练也快适合字符已经被分割得很干净的场景。坏处是对旋转、扭曲、噪声这些干扰不敏感换一套风格相似的验证码可能就要重新调特征。CNN 是目前最稳妥的选择。哪怕只用三层卷积也能自动学到字体排布、笔画粗细这些特征对轻微变形和噪声的容忍度比 HOG 加 SVM 高很多。训练数据每类 500 张起步实测能到 90% 以上数据增强跟上后每类 1000 张基本是这类任务的甜点区。模型结构不需要很复杂VGG 那种十几个卷积层的网络在 32×32 的字符图上有点浪费两个卷积块加两个全连接已经够用。至于 Tesseract 这类现成 OCR 引擎它们在自然场景文字识别上很强但验证码就是故意反 OCR 设计的字符扭曲、旋转、加干扰线之后通用 OCR 引擎的准确率会掉到不可用。所以没必要从 OCR 硬调专项模型才是正路。这里还要提一个经常被忽略的后处理环节把易混淆字符映射掉。数字 0 和字母 O、数字 1 和字母 l 在分割后的图上很难区分如果业务场景允许直接跑一个规则映射把这类字符归一到同一个类别能显著拉高整体准确率。评价指标也要提前定死字符级准确率只看单个字符整图准确率要求几位字符全对才计一次对。通常整图准确率在 70% 时说明管线里已经有环节不稳到 85% 以上才敢拿去做批量处理。样本从哪来两个来源。一是人工标注真实验证码截图后按字符拆开分别放进以标签命名的文件夹。二是用 Pillow 写一个随机生成器模拟字体、旋转、噪点、干扰线批量产出带标签的样本。真实样本不够时先用生成样本把模型跑起来再用真实样本做增量训练这个组合在实际项目里反复验证过有效。3. 把识别脚本跑通预处理、训练与预测的核心代码拿到一个验证码识别脚本源码包最常见到的文件结构是 config.py、preprocess.py、train.py、predict.py、dataset 目录和说明文档。不管文件名是不是完全一致核心功能就是三块预处理切字符、训练模型、加载模型做预测。下面按这三块给出可以直接套用的实现并对关键参数逐一说明。3.1 字符切分与预处理把验证码拆成单字符import cv2 import numpy as np def preprocess(image_path: str) - np.ndarray: 读入验证码图片返回前景为白色的二值图。 img cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) # 大津法自动找阈值INV 把深色字符翻成白色前景 _, binary cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 中值滤波去噪点核大小取 3太大会抹掉笔画 binary cv2.medianBlur(binary, 3) return binary def split_by_projection(binary: np.ndarray, min_width: int 5) - list: 按垂直投影切割字符返回一组单字符二值图。 h, w binary.shape col_sum np.sum(binary 255, axis0) chars [] in_char False start 0 for i in range(w): if col_sum[i] 0 and not in_char: start i in_char True elif col_sum[i] 0 and in_char: if i - start min_width: chars.append(binary[:, start:i]) in_char False if in_char: chars.append(binary[:, start:]) return chars def normalize_char(char_img: np.ndarray, size: int 32) - np.ndarray: 把字符图等比例缩放到 size四周补黑边最后归一化到 0~1。 h, w char_img.shape scale min(size / h, size / w) new_h, new_w int(h * scale), int(w * scale) resized cv2.resize(char_img, (new_w, new_h), interpolationcv2.INTER_AREA) canvas np.zeros((size, size), dtypenp.uint8) y0 (size - new_h) // 2 x0 (size - new_w) // 2 canvas[y0:y0 new_h, x0:x0 new_w] resized return canvas.astype(np.float32) / 255.0这段代码的逻辑不复杂但三个函数的边界条件要说明。preprocess 先读灰度图然后交给 OTSU 自动二值化。阈值函数里传入 0配合 THRESH_OTSU 表示阈值由大津法决定THRESH_BINARY_INV 则决定了前景是字符还是背景。中值滤波的核大小只能是奇数取 3 是经验值遇到笔画特别细的验证码可以改成 1 表示不过滤。split_by_projection 里col_sum 统计的是每一列白色像素总数白色像素从 0 变正意味着进入字符区从正变 0 意味着字符区结束。这里 min_width 的作用是过滤掉孤立噪点和短干扰线产生的“伪字符”但设太大也会把窄字符如数字 1 丢掉取值范围建议 5 到 8。normalize_char 是很多人容易忽略的细节。它先把字符等比例缩放再放到 32×32 的黑色画布中央而不是直接 resize 拉伸。字符如果被拉伸变形CNN 学到的笔画比例就乱了。最后除以 255 把像素归一化到 0 到 1这一步必须和后续训练时的数据处理保持一致。3.2 训练一个 CNN 字符分类器数据目录与关键参数import json import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers from tensorflow.keras.preprocessing.image import ImageDataGenerator def build_model(num_classes: int) - keras.Model: model keras.Sequential([ layers.Conv2D(32, (3, 3), activationrelu, paddingsame, input_shape(32, 32, 1)), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(128, activationrelu), layers.Dropout(0.3), layers.Dense(num_classes, activationsoftmax), ]) model.compile(optimizerkeras.optimizers.Adam(learning_rate1e-3), losscategorical_crossentropy, metrics[accuracy]) return model datagen ImageDataGenerator( rotation_range10, width_shift_range0.1, height_shift_range0.1, zoom_range0.1, rescale1.0 / 255, ) train_generator datagen.flow_from_directory( dataset/train, target_size(32, 32), color_modegrayscale, batch_size32, class_modecategorical, ) model build_model(num_classeslen(train_generator.class_indices)) model.fit(train_generator, epochs20) with open(class_indices.json, w) as f: json.dump(train_generator.class_indices, f, indent2) model.save(captcha_model.h5)训练目录在 dataset/train 下每个字符标签建一个子目录目录名就是类别。flow_from_directory 会自动扫描子目录并生成标签编号class_indices 保存了标签名和编号的映射这个映射在预测时必须加载回来否则模型输出的是一个数字编号而不是字符。网络只用两个卷积块加一个全连接参数量小普通 CPU 也能跑完训练没必要上来就上 ResNet。参数说明rotation_range 控制在 10 度以内验证码实际旋转幅度一般不会太大过大的旋转增强反而会让模型学到错误形态。width_shift_range 和 height_shift_range 设 0.1 表示允许图片在宽高方向平移 10%能模拟分割时的小幅偏移。Dropout 放在全连接层之前取 0.3 是个折中值太小防不住过拟合太大又会让模型欠拟合。训练轮数 20 不是固定值更稳妥的做法是用 EarlyStopping当验证集准确率连续几个 epoch 不再提升时自动停。还有一个数据划分上的关键点切分训练集和验证集时必须按“整张验证码图片”为单位切不能按“单字符图片”随机切。因为同一张验证码里的字符共享同一种字体风格和噪声环境如果把同一张里的不同字符分到训练和验证两边模型能通过记忆字体特征拿到高验证分真实场景里立刻露馅。3.3 单张预测脚本把预处理、分割、模型串起来import json import numpy as np from tensorflow import keras from preprocess import preprocess, split_by_projection, normalize_char def predict_captcha(image_path: str, model, label_map: dict) - str: binary preprocess(image_path) char_imgs split_by_projection(binary) result [] for ch in char_imgs: norm normalize_char(ch) x norm.reshape(1, 32, 32, 1) probs model.predict(x, verbose0) cls int(np.argmax(probs)) result.append(label_map[cls]) return .join(result) model keras.models.load_model(captcha_model.h5) with open(class_indices.json, r) as f: class_indices json.load(f) # class_indices 保存的是 {0: 0, 1: 1, ...}翻转成 {0: 0, 1: 1, ...} label_map {v: k for k, v in class_indices.items()} print(predict_captcha(samples/login_captcha.png, model, label_map))预测流程和训练时的数据处理完全对称同一套 preprocess、同一套 split_by_projection、同一个 32×32 输入尺寸。模型加载一次后面每次调用只做前向传播不要在每个循环里重复 load_model那是批量识别变慢和内存暴涨的最常见原因。这里有个可以润色的地方model.predict 返回的是每个类别的概率向量如果不只是取 argmax而是同时拿到 probs.max()可以在置信度低于 0.7 时把该字符标记成问号让上层接口决定重试还是放弃。对于自动化测试场景重试一次往往比硬吞一个错误结果更划算。4. 训练与调优准确率上不去的四个原因和对策很多人在拿到源码包后最关心的是准确率。实际上字符分类模型的准确率很少遇到大问题真正拉低整图准确率的往往是数据量不够、预处理不一致、学习率不合适以及后处理规则没做。下面四个问题是我在调这类方案时反复踩过的。4.1 样本量每类 500 张是门槛1000 张是甜点单字符分类任务对数据量的要求并不高。类别数 36 时每类 300 张就能看到模型过拟合500 张以上才开始稳定1000 张左右准确率增长变得平缓。如果每个字符类的样本量严重不均衡模型会对高频类别有偏向低频类别经常被误判成高频类别。解决样本不均衡的一种常见做法是目录层面补样数一下每个标签文件夹里的图片数对少的类别多做几轮增强。ImageDataGenerator 的增强参数别开太大rotation_range 开到 15 度以上对验证码这种小图没什么意义生成的样本反而会失真。真实样本和生成样本混合时生成样本比例建议控制在 30% 以下否则模型学会的是生成器的噪声分布而不是真实验证码的风格。4.2 学习率、batch size 和训练轮数怎么调用 Adam 优化器时learning_rate 默认 1e-3 对这个小模型通常够用。如果训练前几个 epoch loss 就在震荡把学习率降到 1e-4同时把 batch size 从 32 降到 16。不要一上来就堆 batch size小数据集上大 batch 容易收敛到尖锐极小值泛化反而变差。训练轮数也不是越多越好。我一般会预留一个不参与训练的验证集用 EarlyStopping 监听验证集准确率patience 设 5连续 5 个 epoch 不涨就停。如果模型在训练集上已经 99%验证集只有 80%优先怀疑数据切分泄漏和增强过猛而不是加网络层数。先打印一批训练数据的 shape 和标签确认归一化范围是 0 到 1、标签编号和字符对应再谈调参。4.3 按验证码难度选方案一张经验对照表验证码特征推荐管道每类样本量整图准确率经验值纯数字、无干扰投影法 简单 CNN20096%数字字母、有噪点和干扰线投影法 CNN 数据增强50092%字符旋转、扭曲、轻度粘连投影法 CNN分割参数细化100085%粘连严重或含汉字检测/序列识别模型300080%这张表不是绝对标准因为不同验证码的干扰风格差异很大但可以帮你判断一个方案的上限在哪。如果你的验证码属于第三行但代码包里只有投影法加 CNN那么预处理和分割参数需要花更多心思而不是盲目加模型复杂度。表格里第四行提到的场景显式分割几乎失效常见做法是换成 YOLO 检测字符位置再分类或者直接用 CRNN 加 CTC 做整图序列识别。这两种方案的工程量和数据准备成本都高一个量级通常不是优化识别脚本的第一选择。4.4 后处理纠错救回三五个点的关键步骤后处理是准确率的最后一层兜底。第一个规则是易混淆字符映射把数字 0 和字母 O、数字 1 和字母 l、数字 2 和字母 Z 这类统一映射到业务侧允许的字符上字符类别数从 36 降到 30 多分类器的压力也会小一些。第二个规则是置信度阈值。模型预测概率低于 0.7 的字符与其强行输出一个错误的不如输出问号交给上层处理。批量场景里一次重试通常比解析错误结果便宜。第三个规则是长度校验。投影法切出来的字符数量和预期位数不一致时大概率是某个字符被切成了两段或者两个粘连字符没切开。这时回头看分割输出比在模型上反复换权重有效得多。5. 避坑指南验证码识别脚本的五个翻车现场这一章是实践中最常遇到的问题每条都按“现象、原因、解决”写清楚希望能帮你少走弯路。5.1 图像读取与二值化的翻车现场第一个翻车现场是 cv2.imread 返回 None。在 Windows 下验证码图片路径如果包含中文目录名OpenCV 的 imread 不认打印出来是 None后续所有操作直接报错。原因是 OpenCV 底层用的是 C 的文件读取接口对系统编码支持不好。解决的办法是不用 imread 读中文路径改用 numpy 从文件读字节再解码import cv2 import numpy as np data np.fromfile(image_path, dtypenp.uint8) img cv2.imdecode(data, cv2.IMREAD_GRAYSCALE)第二个翻车和二值化方向有关。现象是预处理后图片里字符区域全黑、背景全白投影法统计出来的全是背景。原因是阈值方向选反了THRESH_BINARY 和 THRESH_BINARY_INV 互换即可。判断方向有个笨办法二值化后统计白色像素比例如果超过 70%多半是背景变成了前景需要把方向反过来。5.2 数据与训练环节的翻车现场第三个翻车是训练集准确率 99%验证集也 95%一上真实环境只剩 70%。最常见的原因是数据切分泄漏把同一张验证码切出的单个字符随机分进了训练和验证集。同一张图里的字体风格完全一致模型等于提前见过了同类样本。解决方法是按整图为单位划分先把验证码文件列表随机切成两份再分别去切字符而不是把所有字符丢在一起按比例切。第四个翻车是 loss 一直是 nan 或者准确率卡在某个值不动。前者通常是输入数据里有 NaN比如读取失败的图片没被跳过直接进到了训练管线后者常见于标签编号和数据不对应flow_from_directory 生成的 class_indices 顺序和预测时加载的 label_map 不一致。解决方法是先写一个数据检查函数打印前几条数据的 shape、dtype、标注文本确认输入输出都正常再跑训练。5.3 批量调用与部署的翻车现场第五个翻车是批量识别脚本越跑越慢。现象是处理前十几张图很快后面每张耗时明显上涨。原因一般有两个一是在循环里重复 load_model每次加载都要重建计算图二是每次只对单张图调用 model.predict没有发挥 batch 的并行能力。解决方法是把模型加载放在循环外同时把一批图片推到同一个 batch 里预测而不是一张一张调。批量预测的代码形态通常是先把一批图片预处理成四维张量再一次性 model.predict拿到所有概率后统一 argmax。这个改动能让吞吐量提升好几倍代码结构也更接近生产环境。6. 从能跑到能落地工程化改造的三个方向6.1 部署形态从命令行脚本到 HTTP 接口很多业务系统是 PHP 或 Java 写的这时候让 Python 识别脚本独立部署成一个 HTTP 服务业务方发请求拿结果是最常见的做法。Flask 包一层接口很简单关键是把模型初始化放在路由注册之前避免每次请求都加载模型。接口可以接收验证码图片的 base64 字符串返回识别结果和置信度这样跨语言调用就不成问题。6.2 验证方法与错误样本回流准确率只说明一部分问题耗时稳定性同样重要。建议单独收集一批不参与训练的验证码截图人工标注好后作为测试集统计整图准确率、字符准确率和单张识别的 P50、P95 耗时。P95 超过 1 秒的识别服务在大量自动化用例里会变成明显的等待瓶颈。错误样本回流是持续提升准确率的核心。把预测失败的图按预测标签存进 error_samples 目录人工修正后补充到训练集里下一轮增量训练只跑沉淀的错误样本训练成本比全量重训低很多。我在实际项目里靠这一套错误样本回流把整图准确率从 82% 抬到 93%过程没有改一行网络结构。6.3 从分割分类到端到端的前进路线如果验证码粘连严重投影法切不干净整图准确率会被分割卡死在 80% 左右。这时候再优化卷积结构意义不大更值得投入的方向是目标检测加分类或者 CRNN 加 CTC 直接做序列识别。端到端路线在精度上有明显上限但训练数据从几万张起步标注要求也更高。我个人的习惯是任何识别模型上线前先手动看 50 张在训练和验证里都没出现过的截图把错误样本单独导出看一眼就知道问题出在预处理还是数据分布比反复调参有效得多。做到这一步这套基于机器学习算法的验证码识别脚本才算真正在你机器上落地了。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网