diff --git a/dist/rainyun_qinglong/README.md b/dist/rainyun_qinglong/README.md index 7c991c5..a9cdcc7 100644 --- a/dist/rainyun_qinglong/README.md +++ b/dist/rainyun_qinglong/README.md @@ -1,6 +1,7 @@ -# 雨云 (Rainyun) 自动登录与每日签到 (青龙面板平铺单目录部署包) +# 雨云 (Rainyun) 自动登录与每日签到 (青龙面板纯 ONNX 轻量部署包) -本目录为纯净的**单层平铺目录**结构,无需嵌套子文件夹,无需 `uv` 或 `venv`,可直接压缩上传至青龙面板或服务器运行。 +本目录为纯净的**单层平铺目录**结构,已全面迁移至 **ONNX Runtime 轻量推理引擎**。 +彻底移除了体积庞大的 `torch`、`torchvision` 与 `ultralytics`(依赖体积从 1.5GB 骤降至 15MB),可在青龙面板或任何小内存 VPS 容器中秒级安装运行。 --- @@ -11,11 +12,11 @@ rainyun_qinglong/ ├── rainyun_checkin.py # 核心签到脚本 (青龙定时任务入口) ├── client.py # 腾讯验证码协议客户端 ├── crypto.py # 腾讯 TDC 混沌虚拟机 32 轮魔改 XTEA 纯 Python 还原 -├── solver.py # AI 视觉检测推理引擎 (YOLOv8 + ResNet18) +├── solver.py # 纯 ONNX 视觉检测推理引擎 (无 torch 依赖) ├── slicer.py # 提示图投影切片 ├── server.py # (可选) 独立 HTTP 验证码微服务 -├── yolo_detector.pt # YOLO 图标候选定位权重文件 -├── icon_classifier.pt # ResNet18 图标分类识别权重文件 +├── yolo_detector.onnx # YOLO 图标候选定位 ONNX 权重 (约 12MB) +├── icon_classifier.onnx # ResNet18 图标分类识别 ONNX 权重 (约 43MB) ├── .env # 本地配置文件 (格式: RAINYUN_ACCOUNT="账号#密码") ├── .env.example # 环境变量示例文件 └── README.md # 本说明文档 @@ -29,17 +30,16 @@ rainyun_qinglong/ 将本目录下的所有文件上传至青龙面板的脚本目录: `/ql/data/scripts/` (新版青龙)或 `/ql/scripts/` (旧版青龙)。 -### 2. 在青龙后台安装依赖 +### 2. 在青龙后台安装依赖 (体积仅数十 MB,极速安装) 进入青龙面板 ->【依赖管理】->【Python3】-> 点击【添加依赖】,依次添加并安装: ```text httpx -torch -torchvision -ultralytics +onnxruntime opencv-python-headless pillow numpy ``` +> **注意**:已彻底无需安装 `torch`、`torchvision`、`ultralytics`,免除了 G 级大包撑爆磁盘和内存的风险! ### 3. 配置账号环境变量 在青龙面板【环境变量】添加变量: @@ -48,6 +48,6 @@ numpy ### 4. 添加定时任务 在青龙面板【定时任务】点击【新建任务】: -- **任务名**:`雨云每日签到` +- **名称**:`雨云每日签到` - **命令**:`task rainyun_checkin.py` - **定时规则**:`30 8 * * *` (每天早上 8:30 自动执行) diff --git a/dist/rainyun_qinglong/icon_classifier.pt b/dist/rainyun_qinglong/icon_classifier.onnx similarity index 78% rename from dist/rainyun_qinglong/icon_classifier.pt rename to dist/rainyun_qinglong/icon_classifier.onnx index fbee80f..ba3983e 100644 Binary files a/dist/rainyun_qinglong/icon_classifier.pt and b/dist/rainyun_qinglong/icon_classifier.onnx differ diff --git a/dist/rainyun_qinglong/solver.py b/dist/rainyun_qinglong/solver.py index 3ae4aa1..d5331f3 100644 --- a/dist/rainyun_qinglong/solver.py +++ b/dist/rainyun_qinglong/solver.py @@ -1,10 +1,11 @@ +# -*- coding: utf-8 -*- """ -腾讯天御点选验证码工业级端到端识别求解器 (Solver v2.1 - 12类精确拓扑版) -架构设计: -1. 提示词解析: 垂直/水平投影切割 + 12 种基准图标模板归一化互相关 (TM_CCOEFF_NORMED) 比对 -> 100% 确定 3 个待点击目标类别序列 [T1, T2, T3] -2. 候选检测: YOLOv8n (conf=0.03) 召回全图所有图标区域 (召回率 100%) + 空间 NMS 去重 (<=20px) -3. 精准分类: ResNet18 迁移学习分类器一次性 Batch 预测所有候选区域的类别概率分布 (13 维向量) -4. 全局匹配: 全局最大后验概率分配 -> 输出毫秒级精准有序点击坐标 [(x1, y1), (x2, y2), (x3, y3)] +腾讯天御点选验证码工业级纯 ONNX 视觉求解器 (Solver v3.0 - Pure ONNX Edition) +特性: +1. 零 PyTorch / 零 Ultralytics 依赖:采用 onnxruntime 推理引擎 (从 1.5GB 骤降至 15MB) +2. 提示词解析: 垂直/水平投影切片 + ResNet18 ONNX 分类器直接预测目标序列 +3. 候选图标定位: YOLOv8n ONNX + Letterbox 自适应缩放 + OpenCV cv2.dnn.NMSBoxes 精准去重 +4. 全局匹配: 后验概率分布优化 + 背景干扰项抑制因子 + 全局非冲突组合搜索,输出有序像素点击坐标 """ import io @@ -20,12 +21,8 @@ if str(CURRENT_DIR) not in sys.path: import cv2 import numpy as np -import torch -import torch.nn as nn -import torchvision.models as models -import torchvision.transforms as transforms +import onnxruntime as ort from PIL import Image -from ultralytics import YOLO try: from slicer import slice_prompt_image @@ -35,87 +32,54 @@ except ImportError: logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger("solver") -# 优先直接在当前平铺目录下查找权重文件,兼容本地与模型子目录 -YOLO_PATH = CURRENT_DIR / "yolo_detector.pt" -if not YOLO_PATH.exists(): - YOLO_PATH = CURRENT_DIR / "models/yolo_detector.pt" +# 权重路径解析 (优先当前目录,兼容 models/ 子目录) +YOLO_ONNX_PATH = CURRENT_DIR / "yolo_detector.onnx" +if not YOLO_ONNX_PATH.exists(): + YOLO_ONNX_PATH = CURRENT_DIR / "models/yolo_detector.onnx" -CLASSIFIER_PATH = CURRENT_DIR / "icon_classifier.pt" -if not CLASSIFIER_PATH.exists(): - CLASSIFIER_PATH = CURRENT_DIR / "models/icon_classifier.pt" - -TEMPLATES_DIR = CURRENT_DIR / "templates" - -NUM_ICON_CLASSES = 12 -TOTAL_CLASSES = 13 +CLASSIFIER_ONNX_PATH = CURRENT_DIR / "icon_classifier.onnx" +if not CLASSIFIER_ONNX_PATH.exists(): + CLASSIFIER_ONNX_PATH = CURRENT_DIR / "models/icon_classifier.onnx" class CaptchaSolver: - def __init__(self, yolo_path: Path = YOLO_PATH, classifier_path: Path = CLASSIFIER_PATH): - self.device = torch.device("mps" if torch.backends.mps.is_available() else "cpu") - - # 1. 加载 YOLO 目标检测器 (定位图标候选位置) + def __init__(self, yolo_path: Path = YOLO_ONNX_PATH, classifier_path: Path = CLASSIFIER_ONNX_PATH): if not yolo_path.exists(): - raise FileNotFoundError(f"YOLO detector weights not found: {yolo_path}") - self.detector = YOLO(str(yolo_path)) - - # 2. 加载 ResNet18 图标分类器 (13 类: 0~11 图标类, 12 背景干扰) + raise FileNotFoundError(f"YOLO ONNX 权重文件不存在: {yolo_path}") if not classifier_path.exists(): - raise FileNotFoundError(f"Icon classifier weights not found: {classifier_path}") + raise FileNotFoundError(f"Classifier ONNX 权重文件不存在: {classifier_path}") - self.classifier = models.resnet18(weights=None) - self.classifier.fc = nn.Linear(self.classifier.fc.in_features, TOTAL_CLASSES) - self.classifier.load_state_dict(torch.load(classifier_path, map_location=self.device)) - self.classifier = self.classifier.to(self.device) - self.classifier.eval() + # 配置轻量级 CPU 推理会话 + sess_options = ort.SessionOptions() + sess_options.inter_op_num_threads = 2 + sess_options.intra_op_num_threads = 2 + sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL - self.transform = transforms.Compose([ - transforms.Resize((64, 64)), - transforms.ToTensor(), - transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), - ]) + self.yolo_sess = ort.InferenceSession(str(yolo_path), sess_options, providers=["CPUExecutionProvider"]) + self.cls_sess = ort.InferenceSession(str(classifier_path), sess_options, providers=["CPUExecutionProvider"]) - # 3. 预加载 12 个干净基准模板 - self.templates = [] - for cid in range(NUM_ICON_CLASSES): - t_path = TEMPLATES_DIR / f"class_{cid}.png" - if t_path.exists(): - t_img = Image.open(t_path).convert("L") - arr = np.array(t_img) - bg = np.median(arr) - bin_t = cv2.resize(((arr < bg - 20).astype(np.uint8) * 255), (48, 48), interpolation=cv2.INTER_AREA) - self.templates.append((cid, bin_t)) + @staticmethod + def _preprocess_patch(img: Image.Image) -> np.ndarray: + """对小图标图像进行 64x64 归一化预处理 (匹配 ResNet18 输入标准)""" + resized = img.convert("RGB").resize((64, 64), Image.Resampling.BILINEAR) + arr = np.array(resized, dtype=np.float32) / 255.0 + mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) + std = np.array([0.229, 0.224, 0.225], dtype=np.float32) + norm = (arr - mean) / std + return np.transpose(norm, (2, 0, 1)) # (3, 64, 64) - def parse_prompt_targets(self, prompt_img: str | Path | bytes | Image.Image) -> List[int]: - """ - 自动识别并解析提示图中 3 个目标图标的真实类别 ID (0~11) - 支持干净模板匹配与 ResNet18 分类器自动双模降级 - """ + def parse_prompt_targets(self, prompt_img: Union[str, Path, bytes, Image.Image]) -> List[int]: + """自动切片并解析提示图中 3 个目标图标的类别 ID (0~11)""" crops = slice_prompt_image(prompt_img) if not crops: return [] - if self.templates: - target_classes = [] - for crop_img, _ in crops[:3]: - arr = np.array(crop_img.convert("L")) - bg = np.median(arr) - bin_c = cv2.resize(((arr < bg - 20).astype(np.uint8) * 255), (48, 48), interpolation=cv2.INTER_AREA) + patch_blobs = np.stack([self._preprocess_patch(c[0]) for c in crops[:3]], axis=0).astype(np.float32) + logits = self.cls_sess.run(None, {"input": patch_blobs})[0] # shape: (N, 13) + return [int(np.argmax(l[:12])) for l in logits] - scores = [cv2.matchTemplate(bin_c, t[1], cv2.TM_CCOEFF_NORMED)[0, 0] for t in self.templates] - best_cid = int(np.argmax(scores)) - target_classes.append(best_cid) - return target_classes - - # 模板缺失时自动使用 ResNet18 分类器直接预测提示小图类别 - patch_tensors = torch.stack([self.transform(c[0].convert("RGB")) for c in crops[:3]]).to(self.device) - with torch.no_grad(): - logits = self.classifier(patch_tensors) - probs = torch.softmax(logits, dim=1).cpu().numpy() - return [int(np.argmax(p[:12])) for p in probs] - - def _extract_candidates(self, bg_img: str | Path | bytes | Image.Image) -> List[Tuple[int, int, Image.Image]]: - """从背景图中提取所有去重后的候选图标 (cx, cy, patch)""" + def _extract_candidates(self, bg_img: Union[str, Path, bytes, Image.Image]) -> List[Tuple[int, int, Image.Image]]: + """利用 YOLOv8n ONNX 召回背景图所有图标候选位置并去重""" if isinstance(bg_img, Image.Image): bg_pil = bg_img.convert("RGB") elif isinstance(bg_img, bytes): @@ -123,19 +87,44 @@ class CaptchaSolver: else: bg_pil = Image.open(bg_img).convert("RGB") - # Ultralytics detector natively accepts PIL.Image - results = self.detector(bg_pil, conf=0.03, verbose=False) - boxes = results[0].boxes.xyxy.cpu().numpy() + orig_w, orig_h = bg_pil.size + img_np = np.array(bg_pil) - w_img, h_img = bg_pil.size + # 1. Letterbox 保持宽高比填充缩放至 640x640 + r = min(640.0 / orig_h, 640.0 / orig_w) + new_w, new_h = int(round(orig_w * r)), int(round(orig_h * r)) + resized = cv2.resize(img_np, (new_w, new_h), interpolation=cv2.INTER_LINEAR) + dw = (640.0 - new_w) / 2.0 + dh = (640.0 - new_h) / 2.0 + top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) + left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) + padded = cv2.copyMakeBorder(resized, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(114, 114, 114)) + + blob = padded.astype(np.float32) / 255.0 + blob = np.transpose(blob, (2, 0, 1))[None, ...] + + # 2. ONNX 目标检测前向推理 + yolo_out = self.yolo_sess.run(None, {"images": blob})[0][0].T # (8400, 5) + valid = yolo_out[yolo_out[:, 4] >= 0.03] + + boxes_xywh = [] + scores = [] + for row in valid: + cx, cy, w, h, conf = row + x1 = (cx - w / 2.0 - dw) / r + y1 = (cy - h / 2.0 - dh) / r + boxes_xywh.append([int(x1), int(y1), int(w / r), int(h / r)]) + scores.append(float(conf)) + + # 3. OpenCV NMS 筛选重叠边界框 + indices = cv2.dnn.NMSBoxes(boxes_xywh, scores, score_threshold=0.03, nms_threshold=0.45) raw_cands = [] + if len(indices) > 0: + for idx in indices: + x, y, w, h = boxes_xywh[idx] + raw_cands.append((int(x + w / 2.0), int(y + h / 2.0))) - for box in boxes: - x1, y1, x2, y2 = box - cx, cy = int((x1 + x2) / 2), int((y1 + y2) / 2) - raw_cands.append((cx, cy)) - - # 空间去重: 距离 < 22px 的框合并为一个中心点 + # 4. 空间中心点近邻聚类去重 (<=22px) dedup_cands = [] for cx, cy in raw_cands: if not any(np.hypot(cx - dcx, cy - dcy) < 22 for dcx, dcy in dedup_cands): @@ -143,16 +132,20 @@ class CaptchaSolver: candidates = [] for cx, cy in dedup_cands: - crop_box = (max(0, cx - 32), max(0, cy - 32), min(w_img, cx + 32), min(h_img, cy + 32)) + crop_box = (max(0, cx - 32), max(0, cy - 32), min(orig_w, cx + 32), min(orig_h, cy + 32)) patch = bg_pil.crop(crop_box) candidates.append((cx, cy, patch)) return candidates - def solve(self, bg_img: str | Path | bytes | Image.Image, prompt_img: str | Path | bytes | Image.Image) -> List[Tuple[int, int]]: + def solve( + self, + bg_img: Union[str, Path, bytes, Image.Image], + prompt_img: Union[str, Path, bytes, Image.Image], + ) -> List[Tuple[int, int]]: """ - 端到端求解验证码点选坐标 - 返回 3 个目标的像素坐标: [(x1, y1), (x2, y2), (x3, y3)] + 端到端纯 ONNX 求解验证码点选坐标 + 返回 3 个目标的像素坐标序列: [(x1, y1), (x2, y2), (x3, y3)] """ # 1. 确定提示图的目标序列 [T1, T2, T3] target_classes = self.parse_prompt_targets(prompt_img) @@ -166,15 +159,14 @@ class CaptchaSolver: logger.warning(f"背景图检出候选图标数量不足: {len(candidates)}") return [] - # 3. 批量前向推理计算候选图标的类别概率分布 - patch_tensors = torch.stack([self.transform(c[2]) for c in candidates]).to(self.device) - with torch.no_grad(): - logits = self.classifier(patch_tensors) - probs = torch.softmax(logits, dim=1).cpu().numpy() # [N_candidates, 13] + # 3. 批量推理计算候选图标的类别概率分布 (13 维) + patch_blobs = np.stack([self._preprocess_patch(c[2]) for c in candidates], axis=0).astype(np.float32) + logits = self.cls_sess.run(None, {"input": patch_blobs})[0] + exp = np.exp(logits - np.max(logits, axis=1, keepdims=True)) + probs = exp / np.sum(exp, axis=1, keepdims=True) - # 4. 全局最佳非冲突分配 (结合背景抑制因子 1 - P(distractor)) - # icon_score = P(candidate = Target_Class) * (1 - P(candidate = Distractor)) - bg_probs = probs[:, 12] # [N_candidates] + # 4. 全局后验概率优化 (结合背景干扰项抑制因子 1 - P(distractor)) + bg_probs = probs[:, 12] clean_probs = probs[:, :12] * (1.0 - bg_probs[:, None]) num_cands = len(candidates) @@ -183,40 +175,31 @@ class CaptchaSolver: cand_indices = range(num_cands) for c1, c2, c3 in itertools.permutations(cand_indices, 3): - score = clean_probs[c1, target_classes[0]] * clean_probs[c2, target_classes[1]] * clean_probs[c3, target_classes[2]] + score = ( + clean_probs[c1, target_classes[0]] + * clean_probs[c2, target_classes[1]] + * clean_probs[c3, target_classes[2]] + ) if score > best_score: best_score = score best_comb = (c1, c2, c3) if best_comb is None: used = set() - best_comb_list = [] + comb_list = [] for t_cls in target_classes: cand_probs = clean_probs[:, t_cls].copy() for u in used: cand_probs[u] = -1.0 best_c = int(np.argmax(cand_probs)) - best_comb_list.append(best_c) + comb_list.append(best_c) used.add(best_c) - best_comb = tuple(best_comb_list) + best_comb = tuple(comb_list) # 5. 输出有序点击坐标 - result_points = [] - for c_idx in best_comb: - cx, cy, _ = candidates[c_idx] - result_points.append((cx, cy)) - - return result_points + return [(candidates[idx][0], candidates[idx][1]) for idx in best_comb] if __name__ == "__main__": solver = CaptchaSolver() - sample_id = sys.argv[1] if len(sys.argv) > 1 else "1789135559041_4170" - bg = Path(f"tcaptcha/data/raw/{sample_id}_bg.png") - prompt = Path(f"tcaptcha/data/raw/{sample_id}_prompt.png") - - if bg.exists() and prompt.exists(): - pts = solver.solve(bg, prompt) - print(f"Sample {sample_id} 预测点击坐标: {pts}") - else: - print(f"样本不存在: {sample_id}") + print("✓ CaptchaSolver ONNX 引擎初始化成功!") diff --git a/dist/rainyun_qinglong/yolo_detector.onnx b/dist/rainyun_qinglong/yolo_detector.onnx new file mode 100644 index 0000000..39e3923 Binary files /dev/null and b/dist/rainyun_qinglong/yolo_detector.onnx differ diff --git a/dist/rainyun_qinglong/yolo_detector.pt b/dist/rainyun_qinglong/yolo_detector.pt deleted file mode 100644 index e2329fa..0000000 Binary files a/dist/rainyun_qinglong/yolo_detector.pt and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index 97ea410..2039a60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ dependencies = [ "ddddocr>=1.6.1", "httpx>=0.28.1", "numpy>=2.5.3", + "onnx>=1.22.0", "opencv-python-headless>=5.0.0.93", "pillow>=12.3.0", "ultralytics>=8.4.147", diff --git a/tcaptcha/models/icon_classifier.onnx b/tcaptcha/models/icon_classifier.onnx new file mode 100644 index 0000000..ba3983e Binary files /dev/null and b/tcaptcha/models/icon_classifier.onnx differ diff --git a/tcaptcha/models/yolo_detector.onnx b/tcaptcha/models/yolo_detector.onnx new file mode 100644 index 0000000..39e3923 Binary files /dev/null and b/tcaptcha/models/yolo_detector.onnx differ diff --git a/tcaptcha/solver.py b/tcaptcha/solver.py index 892fa0b..7300945 100644 --- a/tcaptcha/solver.py +++ b/tcaptcha/solver.py @@ -1,10 +1,11 @@ +# -*- coding: utf-8 -*- """ -腾讯天御点选验证码工业级端到端识别求解器 (Solver v2.1 - 12类精确拓扑版) -架构设计: -1. 提示词解析: 垂直/水平投影切割 + 12 种基准图标模板归一化互相关 (TM_CCOEFF_NORMED) 比对 -> 100% 确定 3 个待点击目标类别序列 [T1, T2, T3] -2. 候选检测: YOLOv8n (conf=0.03) 召回全图所有图标区域 (召回率 100%) + 空间 NMS 去重 (<=20px) -3. 精准分类: ResNet18 迁移学习分类器一次性 Batch 预测所有候选区域的类别概率分布 (13 维向量) -4. 全局匹配: 全局最大后验概率分配 -> 输出毫秒级精准有序点击坐标 [(x1, y1), (x2, y2), (x3, y3)] +腾讯天御点选验证码工业级纯 ONNX 视觉求解器 (Solver v3.0 - Pure ONNX Edition) +特性: +1. 零 PyTorch / 零 Ultralytics 依赖:采用 onnxruntime 推理引擎 (从 1.5GB 骤降至 15MB) +2. 提示词解析: 垂直/水平投影切片 + ResNet18 ONNX 分类器直接预测目标序列 +3. 候选图标定位: YOLOv8n ONNX + Letterbox 自适应缩放 + OpenCV cv2.dnn.NMSBoxes 精准去重 +4. 全局匹配: 后验概率分布优化 + 背景干扰项抑制因子 + 全局非冲突组合搜索,输出有序像素点击坐标 """ import io @@ -14,98 +15,74 @@ import sys from pathlib import Path from typing import List, Tuple, Union -PROJECT_ROOT = Path(__file__).resolve().parent.parent +CURRENT_DIR = Path(__file__).resolve().parent +PROJECT_ROOT = CURRENT_DIR.parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) +if str(CURRENT_DIR) not in sys.path: + sys.path.insert(0, str(CURRENT_DIR)) import cv2 import numpy as np -import torch -import torch.nn as nn -import torchvision.models as models -import torchvision.transforms as transforms +import onnxruntime as ort from PIL import Image -from ultralytics import YOLO -from tcaptcha.slicer import slice_prompt_image +try: + from slicer import slice_prompt_image +except ImportError: + from tcaptcha.slicer import slice_prompt_image logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger("solver") -YOLO_PATH = PROJECT_ROOT / "tcaptcha/models/yolo_detector.pt" -CLASSIFIER_PATH = PROJECT_ROOT / "tcaptcha/models/icon_classifier.pt" -TEMPLATES_DIR = PROJECT_ROOT / "tcaptcha/data/templates" +# 权重路径解析 (优先 models/ 目录,兼容当前目录) +YOLO_ONNX_PATH = CURRENT_DIR / "models/yolo_detector.onnx" +if not YOLO_ONNX_PATH.exists(): + YOLO_ONNX_PATH = CURRENT_DIR / "yolo_detector.onnx" -NUM_ICON_CLASSES = 12 -TOTAL_CLASSES = 13 +CLASSIFIER_ONNX_PATH = CURRENT_DIR / "models/icon_classifier.onnx" +if not CLASSIFIER_ONNX_PATH.exists(): + CLASSIFIER_ONNX_PATH = CURRENT_DIR / "icon_classifier.onnx" class CaptchaSolver: - def __init__(self, yolo_path: Path = YOLO_PATH, classifier_path: Path = CLASSIFIER_PATH): - self.device = torch.device("mps" if torch.backends.mps.is_available() else "cpu") - - # 1. 加载 YOLO 目标检测器 (定位图标候选位置) + def __init__(self, yolo_path: Path = YOLO_ONNX_PATH, classifier_path: Path = CLASSIFIER_ONNX_PATH): if not yolo_path.exists(): - raise FileNotFoundError(f"YOLO detector weights not found: {yolo_path}") - self.detector = YOLO(str(yolo_path)) - - # 2. 加载 ResNet18 图标分类器 (13 类: 0~11 图标类, 12 背景干扰) + raise FileNotFoundError(f"YOLO ONNX 权重文件不存在: {yolo_path}") if not classifier_path.exists(): - raise FileNotFoundError(f"Icon classifier weights not found: {classifier_path}") + raise FileNotFoundError(f"Classifier ONNX 权重文件不存在: {classifier_path}") - self.classifier = models.resnet18(weights=None) - self.classifier.fc = nn.Linear(self.classifier.fc.in_features, TOTAL_CLASSES) - self.classifier.load_state_dict(torch.load(classifier_path, map_location=self.device)) - self.classifier = self.classifier.to(self.device) - self.classifier.eval() + # 配置轻量级 CPU 推理会话 + sess_options = ort.SessionOptions() + sess_options.inter_op_num_threads = 2 + sess_options.intra_op_num_threads = 2 + sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL - self.transform = transforms.Compose([ - transforms.Resize((64, 64)), - transforms.ToTensor(), - transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), - ]) + self.yolo_sess = ort.InferenceSession(str(yolo_path), sess_options, providers=["CPUExecutionProvider"]) + self.cls_sess = ort.InferenceSession(str(classifier_path), sess_options, providers=["CPUExecutionProvider"]) - # 3. 预加载 12 个干净基准模板 - self.templates = [] - for cid in range(NUM_ICON_CLASSES): - t_path = TEMPLATES_DIR / f"class_{cid}.png" - if t_path.exists(): - t_img = Image.open(t_path).convert("L") - arr = np.array(t_img) - bg = np.median(arr) - bin_t = cv2.resize(((arr < bg - 20).astype(np.uint8) * 255), (48, 48), interpolation=cv2.INTER_AREA) - self.templates.append((cid, bin_t)) + @staticmethod + def _preprocess_patch(img: Image.Image) -> np.ndarray: + """对小图标图像进行 64x64 归一化预处理 (匹配 ResNet18 输入标准)""" + resized = img.convert("RGB").resize((64, 64), Image.Resampling.BILINEAR) + arr = np.array(resized, dtype=np.float32) / 255.0 + mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) + std = np.array([0.229, 0.224, 0.225], dtype=np.float32) + norm = (arr - mean) / std + return np.transpose(norm, (2, 0, 1)) # (3, 64, 64) - def parse_prompt_targets(self, prompt_img: str | Path | bytes | Image.Image) -> List[int]: - """ - 自动识别并解析提示图中 3 个目标图标的真实类别 ID (0~11) - 支持干净模板匹配与 ResNet18 分类器自动双模降级 - """ + def parse_prompt_targets(self, prompt_img: Union[str, Path, bytes, Image.Image]) -> List[int]: + """自动切片并解析提示图中 3 个目标图标的类别 ID (0~11)""" crops = slice_prompt_image(prompt_img) if not crops: return [] - if self.templates: - target_classes = [] - for crop_img, _ in crops[:3]: - arr = np.array(crop_img.convert("L")) - bg = np.median(arr) - bin_c = cv2.resize(((arr < bg - 20).astype(np.uint8) * 255), (48, 48), interpolation=cv2.INTER_AREA) + patch_blobs = np.stack([self._preprocess_patch(c[0]) for c in crops[:3]], axis=0).astype(np.float32) + logits = self.cls_sess.run(None, {"input": patch_blobs})[0] # shape: (N, 13) + return [int(np.argmax(l[:12])) for l in logits] - scores = [cv2.matchTemplate(bin_c, t[1], cv2.TM_CCOEFF_NORMED)[0, 0] for t in self.templates] - best_cid = int(np.argmax(scores)) - target_classes.append(best_cid) - return target_classes - - # 模板缺失时自动使用 ResNet18 分类器直接预测提示小图类别 - patch_tensors = torch.stack([self.transform(c[0].convert("RGB")) for c in crops[:3]]).to(self.device) - with torch.no_grad(): - logits = self.classifier(patch_tensors) - probs = torch.softmax(logits, dim=1).cpu().numpy() - return [int(np.argmax(p[:12])) for p in probs] - - def _extract_candidates(self, bg_img: str | Path | bytes | Image.Image) -> List[Tuple[int, int, Image.Image]]: - """从背景图中提取所有去重后的候选图标 (cx, cy, patch)""" + def _extract_candidates(self, bg_img: Union[str, Path, bytes, Image.Image]) -> List[Tuple[int, int, Image.Image]]: + """利用 YOLOv8n ONNX 召回背景图所有图标候选位置并去重""" if isinstance(bg_img, Image.Image): bg_pil = bg_img.convert("RGB") elif isinstance(bg_img, bytes): @@ -113,19 +90,44 @@ class CaptchaSolver: else: bg_pil = Image.open(bg_img).convert("RGB") - # Ultralytics detector natively accepts PIL.Image - results = self.detector(bg_pil, conf=0.03, verbose=False) - boxes = results[0].boxes.xyxy.cpu().numpy() + orig_w, orig_h = bg_pil.size + img_np = np.array(bg_pil) - w_img, h_img = bg_pil.size + # 1. Letterbox 保持宽高比填充缩放至 640x640 + r = min(640.0 / orig_h, 640.0 / orig_w) + new_w, new_h = int(round(orig_w * r)), int(round(orig_h * r)) + resized = cv2.resize(img_np, (new_w, new_h), interpolation=cv2.INTER_LINEAR) + dw = (640.0 - new_w) / 2.0 + dh = (640.0 - new_h) / 2.0 + top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) + left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) + padded = cv2.copyMakeBorder(resized, top, bottom, left, right, cv2.BORDER_CONSTANT, value=(114, 114, 114)) + + blob = padded.astype(np.float32) / 255.0 + blob = np.transpose(blob, (2, 0, 1))[None, ...] + + # 2. ONNX 目标检测前向推理 + yolo_out = self.yolo_sess.run(None, {"images": blob})[0][0].T # (8400, 5) + valid = yolo_out[yolo_out[:, 4] >= 0.03] + + boxes_xywh = [] + scores = [] + for row in valid: + cx, cy, w, h, conf = row + x1 = (cx - w / 2.0 - dw) / r + y1 = (cy - h / 2.0 - dh) / r + boxes_xywh.append([int(x1), int(y1), int(w / r), int(h / r)]) + scores.append(float(conf)) + + # 3. OpenCV NMS 筛选重叠边界框 + indices = cv2.dnn.NMSBoxes(boxes_xywh, scores, score_threshold=0.03, nms_threshold=0.45) raw_cands = [] + if len(indices) > 0: + for idx in indices: + x, y, w, h = boxes_xywh[idx] + raw_cands.append((int(x + w / 2.0), int(y + h / 2.0))) - for box in boxes: - x1, y1, x2, y2 = box - cx, cy = int((x1 + x2) / 2), int((y1 + y2) / 2) - raw_cands.append((cx, cy)) - - # 空间去重: 距离 < 22px 的框合并为一个中心点 + # 4. 空间中心点近邻聚类去重 (<=22px) dedup_cands = [] for cx, cy in raw_cands: if not any(np.hypot(cx - dcx, cy - dcy) < 22 for dcx, dcy in dedup_cands): @@ -133,16 +135,20 @@ class CaptchaSolver: candidates = [] for cx, cy in dedup_cands: - crop_box = (max(0, cx - 32), max(0, cy - 32), min(w_img, cx + 32), min(h_img, cy + 32)) + crop_box = (max(0, cx - 32), max(0, cy - 32), min(orig_w, cx + 32), min(orig_h, cy + 32)) patch = bg_pil.crop(crop_box) candidates.append((cx, cy, patch)) return candidates - def solve(self, bg_img: str | Path | bytes | Image.Image, prompt_img: str | Path | bytes | Image.Image) -> List[Tuple[int, int]]: + def solve( + self, + bg_img: Union[str, Path, bytes, Image.Image], + prompt_img: Union[str, Path, bytes, Image.Image], + ) -> List[Tuple[int, int]]: """ - 端到端求解验证码点选坐标 - 返回 3 个目标的像素坐标: [(x1, y1), (x2, y2), (x3, y3)] + 端到端纯 ONNX 求解验证码点选坐标 + 返回 3 个目标的像素坐标序列: [(x1, y1), (x2, y2), (x3, y3)] """ # 1. 确定提示图的目标序列 [T1, T2, T3] target_classes = self.parse_prompt_targets(prompt_img) @@ -156,15 +162,14 @@ class CaptchaSolver: logger.warning(f"背景图检出候选图标数量不足: {len(candidates)}") return [] - # 3. 批量前向推理计算候选图标的类别概率分布 - patch_tensors = torch.stack([self.transform(c[2]) for c in candidates]).to(self.device) - with torch.no_grad(): - logits = self.classifier(patch_tensors) - probs = torch.softmax(logits, dim=1).cpu().numpy() # [N_candidates, 13] + # 3. 批量推理计算候选图标的类别概率分布 (13 维) + patch_blobs = np.stack([self._preprocess_patch(c[2]) for c in candidates], axis=0).astype(np.float32) + logits = self.cls_sess.run(None, {"input": patch_blobs})[0] + exp = np.exp(logits - np.max(logits, axis=1, keepdims=True)) + probs = exp / np.sum(exp, axis=1, keepdims=True) - # 4. 全局最佳非冲突分配 (结合背景抑制因子 1 - P(distractor)) - # icon_score = P(candidate = Target_Class) * (1 - P(candidate = Distractor)) - bg_probs = probs[:, 12] # [N_candidates] + # 4. 全局后验概率优化 (结合背景干扰项抑制因子 1 - P(distractor)) + bg_probs = probs[:, 12] clean_probs = probs[:, :12] * (1.0 - bg_probs[:, None]) num_cands = len(candidates) @@ -173,40 +178,31 @@ class CaptchaSolver: cand_indices = range(num_cands) for c1, c2, c3 in itertools.permutations(cand_indices, 3): - score = clean_probs[c1, target_classes[0]] * clean_probs[c2, target_classes[1]] * clean_probs[c3, target_classes[2]] + score = ( + clean_probs[c1, target_classes[0]] + * clean_probs[c2, target_classes[1]] + * clean_probs[c3, target_classes[2]] + ) if score > best_score: best_score = score best_comb = (c1, c2, c3) if best_comb is None: used = set() - best_comb_list = [] + comb_list = [] for t_cls in target_classes: cand_probs = clean_probs[:, t_cls].copy() for u in used: cand_probs[u] = -1.0 best_c = int(np.argmax(cand_probs)) - best_comb_list.append(best_c) + comb_list.append(best_c) used.add(best_c) - best_comb = tuple(best_comb_list) + best_comb = tuple(comb_list) # 5. 输出有序点击坐标 - result_points = [] - for c_idx in best_comb: - cx, cy, _ = candidates[c_idx] - result_points.append((cx, cy)) - - return result_points + return [(candidates[idx][0], candidates[idx][1]) for idx in best_comb] if __name__ == "__main__": solver = CaptchaSolver() - sample_id = sys.argv[1] if len(sys.argv) > 1 else "1789135559041_4170" - bg = Path(f"tcaptcha/data/raw/{sample_id}_bg.png") - prompt = Path(f"tcaptcha/data/raw/{sample_id}_prompt.png") - - if bg.exists() and prompt.exists(): - pts = solver.solve(bg, prompt) - print(f"Sample {sample_id} 预测点击坐标: {pts}") - else: - print(f"样本不存在: {sample_id}") + print("✓ CaptchaSolver ONNX 引擎初始化成功!") diff --git a/uv.lock b/uv.lock index 5a4d989..0dbf4dc 100644 --- a/uv.lock +++ b/uv.lock @@ -2,9 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.12" resolution-markers = [ - "sys_platform == 'win32'", - "sys_platform != 'darwin' and sys_platform != 'win32'", - "sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'darwin'", ] [[package]] @@ -240,7 +246,7 @@ name = "cuda-bindings" version = "13.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, + { name = "cuda-pathfinder" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/d6/d0/76d0e45d98bf4933bf48eac6bbeb17464684540f69edec41fc37c7a422b0/cuda_bindings-13.4.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84ee88862e2e6ac39a5434c061f7f4389fbefbc418487d0670c86601d517d038", size = 6479976, upload-time = "2026-09-10T01:16:54.622Z" }, @@ -271,43 +277,43 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cufft = [ - { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cufile = [ - { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] curand = [ - { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cusolver = [ - { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] [[package]] @@ -435,6 +441,7 @@ dependencies = [ { name = "ddddocr" }, { name = "httpx" }, { name = "numpy" }, + { name = "onnx" }, { name = "opencv-python-headless" }, { name = "pillow" }, { name = "ultralytics" }, @@ -445,6 +452,7 @@ requires-dist = [ { name = "ddddocr", specifier = ">=1.6.1" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "numpy", specifier = ">=2.5.3" }, + { name = "onnx", specifier = ">=1.22.0" }, { name = "opencv-python-headless", specifier = ">=5.0.0.93" }, { name = "pillow", specifier = ">=12.3.0" }, { name = "ultralytics", specifier = ">=8.4.147" }, @@ -726,6 +734,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/d4/97c19b77e0a6e3b48581185bb65088f431cd20186076cc0f650a1757ea46/matplotlib-3.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987", size = 9213472, upload-time = "2026-07-18T03:39:37.141Z" }, ] +[[package]] +name = "ml-dtypes" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/72/307d7c4bd0600601c7133fba5cb78af7db968152951c1cd473abb1cda782/ml_dtypes-0.6.0.tar.gz", hash = "sha256:5e60251d32ced5598972e4d5e06a2f044341f9291402551a3f6f0ec44f9299b0", size = 3032327, upload-time = "2026-08-13T14:14:40.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/6a/441eb053b078954f7fea284dfb288701884d0a1404d39babb858e1649023/ml_dtypes-0.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5359c588cc62de6f78d7430f06b65853d884955494d86d6ad90b6dd64a3f3a08", size = 565447, upload-time = "2026-08-13T14:14:01.737Z" }, + { url = "https://files.pythonhosted.org/packages/ed/cf/87e8a6c57eed63a91782a0d229856ddf73e138ce004dd71e2799a9dcdb33/ml_dtypes-0.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37da32aa97749251025666d62372775019594577b9c9e9cfda83bed48d778fdb", size = 360227, upload-time = "2026-08-13T14:14:02.938Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f9/7d76c1eae866f5d4636401b31b6d6dd90e4b4ced1fa7cfdfcca9c60e4bd3/ml_dtypes-0.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b4a480aa8fd54a1805b8ac10f3f91763926a74f73c0c364c10f9231854f4170", size = 409890, upload-time = "2026-08-13T14:14:04.248Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/9c61ec2760b5cbfb1c6558d5c991a6d8fd3271053c32db20506a9a90272b/ml_dtypes-0.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a3e9d53925597fbffafd2a37048dadeddd0bdaba58058f6ae0869ed709a184d", size = 439333, upload-time = "2026-08-13T14:14:05.501Z" }, + { url = "https://files.pythonhosted.org/packages/6a/57/780ca3e5ab135b9fbdd8e5441abf5f801b30398371b691291e05ab9834c0/ml_dtypes-0.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:6eaed129a4afe90694b8685e2f9b6294849f5eda4af9a15be83a4326eeebd775", size = 552268, upload-time = "2026-08-13T14:14:06.866Z" }, + { url = "https://files.pythonhosted.org/packages/50/51/fd1582b8f5ed8a9e7be0e161a6ea0dff70cb280479a12178df0b3a72700e/ml_dtypes-0.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:084dfe51a7ad58b171f05115f8226ed4233a454a1611371947e806e76f0c638d", size = 565468, upload-time = "2026-08-13T14:14:08.5Z" }, + { url = "https://files.pythonhosted.org/packages/d2/22/20fd70ca6ed12446cb92d5b2a7745bd185f9d8b8cdeeadad976574398e6b/ml_dtypes-0.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28d676428b104bb9717b0928bc5c5129f2d6b51b6727587cc4289e7bf8713cb5", size = 360232, upload-time = "2026-08-13T14:14:09.873Z" }, + { url = "https://files.pythonhosted.org/packages/89/a5/da8ae6c6f1babe4b68e3e55d43d39b529e29774f10e0910671a6b8c86eb8/ml_dtypes-0.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26b1f1fa4f0435a2946859823f6e2bf06796f1e9f10f5a05b08a5e3c8f46ff69", size = 410169, upload-time = "2026-08-13T14:14:11.036Z" }, + { url = "https://files.pythonhosted.org/packages/e2/55/4561acefa00fa4bcbfb82ca6a48578b41f372cd7dd7cdd6eb4720abc2e5f/ml_dtypes-0.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:fb87f46b4f7ad7b5d3ad8f4b452b024bd4229d44c8ff934798c1fe656210387a", size = 439357, upload-time = "2026-08-13T14:14:12.172Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5d/6a01538e507ef0ed5e879985b13a92467bf8960696fb1131f8b8cadc60ff/ml_dtypes-0.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:57ed0d6b4ac5e7868361303a9c57fbcf63b768236ee14456f585dfcf260d0292", size = 552278, upload-time = "2026-08-13T14:14:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/97dc35667b7c9db33c5344c673cd27f87e34771875ea7100138726132ac9/ml_dtypes-0.6.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:84fa136b8602c8c39e3b6cb24918960cd6f36cade7a70376f56770729cd56510", size = 562551, upload-time = "2026-08-13T14:14:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/db/48/77f0ede10558d0d935da2e3276ed7e9c8cc2bad3463b9a0b66b03fc60be2/ml_dtypes-0.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:317be9967fb84b0ce4e80e6b1bf71213d21971621cf6f1e501a63602a95297bf", size = 360334, upload-time = "2026-08-13T14:14:16.079Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b1/1831dd8c9b06c013085d31a2ac4f03392d43bd36bfc6ff591a08bcedc1cf/ml_dtypes-0.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f490c003369ce60e514a0c3b12374f05274c101fee1bead6740ec8a564032b0", size = 409966, upload-time = "2026-08-13T14:14:17.477Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ad/9c32c53f823dda3742df19a79c10bc198365937873ea125ba65747440c23/ml_dtypes-0.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:d574c2b28921dc72e869df248f1a278f6eee176a1f237c8642e1a71eb15f3977", size = 457224, upload-time = "2026-08-13T14:14:18.608Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/dd98205418a13353d41c52bf5326d8cbec515aace46174e23c6ea01c2978/ml_dtypes-0.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:f4adb4af61516510d786cf8c01851a66f6d3ddfa79e1144deaa5b40d8507231e", size = 568378, upload-time = "2026-08-13T14:14:19.843Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/32e7beef3281fed74883451477ad976364323206dbfaa95e948ba788dac7/ml_dtypes-0.6.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3e169214e0d80ff1c038e1b3017e33c23e43bdf948d42d31de8283111c7e2fa3", size = 590177, upload-time = "2026-08-13T14:14:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/99b3d9b3c984b3bd1e81d8244f1fa2f812e44060d853205b2df6271aa17c/ml_dtypes-0.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:573b11f3c327e17ef3826d266e676cf1149a1f3016f822a05f2306c55d8246bf", size = 363142, upload-time = "2026-08-13T14:14:22.463Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fb/8091c0aee7f2712de99c7fd4b1642382644dec6a4962effe4f5b9d16a973/ml_dtypes-0.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b76fa1d3f92967d58289ac47ab7458ede66e6f3527fff3e59142aee57d9307cd", size = 430645, upload-time = "2026-08-13T14:14:23.737Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/962d2c589513b5930d05b6eae5fbd22ad8bbcf26bb763449f3d8f912360f/ml_dtypes-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3be9911d953f97cddded4b9961d7b650473b7e55806d20f6176f8356dfe7b38e", size = 465667, upload-time = "2026-08-13T14:14:25.04Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/bcb25e246edd19af5fa1cf6267040bd9977a7afca846e6cfd4a52078b44f/ml_dtypes-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e74266ca8e97874a937b7646378c178025650a236584f7474d10d8086a6edea3", size = 572706, upload-time = "2026-08-13T14:14:26.296Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/46cb442648e3c774d8cb25f2e1e41d496cdcc91fbe9c2a6f75c0b8df7af6/ml_dtypes-0.6.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:b1b503864fada3f74fabf8d9fee7b4c1cbe956301e6fdece975d5f77c2fce958", size = 562550, upload-time = "2026-08-13T14:14:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/07/56/844eff5af7a2d1a09d75df12c70225c3a6b6a771f95876b2bf5f7d10ad44/ml_dtypes-0.6.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c6ad60af4102789a5c09824004beade2f7f28cd1cd581ee5c170d9dc2fbb00e", size = 360332, upload-time = "2026-08-13T14:14:28.767Z" }, + { url = "https://files.pythonhosted.org/packages/b6/29/b7165a3a76364a5baa6aa4ee82a0adf73a3c014b8cd126120b62cc087992/ml_dtypes-0.6.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4f1b9329a251e4affe3bb58f4d3e2db22a714396fd7ffb40d0b5db423c24d17", size = 409964, upload-time = "2026-08-13T14:14:30.023Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2e/f61c54a0544b6a170ac1bb89bcf406af53fb2deffc5476b6d2d3df5ba13e/ml_dtypes-0.6.0-cp315-cp315-win_amd64.whl", hash = "sha256:488c99ab181a2f59d9ec3b12c5fa11ec904e92be2c4ba18cded54dd7501208fe", size = 457249, upload-time = "2026-08-13T14:14:31.213Z" }, + { url = "https://files.pythonhosted.org/packages/63/00/bee1bc9faa02a46e7a851019fd23f47ca1f906609edbec8b6ba5decc3cc3/ml_dtypes-0.6.0-cp315-cp315-win_arm64.whl", hash = "sha256:de9d14748dbf3968951436ef514a29c9d1fe438aa680d110134ee2f7a9f9df18", size = 568381, upload-time = "2026-08-13T14:14:32.548Z" }, + { url = "https://files.pythonhosted.org/packages/72/f7/9a5edede28f73185fd51d75030ef7f11d76997bab3a92427d986e54fe2eb/ml_dtypes-0.6.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e25bb3b0ad1217b60626e4ed45b10ca170c41d99fbe44a12bebc1e07ec4aad55", size = 589877, upload-time = "2026-08-13T14:14:33.695Z" }, + { url = "https://files.pythonhosted.org/packages/fd/81/d5924a141b850b606eb027493c9c3ca3c665cca5163af3f5b6e5e3345503/ml_dtypes-0.6.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31f1ce979d31a357e95aa81812f20412c8c954fa43c44ee3ead1e1c8a78575ef", size = 362788, upload-time = "2026-08-13T14:14:34.996Z" }, + { url = "https://files.pythonhosted.org/packages/59/8f/3298e3f334832bc28dd144af6b99cdc93502a8687e71922ea68b0a319929/ml_dtypes-0.6.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2d6149f3a57f405bcad5fb41e03218b8373936253f23e1ca84c0108abbc3392", size = 430823, upload-time = "2026-08-13T14:14:36.44Z" }, + { url = "https://files.pythonhosted.org/packages/93/d2/f2dbf118f42ce4c325a139c9236737f436b7f8e00cd18701c99ef2405e6f/ml_dtypes-0.6.0-cp315-cp315t-win_amd64.whl", hash = "sha256:ce7563e0b1a4482cbc1b4a6272145e54e4489e54fe7428f94908c3d87103abfa", size = 465119, upload-time = "2026-08-13T14:14:37.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ff/bda40387b5c5c64254595f4d81a12351770856acc5de4e6d43606a31f161/ml_dtypes-0.6.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f6cb525101b6b903779188c1e9e9490c343b455ab822883e02cf01e5547338d2", size = 572666, upload-time = "2026-08-13T14:14:38.993Z" }, +] + [[package]] name = "mpmath" version = "1.3.0" @@ -822,7 +871,7 @@ name = "nvidia-cublas" version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, + { name = "nvidia-cuda-nvrtc" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, @@ -861,7 +910,7 @@ name = "nvidia-cudnn-cu13" version = "9.24.0.43" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, + { name = "nvidia-cublas" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ca/30/7c257e3d5cb4fecb147b93895c66e29c93f8e76d74b45bb418ff0587c4ec/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:a6812a554a1ff0413e9c52b84c26c050380649ab9615f9c16bded368ce9f421f", size = 650976863, upload-time = "2026-07-02T16:23:39.248Z" }, @@ -873,7 +922,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -903,9 +952,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, - { name = "nvidia-cusparse", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, - { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -917,7 +966,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, @@ -978,6 +1027,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, ] +[[package]] +name = "onnx" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/19/8ea73a64b368b75fe339771a20a02bc61ea1f551484c9e3d9d0bfbd0450f/onnx-1.22.0.tar.gz", hash = "sha256:ef40c0aaf0b643857ea9306fc7eddce17eaf9fb0407e4801f1fc5758443a38e0", size = 12024721, upload-time = "2026-06-15T12:50:05.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/6a/481561f1093834376ed493e4ca42a73e5be0d50031f2969c86593bdc7c96/onnx-1.22.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:596fbf0490947533c1c1045ba860851dc9fb77471023dac9a71ba5b42ceab103", size = 20167081, upload-time = "2026-06-15T12:49:32.078Z" }, + { url = "https://files.pythonhosted.org/packages/84/55/b34fc2aa30aa54b4a775402d24c4082242c720283a274fe976ac8eb94480/onnx-1.22.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae5a563f281cd9d2845622cecf6c092a57e4ee1b138f66fdbbdd4200567a5e16", size = 18889249, upload-time = "2026-06-15T12:49:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/09/a6/bd32357e6cc1ecb473afd78193d7231724f284435d2db25696ecfaaa1503/onnx-1.22.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:955e02e1f6d385b53d52f9cd7b9cdf5caf417c300bcfe3c64c6d542be763845b", size = 19106514, upload-time = "2026-06-15T12:49:37.424Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9d/3af461ac6c714b8b369cb71499659932f4f12cfb066250b62f7567c3d530/onnx-1.22.0-cp312-abi3-pyemscripten_2025_0_wasm32.whl", hash = "sha256:82e9f27fc1223cb06d68a56bed6f9d3caf3d0dad1b61bce45006d529b15bd94c", size = 16966387, upload-time = "2026-06-15T12:49:40.918Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f0/68195b5e5a53e333faf2660f5352ee43738d0e42fc5216cc6b1871a9fbfb/onnx-1.22.0-cp312-abi3-win32.whl", hash = "sha256:cc8b66b312f8f03a53e268afb67180a2d97dd12cc79e2b61361c6c0073448016", size = 17081568, upload-time = "2026-06-15T12:49:43.398Z" }, + { url = "https://files.pythonhosted.org/packages/13/a8/734725bb703c5fabb687f79c79e51249475212b3eb37771ac4a4ac9b487f/onnx-1.22.0-cp312-abi3-win_amd64.whl", hash = "sha256:72ccebab3bac07215c204ce8848d42e78eaaa666badbf72d25cd359b9f269e3a", size = 17213290, upload-time = "2026-06-15T12:49:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/8ce48d8ae26a8761ad4e5dc771961b155c5c3c7c8540ec7f2f2d71b69af0/onnx-1.22.0-cp312-abi3-win_arm64.whl", hash = "sha256:f3c120dcdb70ad738f3c061b32798f408ea299eb69f84dd69ab4a6bf3c2ec01f", size = 17207030, upload-time = "2026-06-15T12:49:48.635Z" }, + { url = "https://files.pythonhosted.org/packages/f3/13/47323b97846387848efb1044ded11bb94b83526f3d1fbdb37c6480d4520f/onnx-1.22.0-cp314-cp314t-macosx_12_0_universal2.whl", hash = "sha256:19e45e4af88e3fe3261458d4b8cc461957ae2782a358a3560503569bf3b23b72", size = 20176465, upload-time = "2026-06-15T12:49:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/13/0c/d3b8a7e7eee123938586c608bb9894b5723f2342b9450c0eec59fbec7099/onnx-1.22.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c21a0e59fd967a95b358e4a6e756d1f1eec2d304a83480f329f66e30d2bf0223", size = 18894028, upload-time = "2026-06-15T12:49:54.451Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8a/da2a97ab46fe6e0cd9beb3ac14603a22f5be492f9ca347faf8233a07bb33/onnx-1.22.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2632406b8f523ef2e2873c363f90b20a3d88c0fbcfac757d3addffccf8f452c2", size = 19110420, upload-time = "2026-06-15T12:49:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a3/ce984063017518307ebfaa545782fc400e593dc2d7fdf4f23ce4be1ed197/onnx-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a3a39fc4643867aecb33417fdddb11e308ee79d2d4a584b9d50cc7aec2091b13", size = 17237547, upload-time = "2026-06-15T12:50:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/257a880384a1dd502d543b0067945074d63cd17d0840e958355bc8197da8/onnx-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:8e268cdc0547e3949799ffd4a44451dc2b9080b57d0824a2db680b6ec65506f0", size = 17231391, upload-time = "2026-06-15T12:50:03.047Z" }, +] + [[package]] name = "onnxruntime" version = "1.30.0"