☰
基于YOLOv11与Siamese孪生网络的验证码识别及ICP备案查询自动化
2026/10/7 6:08:22 网站建设 项目流程

简介:一套融合YOLOv11目标检测与Siamese孪生神经网络的实战项目包,面向计算机视觉、验证码识别与自动化测试场景,适合具备Python和OpenCV基础的中级开发者参考。资源采用zip压缩,共22个文件、69.46MB,主体包含Python源码、onnx推理模型、XML配置及说明文档:py脚本覆盖目标检测、图像相似度比对和ICP备案API调用,onnx模型可直接加载运行,docx与md说明了环境部署与使用流程,jpg样例图便于快速验证。已有74人学习,内容涵盖模型推理、验证码自动识别与绕过、域名备案状态查询的完整链路,并附requirements.txt、pyc缓存与项目工程配置,目录结构清晰,可作为算法选型、系统集成或课程设计的有力参考。

1. 先拆技术栈:YOLOv11检测、Siamese比对、ICP查询这条链路在解决什么

验证码是Web自动化里绕不开的坎:扭曲字符、干扰线、背景噪声,传统OCR方案在这种图上前期处理还没做完,准确率先掉了一半。这个标题给出的组合思路很直接——基于深度学习的目标检测与图像相似度比对系统,把“找字符”和“认字符”拆成两个独立环节:YOLOv11负责检测验证码里的每个字符位置,Siamese孪生神经网络负责把裁剪出来的字符图和模板库做相似度比对,识别结果再作为参数传给API接口查询域名ICP备案状态,三个模块串成一条自动处理链路。

这套方案适合三类人:做Web自动化时被验证码卡住、想脱离打码平台的技术人员;研究小样本图像分类、对孪生网络方向感兴趣的深度学习入门者,这一波正好在搜yolov11环境配置和训练自己模型,可以当成一个可落地的实验项目照着做;以及需要在自建业务里批量核实域名备案状态、又不想人工登录反复查询的开发者。整条链路涉及的技术点只有目标检测、图像相似度比对和HTTP接口调用,门槛没有想象高,但坑一点不少。

2. YOLOv11目标检测:从ultralytics环境配置到字符检测模型训练

2.1 yolov11(ultralytics)环境配置:适合0基础纯小白的安装顺序

YOLOv11实际是ultralytics这套框架在YOLOv8之后迭代出的检测模型,训练、验证、预测、导出全走同一个包,不需要自己搭检测头。热榜上问“yolov11(ultralytics)环境配置”“适合0基础纯小白”的人很多,核心问题是安装顺序不对,导致torch和CUDA版本打架。

先创建虚拟环境再装依赖,顺序不要乱:

conda create -n yolo11 python=3.11 -y conda activate yolo11 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install ultralytics

装完之后跑一句验证,能打印版本号就说明环境通了:

python -c "from ultralytics import YOLO; print(YOLO.__version__)" nvidia-smi | grep "CUDA Version"

先说为什么先把torch装上再装ultralytics:ultralytics的依赖里会自动带一份CPU版torch,如果你先装ultralytics再装GPU版torch,很可能被覆盖或冲突。用conda建一个干净的3.11环境,torch单独装,等于给后续的翻车提前买了道保险。cuda11.8还是cu121可以根据nvidia-smi里的驱动版本挑,驱动够新就cu121。验证这步别跳过,打印不出版本号直接排查torch,不要急着改模型代码。

2.2 字符检测数据集:标注、格式转换与YOLO训练配置文件

验证码字符检测的数据集可以自己标。用labelimg打开验证码图片,框住每个字符,类别统一叫char,导出格式选YOLO。每张图对应一个同名txt,一行一个目标,五个数字:类别ID、归一化中心x、归一化中心y、归一化宽、归一化高。

import os import cv2 def voc_to_yolo(xml_path, img_path, out_dir): img = cv2.imread(img_path) h, w = img.shape[:2] # 解析VOC格式的xml,这里省略xml解析细节 boxes = parse_xml(xml_path) # [(xmin, ymin, xmax, ymax), ...] lines = [] for xmin, ymin, xmax, ymax in boxes: cx = (xmin + xmax) / 2 / w cy = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines))

这段脚本把VOC格式的标注转成YOLO格式,关键在归一化:cx、cy、w、h全部除以原图宽高,而不是除以目标尺寸。很多标注工具直接导出的是像素绝对值,如果不归一化,YOLO训练时会把框解释到图像外面去,loss再低也不出正确结果。

数据准备好之后,写一个data.yaml:

train: ./dataset/images val: ./dataset/images nc: 1 names: ['char']

train和val的路径可以用相对路径,py文件所在目录放data.yaml,模型训练脚本在同一目录跑,路径解析不容易出错。

2.3 训练参数与推理结果保存:三个必调参数和两种验证方式

训练命令很短,但参数要会调。以字符检测为例:

from ultralytics import YOLO model = YOLO("yolov11n.yaml") model.train( data="dataset.yaml", epochs=100, imgsz=640, batch=16, device=0, patience=20, )

核心参数只有几个,重点说说:

参数建议值理由
imgsz640验证码字符普遍只有几十像素,属于典型小目标,分辨率低了字符直接糊掉
epochs80-120字符检测任务特征简单,100个epoch通常够;patience设20做早停
batch8-16看显存,6G以下用8,12G以上才能上16;batch对检测精度影响不如对分类大
device0指定第一块GPU;没有GPU就填cpu,训练时间翻好几倍,但能跑通

训练完会打印模型保存路径,best.pt和last.pt都在runs/detect目录下。验证推理用best.pt,别用last.pt,last是最后一步的存档,通常不如果断早停时的best。

推理这块注意一下“yolov11保存推理结果”的姿势:

from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model("captcha.png", conf=0.5, save=True, save_dir="output") for r in results: for box in r.boxes.xyxy: print(box.tolist())

save=True会把检测框画到原图上保存到save_dir,适合人工看效果;如果要看模型在哪些位置产生响应,可以用results[0].plot()生成带框图片,也可以用model.predict的save_txt=True导出坐标文本。想进一步理解模型关注区域,可以输出特征图热力图,改走model(..., embed=True)或者用ultralytics内部的feature_visualization方法。这里只说结论:热力图能帮你判断模型是在看字符还是看干扰线,如果激活区域集中在背景纹理上,说明数据里干扰图太多,模型学偏了。

3. Siamese孪生神经网络:验证码字符相似度比对的原理与PyTorch实现

3.1 为什么验证码字符识别适合用孪生网络而不是分类网络

先回答一个高频疑问:验证码字符就几十类,用普通CNN分类不行吗?能行,但有个硬约束——每个类别需要足够的训练样本。验证码字符的难点在于同一字符在不同验证码里字体、倾斜、粗细都不一样,分类网络要覆盖这种多样性,每个字符至少得标几百张,标注成本一下子失控。

孪生网络换了个思路:不直接学“这个字符是哪个类”,而是学“两张图是不是属于同一个字符”。输入是一对图像,两个分支共享同一套权重,分别提特征,再算两个特征向量的距离。距离小于阈值就是同一类,大于阈值就是不同类。这样训练数据从“每类几百张”变成“成对的样本”,同一张图既能跟同类图组成正样本对,又能跟异类图组成负样本对,样本利用率高很多。

这个特性让它天然适合验证码识别:准备一个只有几十张的字符模板库,每个字符两三张干净图,推理时把验证码裁剪出的字符图跟模板库里的图逐一做相似度比对,取距离最近的那一个模板作为结果。这就是标题里“图像相似度比对”落地的位置。

3.2 从零写一个轻量孪生网络:网络结构、对比损失与训练循环

不依赖现成框架,用PyTorch直接写一个能跑的结构:

import torch import torch.nn as nn import torch.nn.functional as F class SiameseNet(nn.Module): def __init__(self, feature_dim=128): super().__init__() self.cnn = nn.Sequential( nn.Conv2d(1, 16, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), ) self.fc = nn.Sequential( nn.Flatten(), nn.Linear(64 * 8 * 8, feature_dim), ) def forward_one(self, x): return self.fc(self.cnn(x)) def forward(self, x1, x2): # 两个输入走同一套权重,共享参数 out1 = self.forward_one(x1) out2 = self.forward_one(x2) return out1, out2

forward里两个输入都调forward_one,参数完全共享,这就是“孪生”的意思。特征维度feature_dim取128,对于几十个字符类别来说足够了,太大反而让距离计算变迟钝。

对比损失(Contrastive Loss)是训练孪生网络的核心:

def contrastive_loss(out1, out2, label, margin=1.0): # label=1表示同一类,label=0表示不同类 dist = F.pairwise_distance(out1, out2) loss = label * dist.pow(2) + (1 - label) * F.relu(margin - dist).pow(2) return loss.mean()

损失分两部分:正样本对(label=1)希望距离趋近0,负样本对(label=0)希望距离大于margin。margin越大,网络被逼迫把不同类拉得越开,但过大也会让训练不稳定,一般从1.0开始试。

训练循环按标准写法组织:

optimizer = torch.optim.Adam(siamese.parameters(), lr=1e-3) for epoch in range(60): total_loss = 0 for batch in dataloader: img1, img2, label = batch # 每个batch包含成对的字符图 out1, out2 = siamese(img1, img2) loss = contrastive_loss(out1, out2, label, margin=1.0) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() print(f"epoch {epoch}, loss {total_loss / len(dataloader):.4f}")

训练数据建议每张图都做一次小范围随机平移、旋转和亮度扰动,让网络学到“同一个字符变一点样子还是同一个字符”,而不是死记像素。这比堆更多原图效果更明显,是孪生网络训练里性价比最高的技巧。

3.3 推理阶段距离阈值怎么定:玄学背后的正负样本分布

训练完之后,保存每个模板字符的特征向量,推理时只算输入图跟所有模板向量的余弦相似度或欧氏距离,取最近的那个。这里有个“玄学”问题:阈值设多少算找到,设多少算不认识?

我的做法是准备一小组验证数据,跑一遍正样本对和负样本对的距离,把两边的距离直方图画出来:

import numpy as np def choose_threshold(siamese, template_feats, val_data): pos_dists, neg_dists = [], [] for img, temp_feat, is_same in val_data: feat = siamese.forward_one(img) dist = F.pairwise_distance(feat, temp_feat).item() if is_same: pos_dists.append(dist) else: neg_dists.append(dist) # 取正负分布交叉点作为阈值 return (np.mean(pos_dists) + np.mean(neg_dists)) / 2

阈值卡在正负样本距离分布的中间位置,比拍脑袋可靠得多。如果两边的分布大量重叠,说明特征没学好,回去检查数据增强和margin,而不是硬调阈值。阈值这个东西在孪生网络里本来就带点半玄学性质,但把它建立在分布数据上,至少心里有底。

4. 验证码自动识别与处理:把YOLOv11检测和Siamese比对串成一条链路

4.1 预处理先做减法:灰度、二值化与干扰线的取舍

验证码图片进YOLO之前先做一遍轻量预处理。常见做法是灰度化、去背景、适度去噪声。

import cv2 import numpy as np def preprocess_captcha(path): img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) # 大津法二值化,自动找阈值 _, thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 去孤立噪点,3x3卷积核 thresh = cv2.medianBlur(thresh, 3) return thresh

关键在“取舍”两个字:二值化能把背景和干扰线去掉,但也会把字符的细笔画一起腐蚀掉。Siamse网络吃的是特征不是像素,与其把图洗得干干净净再喂模型,不如保留灰度图上的纹理信息。实际工程里我一般会同时保留二值图和灰度图:二值图给YOLO做检测定位,灰度图给Siamese做特征比对,这样两边都吃到最合适的数据。

4.2 用YOLOv11定位字符并排序裁剪

验证码字符检测出来的框是无序的,YOLO不负责排序。识别结果要按从左到右拼成字符串,所以裁剪前必须按x坐标排序。

from ultralytics import YOLO yolo = YOLO("runs/detect/train/weights/best.pt") captcha = cv2.imread("captcha.png") results = yolo("captcha.png", conf=0.4) boxes = [] for r in results: for box in r.boxes.xyxy: x1, y1, x2, y2 = box.tolist() boxes.append([x1, y1, x2, y2]) # 按x坐标升序排,得到字符顺序 boxes.sort(key=lambda b: b[0]) chars = [] for x1, y1, x2, y2 in boxes: padding = 4 crop = captcha[max(0, int(y1) - padding):int(y2) + padding, max(0, int(x1) - padding):int(x2) + padding] chars.append(crop)

padding加4个像素是为了把字符边缘的残缺部分包进来,避免把笔画裁在边界上。如果字符间距很近,padding太大会把相邻字符的边带进来,这个值要根据实际图微调,经验上4到8像素是安全区间。

4.3 端到端识别脚本:检测裁剪到比对输出的完整代码

两个模型都训练好后,串起来的完整推理链路是:

import torch import torch.nn.functional as F from siamese_model import SiameseNet # 复用第3章的结构 # 加载模板库特征,模板库里每个字符放2-3张干净图 template_names = ['A', 'B', 'C', 'D', 'E', 'F', ...] template_feats = torch.load("template_feats.pt") # shape [num_templates, 128] siamese = SiameseNet() siamese.load_state_dict(torch.load("siamese_best.pt")) siamese.eval() result_text = "" with torch.no_grad(): for crop in chars: # 统一缩放到模型输入尺寸 crop = cv2.resize(crop, (64, 64)) crop = cv2.cvtColor(crop, cv2.COLOR_GRAY2RGB) if len(crop.shape) == 2 else crop x = torch.tensor(crop / 255.0, dtype=torch.float32).unsqueeze(0).unsqueeze(0) feat = siamese.forward_one(x) # 和所有模板特征算距离,取最近的 dists = torch.cdist(feat, template_feats) idx = torch.argmin(dists).item() result_text += template_names[idx]

识别结果拼接成完整字符串后,可以直接当验证码答案用。这里有一个容易被忽略的点:阈值判断。argmin取最近的模板没错,但如果最近距离都大于阈值,说明这个字符不在模板库里,强行用argmin只会输出错误结果。在线上一旦出现这种识别置信度低的字符,最稳妥的做法是让业务层直接判定本次识别失败,重新拉取验证码再试一轮。

5. 通过API接口查询域名ICP备案状态:请求构造、响应解析与联动重试

5.1 查询接口的请求参数设计:超时、请求头与查询字段

验证码识别结果拿到后,下一步就是通过API接口查询域名ICP备案状态。这部分的接口一般有两种:官方公开的备案查询服务,以及各类数据服务商提供的封装接口。不管接哪种,请求设计套路一致:

import requests import time def query_icp(domain, verify_code): url = "https://api.example.com/icp/query" # 以实际接口文档为准 params = { "domain": domain, "code": verify_code, # 验证码识别结果,有些接口做人机校验用 "page": "1", "pageSize": "10", } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Referer": "https://example.com/", } resp = requests.get(url, params=params, headers=headers, timeout=10) return resp.json()

timeout务必设置,默认的requests不设超时会一直挂着,批量查询时一个接口卡死整个任务。请求头里User-Agent和Referer是很多接口校验的基础字段,不带的很容易直接被拒。接口对接前先通读文档,确认查询参数名是domain还是host,是GET还是POST,这步不做就靠试错,会浪费大量时间。

5.2 响应数据结构与备案状态字段解析

备案查询接口的响应通常返回顶层code和msg,业务数据放在data字段里。data里的核心字段一般是备案号、备案主体名称、备案类型、审核时间、当前状态:

data = resp.get("data", {}) if not data: print("无备案记录或查询失败:", resp.get("msg")) return None record = data.get("record", {}) icp_number = record.get("icpNumber") # 如 京ICP备12345678号 subject = record.get("subjectName") # 备案主体名称 status = record.get("status") # 常见值:normal / expired / pending

解析时别用data["record"]这种硬索引,要习惯data.get("record", {}),接口偶尔会缺字段或者返回空结构,硬索引会在某个凌晨任务里突然抛KeyError。返回结构里的字段名各家不一定一致,但语义差不多:备案号、主体、状态是核心三要素。核对一下这三项跟预期是否匹配,就能判断域名备案是否有效。

5.3 与验证码识别模块联动:查不到结果时自动重识别重查

这整条链路的闭环价值就在这一节:验证码识别失败不代表任务失败,而是触发重试机制。实际跑的时候,验证码识别不可能每次都对,尤其遇到字体变形的字符。与其等人工介入,不如设计一层联动重试:

for attempt in range(3): captcha_resp = fetch_captcha() # 拉取新的验证码 code = recognize_captcha(captcha_resp) # YOLO+Siamese识别 if not code or len(code) != 4: time.sleep(random.uniform(1, 2)) continue result = query_icp(target_domain, code) if result.get("code") == 0: return parse_icp(result) # 查询接口提示验证码错误,重新拉图重识 time.sleep(random.uniform(1.5, 3)) raise RuntimeError("三次尝试后仍未查询成功")

这里两个细节值得说:每次重试之间加随机延时,不只是sleep固定值,而是用random.uniform生成1到2秒的随机间隔。固定间隔容易被接口侧风控识别成脚本行为,随机延时是业界共识的低成本反误判手段。另一个细节是“拉新验证码”而不是“用同一张图反复试”,验证码识别接口对同一张图频繁提交大概率直接拉黑,换新图才是正道。

6. 避坑与排查:训练、推理、接口调用最容易翻车的四个位置

6.1 YOLO坐标归一化的坑:loss正常但框全偏

现象:训练loss曲线漂亮,跑到验证集上一看,检测框全部偏移半张图。原因:标注工具导出的是绝对像素坐标,直接写进txt没除以原图宽高。解决:写脚本检查txt,把每行的cx原图宽、cy原图高反向还原,对比原图核对坐标是否落在字符中心,这一步5分钟能做完,但能救下后续几十小时的训练。

6.2 孪生网络loss不收敛:先查正负样本比例再调margin

现象:loss训练到第五十个epoch还挂在0.7上下抖。原因:负样本对太多或margin设得太大,正样本对的梯度被淹没。解决:先把batch里正负样本比例调到1比1,margin从1.0改到0.8再训一轮;还不收敛就检查输入归一化——模板图用归一化预处理,训练图也得做同样处理,预处理不一致是“loss不降”排名第一的隐藏原因。

6.3 预处理用力过猛:模板库高分、线上低分的经典翻车

现象:模板库回测识别率99%,换到真实验证码图片直接掉到一半。原因:模板库是干净裁剪图,线上验证码带干扰线,二值化把字符笔画腐蚀断,特征直接崩掉。解决:Siamese的输入改成灰度图而不是二值图,灰度图保留笔画细节,干扰线交给模型自己适应。这条也是我做这方向时最深刻的踩坑记忆。

6.4 查询接口被限流:随机延时与指数退避

现象:连续查询十几条域名后,接口开始返回“请求过于频繁”或验证码错误。原因:请求频率超过接口阈值,IP被临时风控。解决:每条查询之间sleep一到两秒,失败时用指数退避重试,第一次等3秒,第二次等9秒,再失败就停手换时段。做批量查询前先估算总量,算一下最慢需要多久,再决定是否要分批。

这四类问题基本覆盖了从图像识别到接口调用的整条链路。我自己做这类系统时养成一个习惯:每一版改动只动一个变量,比如只调margin、只动预处理、只改重试策略,不允许多个变量同时变化。原因是出了问题你根本说不清是哪个调整导致的,排查成本比调试成本高得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询