简介:面向计算机视觉与网络安全自动化开发者,资源将YOLOv11目标检测与Siamese孪生网络结合,实现验证码自动识别与绕过,并通过API查询域名ICP备案状态,适用于自动化测试、图像监控与网站管理。压缩包共22个文件、约69.46MB,含python脚本、onnx模型、配置与说明文档,其中yolov11.onnx和siamese.onnx为预训练模型。已有74人学习下载,适合具备Python基础、希望快速上手验证码识别或图像相似度比对项目的学习者。资源提供PyCharm工程ICP_FLASK_YOLOY11,包括YZM.py识别脚本、GetIcp.py备案查询、API.py接口封装,附README及资料文档,便于对照源码理解具体实现。
1. 基于 YOLOv11 与 Siamese 双模型的验证码识别系统:把目标检测和相似度比对组合成一个可落地的 API 服务
很多做自动化测试或数据采集的工程师,都被验证码卡在登录这一步。硬编码规则只能解固定样式的验证码,而通用 OCR 对扭曲、粘连、带干扰线的图片效果很不稳定。这个项目的思路很直接:用 YOLOv11 先把验证码图片里的每个字符区域检测出来,再用 Siamese 孪生神经网络把检测到的字符图和模板字符图做相似度比对,最后按坐标拼接出识别结果。相比传统模板匹配,这套方案对字符位置变化、旋转和轻微形变更鲁棒,而且两个模型都已转成 ONNX,不依赖 GPU 也能跑。项目里还附带了一个基于 API 的域名 ICP 备案状态查询模块,适合在自动化流程里同时处理图像识别和域名合规检查。适合有 Python 基础、想上手 ONNX 部署和双模型串联的视觉开发者。
2. 环境与模型准备:把 yolov11.onnx 和 siamese.onnx 跑起来的第一个关键点
2.1 项目文件里哪些是核心:分清主力和赠品
拿到压缩包后我习惯先列一下目录,因为这类项目经常混着 IDE 配置文件和模型文件。核心文件是yolov11.onnx、siamese.onnx、YZM.py、GetIcp.py、API.py。yolov11.onnx是 YOLOv11 目标检测模型,负责从验证码图片中回归出每个字符的边界框和置信度;siamese.onnx是 Siamese 孪生网络模型,负责计算两张字符图像的相似度分数。YZM.py是识别验证码的主脚本,GetIcp.py封装了域名 ICP 备案查询 API,API.py是 Flask 服务入口,把识别和备案查询暴露成 HTTP 接口。
剩下的一堆文件可以按优先级处理:requirements.txt是依赖清单,必须先看;img/下的big.jpg和small.jpg是测试样例,用来快速验证模型是否加载成功;log.txt是运行日志,排查问题时有参考价值;.idea/和.iml是 PyCharm 的项目配置,换机器后可以直接删掉,不影响运行;附赠资源.docx是补充资料,主要看有没有说明模型训练细节,但和主流程不耦合。
2.2 依赖安装:onnxruntime 是核心,Flask 是服务层
这个项目的好处是模型都转成了 ONNX,不需要安装 PyTorch,依赖量很小。ONNX Runtime 是微软的推理引擎,适合 CPU 部署,不用纠结 CUDA 和 cuDNN 版本。核心依赖如下:
onnxruntime==1.17.1 Flask==3.0.0 requests==2.31.0 opencv-python==4.9.0.80 numpy==1.24.3参数说明:onnxruntime负责加载和运行两个 ONNX 模型;opencv-python用于图像读取、缩放、颜色空间转换和字符小图裁剪;requests是GetIcp.py发起 HTTP 请求用的;Flask是API.py的服务框架。numpy 建议锁在 1.x,别直接升到 2.x,因为部分旧版 ONNX 算子在 numpy 2.x 下会报类型错误,这种坑在部署现场很常见。
装完之后先别急着跑识别,先验证 ONNX 模型能不能被正确解析:
import onnxruntime as ort yolo_session = ort.InferenceSession("yolov11.onnx", providers=["CPUExecutionProvider"]) siamese_session = ort.InferenceSession("siamese.onnx", providers=["CPUExecutionProvider"]) for name, session in [("yolov11", yolo_session), ("siamese", siamese_session)]: print(f"[{name}] inputs:", [(i.name, i.shape) for i in session.get_inputs()]) print(f"[{name}] outputs:", [(o.name, o.shape) for o in session.get_outputs()])这段代码做了两件事:一是确认两个权重文件没有损坏,能正常被 ONNX Runtime 加载;二是打印输入输出节点的名称和形状。YOLOv11 模型的输入通常是images,形状可能是[1, 3, 640, 640]或动态维度;输出节点一般是output0,形状可能是[1, 84, 8400],其中的 84 可以解成 4 个坐标加 80 个类别概率。Siamese 模型的输入往往是两路input_a和input_b,输出是一个 sigmoid 后的相似度分数。打印这几行信息,后面写预处理和后处理时能少走很多弯路。
2.3 先跑通单张图片推理:YZM.py 的关键逻辑
YZM.py的流程可以分成三段:图像预处理、YOLOv11 检测、Siamese 比对。下面这是去掉业务包装后的推理骨架:
import cv2 import numpy as np import onnxruntime as ort yolo_session = ort.InferenceSession("yolov11.onnx", providers=["CPUExecutionProvider"]) siamese_session = ort.InferenceSession("siamese.onnx", providers=["CPUExecutionProvider"]) def preprocess(img): resized = cv2.resize(img, (640, 640)) blob = resized[:, :, ::-1].transpose(2, 0, 1) blob = np.ascontiguousarray(blob, dtype=np.float32) blob = blob / 255.0 return blob[None, :, :, :] img = cv2.imread("img/big.jpg") blob = preprocess(img) outputs = yolo_session.run(None, {"images": blob})这里的preprocess做了四件事:把图片缩放到 640×640;将 OpenCV 默认的 BGR 颜色空间转成 RGB;把 HWC 维度顺序转成 CHW;把像素值从 0-255 归一化到 0-1。如果你手上的模型在导出时没有做归一化,或者输入尺寸不是 640,那这段代码必须按实际模型修改。很多推理结果全是乱框的原因,就是预处理和训练时不一致。
跑通这一步之后,接着要处理 YOLOv11 的输出。ONNX 模型输出的是一个扁平的张量,需要解码成边界框。下面这段后处理是验证码场景下常用的:
def postprocess(output, conf_threshold=0.5): output = output[0] # shape: [84, 8400] candidates = [] for pred in output.T: scores = pred[4:] class_id = int(scores.argmax()) confidence = float(scores[class_id]) if confidence < conf_threshold: continue cx, cy, w, h = pred[:4] x1 = cx - w / 2 y1 = cy - h / 2 x2 = cx + w / 2 y2 = cy + h / 2 candidates.append([x1, y1, x2, y2, confidence, class_id]) return candidates参数说明:conf_threshold是置信度阈值,验证码字符如果比较清晰,设 0.5 够用;如果字符有粘连或干扰线,可以降到 0.35,但要接受更多误检。output.T是因为 YOLO 的 ONNX 输出格式通常把 8400 个候选框放在最后维度,转置后好遍历。这里没有做 NMS,只是先筛掉低置信度框。实际使用时,同一字符可能被多个框覆盖,还需要用 NMS 合并,否则后续 Siamese 会对同一个字符比对多次。
3. YOLOv11 负责定位、Siamese 负责认字:双模型流水线的工作原理
3.1 YOLOv11 把目标检测当回归问题:一次前向直接出坐标和类别
YOLO 系列的核心思想是“You Only Look Once”,把目标检测建模成一个回归问题。输入图像被划分成若干个网格,每个网格负责预测中心点落在自己区域内的目标。YOLOv11 作为该系列的新版本,网络结构上继续强化了跨阶段特征融合,对小目标、密集目标的检测性能比前几代更强。验证码里的字符往往比较小、排列密集,甚至会互相靠近,这正是 YOLOv11 相对传统模板匹配的优势所在。
传统 OCR 或模板匹配要先做字符分割,遇到粘连字符就崩。YOLOv11 直接把字符框回归出来,不需要先分割再识别。它的 Backbone 负责提取图像特征,Neck 做多尺度特征融合,Head 输出框坐标、置信度和类别概率。整个过程一次前向就完成,效率很高。在验证码场景里,YOLOv11 不是用来做最终字符分类的,而是只做粗定位,具体这个框里是什么字符,交给 Siamese 网络来判断。这个分工很关键:YOLO 负责“在哪”,Siamese 负责“是谁”。
3.2 Siamese 孪生网络:共享权重的双胞胎特征提取器
Siamese 网络的结构是两个完全相同的子网络,它们共享权重。输入两幅图像,分别经过同样的卷积和全连接层,得到两个特征向量,再计算两个向量之间的距离或余弦相似度。训练时用的损失函数一般是对比损失或三元组损失,让同类别样本的特征靠近,不同类别样本的特征远离。它本身不是分类模型,而是度量模型,回答的是“这两张图有多像”。
在这个项目里,Siamese 的用法很巧妙。先准备一组参考字符模板,比如字母和数字各若干张。然后 YOLOv11 从验证码中定位出每个字符小图,把这个小图和模板库里的每一张图都配对送入 Siamese,得分最高的模板字符就是识别结果。这种做法的扩展性比软分类好,因为新增字符类型不需要重新训练分类器,只要往模板库里加图片。它也有一个明显的缺点:模板库越大,比对次数越多。如果验证码里有 4 个字符、模板库有 80 张参考图,就要做 320 次 Siamese 推理。好在单张字符图很小,CPU 上也能接受。
3.3 将检测和比对连起来:从图片到识别文本的完整链路
完整链路是:原图进入 YOLOv11,输出候选框;每个候选框裁出字符小图;对字符小图做尺寸归一化和清理;再与模板库逐张比对;最后按 x 坐标从左到右排序,拼接出字符串。下面这段代码是流水线的核心实现:
def recognize_captcha(img, templates): blob = preprocess(img) output = yolo_session.run(None, {"images": blob})[0] boxes = postprocess(output, conf_threshold=0.5) chars = [] for box in sorted(boxes, key=lambda b: b[0]): x1, y1, x2, y2 = map(int, box[:4]) # 稍微内缩裁边,去掉字符边缘干扰 char_img = img[y1 + 2:y2 - 2, x1 + 2:x2 - 2] char_img = cv2.resize(char_img, (64, 64)) best_label, best_score = None, -1.0 for label, tmpl in templates.items(): score = compare_similarity(char_img, tmpl) if score > best_score: best_score = score best_label = label chars.append((best_label, best_score)) return chars def compare_similarity(char_img, tmpl_img): a = char_img.astype(np.float32) / 255.0 b = tmpl_img.astype(np.float32) / 255.0 a = a[None, :, :, :] b = b[None, :, :, :] sim = siamese_session.run(None, {"input_a": a, "input_b": b})[0] return float(sim[0][0])参数说明:yolo_session.run的第二个参数{"images": blob}中的images必须与第 2.2 节打印出来的输入节点名一致,否则会报错。compare_similarity里把输入名称写成了input_a和input_b,实际要以你手上的模型为准。Siamese 输出的是 0 到 1 之间的相似度,通常要设一个阈值,比如 0.7 以下不认可,避免随便拿一个低分结果凑数。下面是一个简单的 NMS 实现,建议在postprocess之后加一步:
def nms(boxes, iou_threshold=0.4): boxes = sorted(boxes, key=lambda b: b[4], reverse=True) result = [] while boxes: best = boxes.pop(0) result.append(best) boxes = [b for b in boxes if iou(best, b) < iou_threshold] return resultiou函数计算两个框的交并比,和检测框重叠度超过阈值的框都会被过滤掉。这里没有用类别过滤,因为验证码里每个字符框的类别其实不重要,甚至可以把类别维度去掉,只保留框和置信度。这个细节和通用目标检测不同,是验证码场景下的一个常见简化。
4. 避坑指南:验证码识别项目最容易翻车的五个问题
4.1 现象:加载 ONNX 模型报错,或者推理输出全为零
原因:最常见的是 onnxruntime 版本不匹配,其次是模型导出时用了 Pytorch 2.0 的新算子,在 CPU 执行 provider 上不支持。还有一种情况是输入数据的归一化方式不对。YOLO 模型如果训练时用的是 0-255 输入,你推理时除以 255,输出置信度就会趋近 0,一个框都检不到。
解决:先打印session.get_inputs()的信息,确认输入节点名和形状。然后在preprocess后面加一句打印,看输入张量的均值和标准差是否符合预期。如果模型要求 0-255,就把blob = blob / 255.0去掉。如果 onnxruntime 报不支持的算子,最简单的方式是升级 onnxruntime 到最新版,或者回到导出方要 Pytorch 的原始权重,重新导出 ONNX。
4.2 现象:检测框错位,字符框与真实字符位置对不上
原因:YOLOv11 的输入尺寸和原图尺寸不成比例,坐标缩放时没有考虑 letterbox 填充。很多项目直接把图resize到 640×640,检测完直接按比例放大,忽略了长宽比变化导致的拉伸,框自然对不上。
解决:用 letterbox 方案替代直接拉伸。先将原图按比例缩放到 640,短边用灰色填充,推理完成后把框坐标还原到原图时,去掉填充偏移。关键代码:
def letterbox_resize(img, target_size=640): h, w = img.shape[:2] scale = min(target_size / w, target_size / h) new_w, new_h = int(w * scale), int(h * scale) resized = cv2.resize(img, (new_w, new_h)) canvas = np.full((target_size, target_size, 3), 114, dtype=np.uint8) pad_x = (target_size - new_w) // 2 pad_y = (target_size - new_h) // 2 canvas[pad_y:pad_y + new_h, pad_x:pad_x + new_w] = resized return canvas, scale, pad_x, pad_y参数说明:114是 YOLO 系列在 letterbox 填充时常用的灰色像素值。坐标还原时,先减去pad_x、pad_y,再除以scale就能回到原图坐标系。如果项目原图本身就已经是固定尺寸的正方形,不做 letterbox 问题不大,但只要图片宽高比不是 1:1,就必须用 letterbox,否则字符会变形,影响后续 Siamese 比对。
4.3 现象:相似字符经常认错,比如0/O、1/I、b/6
原因:Siamese 网络靠视觉特征判断相似度,这类字符在像素层面差异很小。如果模板库字体和验证码字体不一致,或者模板图预处理方式和验证码字符图不一致,相似度分数会整体偏移。
解决:每个字符多准备几套模板,覆盖常见字体风格。比对时不是简单取最高分,而是取所有模板里的最高分,同时加入最低可接受阈值。预处理上,在进入 Siamese 前可以先做灰度化、二值化,把背景干扰线削掉。这个项目里的img/big.jpg和small.jpg可以用来测一测 Siamese 的灵敏程度,如果对这两张图的相似度判断不准,模板库和预处理都要重新调。
4.4 现象:Flask 接口每次请求都重新加载模型,内存和耗时都炸
原因:把InferenceSession(...)写进路由函数内部,每次请求都从磁盘重新读模型。ONNX 模型加载在 CPU 上耗时几百毫秒,并发一上来,服务直接卡死。
解决:把模型加载放到模块顶层,或者用一个类来管理,保证全局只有一个 Session 实例。ONNX Runtime 的 Session 是线程安全的,Flask 默认的多线程模式下可以共用。推荐方式:
class CaptchaRecognizer: def __init__(self): self.yolo = ort.InferenceSession("yolov11.onnx", providers=["CPUExecutionProvider"]) self.siamese = ort.InferenceSession("siamese.onnx", providers=["CPUExecutionProvider"]) recognizer = CaptchaRecognizer()参数说明:providers=["CPUExecutionProvider"]明确指定只用 CPU,避免在某些机器上自动选择 CUDA 版本时报缺少 dll。如果服务器有 GPU,可以改成["CUDAExecutionProvider", "CPUExecutionProvider"],但要注意 onnxruntime-gpu 版本和 CUDA 版本要匹配,否则反而更慢。
4.5 现象:识别出的字符顺序错乱,或者把干扰线当成字符
原因:YOLO 检测的是水平矩形框,字符带旋转或倾斜时,裁剪出来的区域混入背景。验证码里的干扰线如果和目标连通域连在一起,也会被框进来。排序时如果直接按检测结果原始顺序取字符,不等按 x 坐标重排,输出文本就是乱的。
解决:排序按box[0]即左上角 x 坐标,这是最基础的一步,不能省。对于干扰线问题,在裁剪字符小图时做内缩,比如img[y1+2:y2-2, x1+2:x2-2],把边框周围的像素裁掉。如果干扰线比较重,还可以在进入 Siamese 前对字符小图做一次中值滤波,把细线噪声去掉。对于倾斜字符,最稳的方案是拿到四角坐标做透视校正,但 YOLOv11 默认输出的是水平框,这个优化需要自己另接一个旋转框检测或角度回归模型,普通场景下内缩加滤波已经够用。
5. 把识别和 ICP 查询封装成 Flask API:端到端验证与进阶技巧
5.1 封装识别服务,暴露/api/captcha路由
把图像识别包装成 HTTP 接口后,调用方就不需要关心 ONNX、OpenCV 这些细节。接口接收multipart/form-data图片,返回识别文本和每个字符的相似度分数。这样自动化测试脚本可以直接用 requests 调用,不需要在测试环境里装 Python 依赖。
from flask import Flask, request, jsonify import cv2 import numpy as np app = Flask(__name__) @app.post("/api/captcha") def captcha_api(): file = request.files.get("image") if file is None: return jsonify({"error": "no image"}), 400 img_bytes = file.read() img = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) if img is None: return jsonify({"error": "decode failed"}), 400 result = recognize_captcha(img, templates) text = "".join([label for label, _ in result]) return jsonify({"text": text, "detail": result})这段代码里,request.files.get("image")的字段名要和调用方约定好,传错了会拿不到文件。imdecode返回None的情况要处理,否则下一行访问img会抛异常。返回的detail里每一项是(字符, 置信度)元组,前端可以用来做二次校验。
5.2 ICP 备案查询接口:独立路由但共享统一服务
GetIcp.py的职责是调用公开的 API 查询域名 ICP 备案状态,和验证码识别没有耦合。把它放进同一个 Flask 服务,是因为自动化场景里经常先识别验证码拿到登录态,再查询一批域名的备案状态。接口不需要额外模型,只做参数校验和结果解析。
@app.get("/api/icp") def icp_api(): domain = request.args.get("domain") if not domain: return jsonify({"error": "domain required"}), 400 data = query_icp(domain) return jsonify(data)参数说明:query_icp是GetIcp.py里封装好的函数,内部用 requests 请求备案查询接口,解析出备案号、主办单位、审核时间等字段。这个接口如果被高频调用,应该加一层简单缓存,我用字典把域名映射到查询结果和过期时间,TTL 设成 1 小时,避免重复请求公开接口导致被限流。
5.3 端到端验证:先单图再并发,最后看 CPU 曲线
服务启动后,先用 curl 验证两个接口:
curl -X POST -F "image=@img/big.jpg" http://127.0.0.1:5000/api/captcha curl "http://127.0.0.1:5000/api/icp?domain=example.com"如果/api/captcha返回空文本,优先怀疑置信度阈值太高,或者模板库里没有对应的字符类型。如果/api/icp返回超时,检查GetIcp.py里 requests 的超时参数和域名格式。接着再用并发压测看服务稳定性。我一般用ab -n 100 -c 10跑一轮,观察 CPU 占用和内存变化。ONNX Runtime 在 CPU 模式下会默认用满多核,如果服务里还有其他计算任务,可以通过intra_op_num_threads限制推理线程数,给其他业务留出资源。
从那以后,我每次部署这种双模型服务都强制走一遍完整链路:先命令行单图验证模型,再 curl 验证路由,最后压测观察资源占用。这套流程能挡住绝大多数部署翻车现场。验证码识别本质上是模型组合问题,YOLOv11 解决“在哪”,Siamese 解决“是谁”,拆开来调试每个环节,合起来才能稳定,希望帮到你。
本文还有配套的精品资源,点击获取