其实很久之前,我就对 PyTorch 挺感兴趣的,也一直琢磨着搞个目标检测或者验证码识别的项目来练练手。但拖延症大家懂的,这个计划在我的收藏夹和 ToDo List 里静静躺了快一年,每次都以“最近太忙”或者“环境配置太麻烦”为借口搁置了。

直到前阵子,看到一个需要频繁人工点击的点选类验证码(就是那种给你一张大图和几个提示字,让你按顺序在图里把对应图标点出来的验证码),我终于下定决心:是时候把这个坑给填上了。

折腾了两周,踩了无数坑,成功把模型训练出来,并在本地部署了 FastAPI 推理接口,回测成功率达到了 90% 以上。在这里做个完整的复盘,记录一下这个从无到有的过程、踩过的坑以及心得体会。


思路与架构设计

点选类验证码的识别,核心在于解决两个问题:

  1. 图标在哪里?(目标检测问题:把大图里的候选图标全部框出来)
  2. 哪个图标对应哪种颜色或文字?(特征分类问题:对框出来的图标进行颜色或类别判定,决定点击的先后顺序)

经过一番调研,我设计了如下的工作流:

  1. 数据采集:通过 API 从某测试平台并发抓取原始的验证码图片和点击顺序指令。
  2. 数据标注:因为人工画框太痛苦,我先利用图像处理(HSV 色彩过滤)搞了一套自动标注脚本,再配合 X-AnyLabeling 进行微调和清洗,事半功倍。
  3. 模型训练:使用目前非常成熟且开箱即用的 YOLOv8(Ultralytics)进行目标检测,训练出能精准框出大图中所有小图标的模型。
  4. 模型导出:把 PyTorch 的 .pt 权重导出为 .onnx 格式,不仅能脱离 PyTorch 的庞大依赖,还能利用 ONNX Runtime 实现毫秒级的推理响应。
  5. 颜色匹配推理:YOLO 框出图标坐标后,我们利用 HSV 空间的颜色直方图特征做颜色匹配,对提取出来的图标和目标指令进行比对,最终输出精准的点击顺序坐标。
  6. API 服务部署:基于 FastAPI 搭建 HTTP 识别服务,实现秒级点选坐标输出。

从项目结构上看,我最后把它拆成了几个比较清晰的模块:

  • Collector:负责采集验证码图片和提示图。
  • TrainerAgent:负责 YOLOv8 训练、指标提取和最佳权重管理。
  • CaptchaInferencer:负责 ONNX/PT 模型加载、图标检测、颜色匹配和点击顺序输出。
  • OrchestratorAgent:把采集、推理、回测串起来,方便单次识别和批量验证。
  • FastAPI 服务:提供 HTTP 接口,对外返回点击坐标。

这样拆完之后,训练、调参、压测和服务化就不会混成一锅粥,后面要替换模型或调整颜色匹配策略也比较方便。


从无到有的实现过程

1. 自动标注的妙用:解救画框奴隶

搞深度学习最头疼的就是数据标注。如果几百张图都要人眼看、鼠标拉框,我估计在这一步就直接放弃了。

观察验证码可以发现:大图里的背景虽然是大理石花纹,但图标的颜色饱和度非常高,呈现明显的单色状态。于是我尝试用 OpenCV 转换到 HSV 空间进行饱和度通道(S通道)的分割:

1
2
3
4
5
6
7
# 核心思路:图标通常饱和度极高,背景饱和度极低
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
_, s, _ = cv2.split(hsv)
# 设定阈值过滤背景噪点,二值化
_, thresh = cv2.threshold(s, 210, 255, cv2.THRESH_BINARY)
# 寻找轮廓并生成 YOLO 格式的标注框
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

这套自动过滤脚本(项目中的 auto_label.py)不是只写死一个阈值,而是会动态尝试多个饱和度阈值;如果本地已有可用 YOLO 模型,还会优先用模型辅助标注,失败后再回退到 HSV 分割。它直接帮我自动完成了 95% 以上的画框工作!随后我只需要用 X-AnyLabeling 打开图片目录,花十几分钟微调一下极少数没对齐的框,并删除一些崩坏的数据,就完成了高质的标注数据集。

2. 训练 YOLOv8 模型

环境方面,我配置了 CUDA 12.8 和 PyTorch 2.11 运行库。在数据清洗完成后,运行划分训练集与验证集的脚本,生成 dataset.yaml 配置文件,就可以开始正式训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from ultralytics import YOLO

# 加载 YOLOv8 纳米模型作为预训练权重
model = YOLO("yolov8n.pt")

# 开始训练
results = model.train(
data="data/labeled/dataset.yaml",
epochs=150,
imgsz=256,
batch=32,
device=0, # 使用 GPU
workers=0 # Windows 下关闭多进程加载
)

后面我把训练流程封装成了 TrainerAgent,默认训练 150 轮、imgsz=256batch=32,并且在 Windows 下把 workers=0 固定下来,避免多进程数据加载引发虚拟内存或句柄相关问题。实际训练下来,模型在验证集上的 mAP50 很快就逼近了 0.99,定位图标的效果已经非常稳。

3. PT 模型导出为 ONNX

PyTorch 的模型文件虽然方便,但在部署到轻量服务器或不想安装复杂依赖的环境时比较笨重。我们将训练出来的 best.pt 导出为 best.onnx

1
2
# 导出为 ONNX 格式并启用简化的 ONNX 模型架构
model.export(format="onnx", simplify=True)

实际推理时,项目会优先加载 models/best.onnx,ONNX Runtime 走 GPU Provider,失败时再回退 CPU;如果没有 ONNX 文件,则回退到 best.pt。使用 ONNX Runtime 后,单次图片的目标框提取耗时压到了 10ms 以内,速度非常惊艳。


踩坑记录与解决方案

在这个过程中,我也经历了数次调试和推倒重来,记录几个最典型的坑:

坑一:背景花纹干扰颜色判定,导致点击顺序错乱

现象:YOLO 目标检测框出图标的准确率接近 100%,但在并发回测时,经常把“黄色图标”判定为“蓝色”,或者反过来,导致点击顺序不对,整体成功率一度只有 60% 左右。

排查:我可视化了提取出来的图标切片,发现 YOLO 检测的边界框稍微偏大了一点点,把图标外圈大理石背景的蓝色/黄色线条也包了进去。我的色彩判定逻辑(ColorMatcher)在计算颜色均值和距离时,被外圈的背景噪点带偏了。

解决

  1. 缩框限制:在提取图标切片用于色彩分析前,先对边界框进行收缩(收缩为中心 90% 的区域),只截取图标的内圈核心像素:
    1
    2
    3
    # 边界框中心收缩
    x1_shrink = cx - w_box * 0.45
    x2_shrink = cx + w_box * 0.45
  2. 提高饱和度过滤:在颜色识别的像素提取逻辑中,提高 HSV 的饱和度下限,将 SAT_MIN 从 30 直接拉高到 210。这样在比对颜色时,背景的大理石花纹像素会被全部过滤为背景色,只统计图标本身的高饱和度像素。
    修改后,色彩判定的准确度直线上升,回测成功率瞬间飙到了 90%+

下面是模型实际的目标检测预测效果:

YOLOv8 预测定位图

坑二:YOLO 的训练缓存导致训练退化

现象:在微调和重新清理数据集后,重新开始训练时,经常发现模型的 Loss 异常大,或者在某些 epoch 后指标突然崩塌。

排查:后来发现 YOLO 在首次读取数据集后,会在 labels/train/labels/val/ 目录下生成同名的 .cache 缓存文件。当我在本地修改了数据集或手动删除了部分噪点 .txt 标注后,YOLO 依然在读取过期的旧缓存,里面包含了可能高宽为 0 的脏数据或已经被物理删除的样本指针。

解决:在我的 dataset_prepare.py 数据准备脚本中,加入了每次重分数据集时自动搜索并彻底物理删除 *.cache 文件的逻辑:

1
2
3
4
# 清理已有的 YOLO .cache 缓存文件,防止 YOLO 缓存旧的坐标尺寸
for parent in [LABELS_TRAIN.parent, LABELS_VAL.parent]:
for cache_file in parent.glob("*.cache"):
cache_file.unlink()

坑三:Windows 下的字符集编码地狱

现象:为了方便一键启动,我写了几个 .bat 批处理文件。然而运行的时候,命令行频繁报错,甚至连 cd 命令都因为路径里的中文变成乱码而失效。

排查:Windows 的 CMD 默认使用 GBK 编码(活动代码页 936),而我们在现代编辑器中写出来的脚本全都是 UTF-8,导致 CMD 执行时对中文路径进行错字拼合或分词错误。

解决:在 .bat 开头加入 chcp 65001 >nul 切换为 UTF-8 代码页,并将 .bat 的文件保存编码强制转为 Windows 本地的 GBK/ANSI 编码,双击运行终于稳定。


最终效果与心得

在解决了上面几个关键问题后,我写了一个并发压测回测脚本(测试.py),对系统进行 100 次的连续实测,最终效果如下:

并发回测 100 次的最终结果

模型训练过程中产出的召回率曲线(Recall-Confidence Curve)也非常漂亮。在置信度为 0.85 以下时,Recall 召回率维持在完美的 1.00,这意味着我们完全不会遗漏任何一个候选图标:

Recall-Confidence 曲线图

个人心得:

  1. 不要过早优化算法,先优化数据:在这次项目中,我最开始花了很多时间去折腾各种复杂的聚类色彩判定和分类模型,结果成功率很低。后来才发现,仅仅是因为标注框稍微偏大引入了背景噪声。把框收紧、把数据洗干净,比换什么高级模型都有用。
  2. 写好工具链事半功倍:像 visualize_labels.py(随机画红线抽检标注)和 dataset_prepare.py 这种工具脚本,在反复迭代和微调数据的过程中省下了我极其庞大的时间。
  3. PyTorch 没想象中那么难:以前总觉得机器学习和深度学习门槛极高,但真正动手从零折腾一个项目、去解决现实中的具体问题时,你会发现一步步查文档、写脚本、调试模型,其实和普通业务搬砖一样,重点是“动手去做”。

开源地址

项目已在 GitHub 上开源,欢迎交流与学习:

👉 CaptchaTrainingCase 开源仓库地址

(注:本项目及相关博文仅供学术交流与个人研究使用,不得用于任何非法或侵入性用途。)


如果你也在折腾 AI、深度学习、自动化识别或者工程实践,可以关注我的公众号,后面我会继续记录这些折腾过程。

墨鱼札记公众号二维码