1. 为什么 YOLO26-obb 导出 ONNX 后跑不通旋转框推理的坑在哪YOLO26-obb 是面向旋转目标检测Oriented Bounding Box的模型典型场景是遥感影像里的飞机、舰船、储油罐或者工业质检里的斜置零件。它和普通水平框检测最大的区别在于输出多了一个角度维度框是带旋转的四边形而不是轴对齐矩形。很多人把模型导出成 ONNX 之后第一反应是拿现成的 YOLOv8 推理脚本改一改结果发现框画出来是歪的、坐标全错、甚至一个框都没有。问题基本都出在三个地方预处理对齐、输出张量格式理解、以及旋转框的解码方式。先说预处理。YOLO26-obb 训练时用的是 letterbox 缩放加灰边填充保持长宽比不变。如果你推理时直接 resize 到 640x640图像被拉伸角度信息就失真了框会整体偏斜。而且 letterbox 的 padding 是居中的还原坐标时必须把 pad 的偏移减掉再除以缩放比例顺序错了坐标就飘到画面外。再说输出格式。YOLO26 默认走 end2end 模式NMS 已经内置在模型里了所以输出不是传统的 (batch, 4nc1, num_anchors)而是 (batch, max_det, 7)。这 7 个值依次是 [x, y, w, h, score, class_id, angle]注意这里的 x、y 是旋转框中心点w、h 是框的宽高angle 是弧度制的旋转角。很多人按老格式去解析把 score 当成类别、把 class_id 当成角度结果自然一团糟。最后是旋转框解码。拿到 xywhr 之后要先做角度规范化regularize把角度归一到 [0, π/2] 区间同时根据是否超过 π/2 交换宽高。这一步不做画出来的框会翻转或者宽高颠倒。然后再用旋转矩阵把中心点加宽高加角度转成四个角点才能用 cv2.polylines 画出来。这篇内容就是围绕这条链路给你一份可以直接跑的 ONNX Runtime 推理脚本配上输入输出张量核对清单再用单张样例图验证框坐标和类别分数。适合已经拿到 YOLO26-obb ONNX 模型、但卡在推理落地这一步的同学。如果你还没导出模型ultralytics 里model.export(formatonnx)就能生成导出时注意 opset 建议 12 以上动态轴按需开。2. 前置准备ONNX Runtime 环境与模型文件核对在写推理脚本之前先把环境和模型这两件事确认清楚能省掉后面一大半的排障时间。环境方面核心依赖就三个onnxruntime或 onnxruntime-gpu、opencv-python、numpy。如果你要用 GPU 推理装 onnxruntime-gpu并且确认 CUDA 和 cuDNN 版本匹配。我一般用 conda 建个干净环境conda create -n yolo26obb python3.10 -y conda activate yolo26obb pip install onnxruntime-gpu opencv-python numpy装完之后验证一下 provider 是否可用import onnxruntime as ort print(ort.get_available_providers())如果输出里有CUDAExecutionProvider说明 GPU 可用只有CPUExecutionProvider也能跑只是慢一点。这里有个细节providers 列表的顺序决定了优先级把 CUDA 放前面CPU 兜底。模型文件方面你需要确认三件事。第一模型输入张量的形状。用下面的代码打印出来import onnxruntime as ort session ort.InferenceSession(yolo26n-obb.onnx, providers[CPUExecutionProvider]) for inp in session.get_inputs(): print(input name:, inp.name, shape:, inp.shape, type:, inp.type) for out in session.get_outputs(): print(output name:, out.name, shape:, out.shape, type:, out.type)正常情况输入是[1, 3, 640, 640]输出是[1, 300, 7]或[1, max_det, 7]。如果输出维度不是 7那说明你导出的不是 end2end 模式需要额外做 NMS这篇先按 end2end 讲。第二确认类别数。DOTA 数据集是 15 类你自己训练的模型类别数可能不同类别字典要对应改。第三确认角度单位。YOLO26-obb 输出的是弧度不是角度画框时不用再转换但如果你要输出给别的系统记得统一。这里提一句如果你在团队里做长期编码或者 Agent 类任务模型文件、脚本、配置经常要跨机器同步用 TaoToken 的 Coding Plan 可以把这些接入配置统一管理省得每台机器重新配一遍。地址在 https://taotoken.net/api 的 coding-plan 页面具体怎么接后面第三节会讲。模型核对清单可以整理成一张表跑之前逐项打勾核对项期望值检查方式输入形状[1, 3, 640, 640]session.get_inputs()输出形状[1, max_det, 7]session.get_outputs()输出格式xywhscoreclsangle打印前几行看数值范围类别数与训练一致看 classes 字典长度角度单位弧度数值应在 0~π 附近providerCUDA 或 CPUget_available_providers()把这张表过一遍后面脚本跑起来基本不会出玄学问题。3. 可复制配置ONNX Runtime 会话与推理脚本这一节给你完整的推理脚本可以直接存成main.py跑。脚本结构分四块letterbox 预处理、end2end 后处理、旋转框解码与绘制、命令行入口。我按顺序贴关键参数都标了注释。先看会话配置和预处理。letterbox 的核心是保持长宽比缩放比例取 min然后居中填充灰边114, 114, 114这和训练时一致import cv2 import numpy as np import math def letterbox(img, new_shape(640, 640)): shape img.shape[:2] r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad round(shape[1] * r), round(shape[0] * r) dw, dh (new_shape[1] - new_unpad[0]) / 2, (new_shape[0] - new_unpad[1]) / 2 if shape[::-1] ! new_unpad: img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom round(dh - 0.1), round(dh 0.1) left, right round(dw - 0.1), round(dw 0.1) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value(114, 114, 114)) return img, r, (top, left)注意round(dh - 0.1)和round(dh 0.1)这个写法是为了让上下 padding 尽量对称避免奇数像素导致的偏移。这个细节在 ultralytics 源码里也是这么处理的直接抄过来最稳。然后是 end2end 后处理。输出是 (1, max_det, 7)先 squeeze 掉 batch 维再按 score 过滤然后把坐标从 letterbox 空间还原回原图def postprocess_end2end(output, ratio, pad, conf_thres0.25): preds np.squeeze(output, axis0) # (max_det, 7) boxes_xywh preds[:, :4] scores preds[:, 4] class_ids preds[:, 5] angles preds[:, 6] mask scores conf_thres boxes_xywh boxes_xywh[mask] scores scores[mask] class_ids class_ids[mask] angles angles[mask] if len(boxes_xywh) 0: return [] boxes_xywh[:, 0] (boxes_xywh[:, 0] - pad[1]) / ratio boxes_xywh[:, 1] (boxes_xywh[:, 1] - pad[0]) / ratio boxes_xywh[:, 2] boxes_xywh[:, 2] / ratio boxes_xywh[:, 3] boxes_xywh[:, 3] / ratio rboxes np.concatenate([boxes_xywh, angles[:, None]], axis1) rboxes regularize_rboxes(rboxes) corners xywhr2xyxyxyxy(rboxes) return [(corners[i], scores[i], class_ids[i]) for i in range(len(rboxes))]坐标还原的顺序是先减 pad再除以 ratio。pad 是 (top, left)所以 x 减 left、y 减 top别搞反。这一步错了框会整体平移。旋转框解码两个函数regularize 负责角度归一和宽高交换xywhr2xyxyxyxy 负责转四角点def regularize_rboxes(rboxes): x, y, w, h, t rboxes[:, 0], rboxes[:, 1], rboxes[:, 2], rboxes[:, 3], rboxes[:, 4] swap (t % math.pi) (math.pi / 2) w_ np.where(swap, h, w) h_ np.where(swap, w, h) t t % (math.pi / 2) return np.stack([x, y, w_, h_, t], axis-1) def xywhr2xyxyxyxy(rboxes): cos np.cos(rboxes[:, 4]) sin np.sin(rboxes[:, 4]) cx, cy rboxes[:, 0], rboxes[:, 1] w, h rboxes[:, 2], rboxes[:, 3] w2, h2 w / 2, h / 2 vec1_x, vec1_y w2 * cos, w2 * sin vec2_x, vec2_y -h2 * sin, h2 * cos pt1 np.stack([cx vec1_x vec2_x, cy vec1_y vec2_y], axis-1) pt2 np.stack([cx vec1_x - vec2_x, cy vec1_y - vec2_y], axis-1) pt3 np.stack([cx - vec1_x - vec2_x, cy - vec1_y - vec2_y], axis-1) pt4 np.stack([cx - vec1_x vec2_x, cy - vec1_y vec2_y], axis-1) return np.stack([pt1, pt2, pt3, pt4], axis1)主流程把上面串起来会话创建时优先 CUDAdef main(model_path, img_path, conf_thres0.25, output_pathNone): available ort.get_available_providers() providers [p for p in (CUDAExecutionProvider, CPUExecutionProvider) if p in available] session ort.InferenceSession(model_path, providersproviders or available) model_inputs session.get_inputs() input_shape model_inputs[0].shape input_h, input_w input_shape[2], input_shape[3] img cv2.imread(img_path) if img is None: raise FileNotFoundError(fImage not found: {img_path}) rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) padded, ratio, pad letterbox(rgb, (input_h, input_w)) blob np.ascontiguousarray(padded.transpose(2, 0, 1)[None].astype(np.float32) / 255.0) outputs session.run(None, {model_inputs[0].name: blob}) detections postprocess_end2end(outputs[0], ratio, pad, conf_thres) out_img img.copy() for corners, score, cls_id in detections: draw_rotated_box(out_img, corners, score, cls_id) print(fFound {len(detections)} detections) if output_path: cv2.imwrite(output_path, out_img) return out_img如果你要把这套配置接入团队协作环境比如让 Claude Code 或 Cline 直接调用这个推理服务可以在项目根目录放一个.mcp.json把模型路径和脚本入口配进去{ mcpServers: { yolo26-obb: { command: python, args: [/path/to/main.py, --model, /path/to/yolo26n-obb.onnx], env: { ORT_PROVIDER: CUDAExecutionProvider } } } }这里三件套要写全Base URL 指向你的推理服务地址Key 用 TaoToken 控制台生成的 API KeyModel ID 填你注册的模型标识。Base URL 是 https://taotoken.net/apiKey 在 console 的 api-keys 页面生成。这样 Claude Code 或 Cline 就能通过 MCP 协议调用你的旋转框检测不用每次手动跑脚本。4. 验证请求单张样例图跑通端到端脚本写完了接下来用一张样例图验证。选图建议用 DOTA 风格的遥感图或者你自己训练集里的一张确保图里有明显的旋转目标比如斜着的飞机、船。先跑命令行python main.py --model yolo26n-obb.onnx --img sample.jpg --conf-thres 0.5 --output result.jpg正常输出类似Found 7 detections Output saved to: result.jpg打开 result.jpg你应该能看到带旋转角度的四边形框每个框左上角有类别名和分数比如plane: 0.87、ship: 0.72。框的边应该贴合目标的长边方向而不是轴对齐的矩形。如果框画出来了但位置不对先别急着改代码按下面的顺序核对。第一步打印原始输出看看数值范围outputs session.run(None, {model_inputs[0].name: blob}) print(outputs[0].shape) print(outputs[0][0, :5, :])正常的话前 4 列是中心点和宽高数值在 0~640 之间第 5 列 score 在 0~1第 6 列 class_id 是整数第 7 列 angle 在 0~1.57 附近。如果 score 列出现大于 1 的数说明你解析的列顺序错了可能模型不是 end2end 格式。第二步验证坐标还原。拿一个检测框手动算一下还原后的中心点看是否落在原图目标上。比如原图 1920x1080letterbox 到 640x640ratio 约 0.333pad 的 top 约 140。如果某个框在 letterbox 空间中心是 (320, 320)还原后应该是 ((320 - left)/ratio, (320 - top)/ratio)left 约 0算出来 x 约 960正好是原图中心。第三步验证角度。挑一个明显斜 45 度的目标看画出来的框是不是也斜 45 度。如果框是斜的但方向反了检查 regularize 里的 swap 逻辑可能是宽高交换条件写反。这里有个我踩过的坑有些导出脚本会把 angle 输出成角度制而不是弧度制画框时 cos/sin 直接算就全乱了。判断方法很简单看 angle 的最大值如果接近 90 就是角度制接近 1.57 就是弧度制。弧度制不用转换角度制要先np.deg2rad。验证通过之后你可以把这张图的检测结果和 ultralytics 原生推理的结果对比一下框的位置和分数应该基本一致差异在 1~2 个像素以内算正常因为 letterbox 的 padding 取整方式可能略有不同。如果你想让验证过程更自动化可以把这套脚本接到 TaoToken 的模型对话里用自然语言描述图片让模型帮你判断检测结果是否合理。模型对话入口在 https://taotoken.net/api 的模型对话页面适合做这种快速验证。5. 常见报错排查401、local proxy failed、reading choices跑推理的过程中报错基本集中在环境、会话、后处理这三块。下面按真实报错逐条给排查路径。401 Unauthorized。这个报错一般出现在你把推理服务包装成 HTTP 接口、通过 TaoToken 调用的时候。原因通常是 API Key 没带或者带错了。检查请求头里的 Authorization 字段格式是Bearer 你的KeyKey 从 console 的 api-keys 页面复制注意别把前后空格带进去。如果 Key 是对的还报 401检查 Base URL 是不是写成了带 UTM 的地址接口调用要用 https://taotoken.net/api 这个干净的地址。local proxy failed。这个报错通常和网络环境有关出现在 SDK 尝试连接服务端的时候。排查顺序先确认本机能不能正常访问外网再确认防火墙有没有拦 onnxruntime 的进程。如果是公司内网可能需要配置 HTTP_PROXY 环境变量指向内网网关。注意这里说的是正常的网络代理配置不是让你去搞什么特殊通道企业内网走网关是常规操作。reading choices 相关报错。这个一般出现在解析模型输出的时候比如IndexError: index 6 is out of bounds或者too many indices for array。根因是你以为输出是 7 列实际不是。回到第二节的核对清单打印outputs[0].shape如果是(1, 7, 8400)这种说明模型不是 end2end 模式输出是 (batch, 4nc1, anchors)需要先转置再做 NMS。转置用outputs[0].transpose(0, 2, 1)然后手动实现 NMS 或者调 cv2.dnn.NMSBoxes。OAuth 相关报错。如果你用 Claude Code 接入推理服务可能会遇到 OAuth token 过期。检查~/.claude/settings.json里的配置确认 token 没过期。如果是 Codex 的 auth.json路径在~/.codex/auth.json里面存的是认证信息过期了重新生成。这类报错的关键是三件套要配全Base URL、Key、Model ID缺一个都会认证失败。框画出来是轴对齐矩形。这不是报错但很常见。原因是你的绘制函数用了cv2.rectangle而不是cv2.polylines。旋转框必须用 polylines 连四个角点rectangle 只能画水平框。检测结果为空。先降 conf_thres 到 0.1 试试如果还是空检查预处理是不是把图搞成全灰了。打印 blob 的均值和方差正常应该在 0.3~0.6 之间如果接近 0 或者 1说明归一化或通道顺序错了。YOLO 要 RGB 输入cv2 读进来是 BGR别忘了cv2.cvtColor。把这几类报错对照着排查基本能覆盖 90% 的推理落地问题。剩下的 10% 多半是模型本身导出有问题重新导出一次通常能解决。6. 长期编码与 Agent 接入把推理链路固化下来单次跑通只是第一步真正落地的时候你需要把这套推理链路固化成一个可复用的服务让团队里的其他人或者 Agent 能直接调用。最直接的方式是把 main.py 包装成一个 FastAPI 服务暴露一个/predict接口接收图片返回检测结果 JSON。这样前端、后端、Agent 都能通过 HTTP 调用不用关心 ONNX Runtime 的细节。接口设计上输入用 base64 或者 multipart 上传输出返回框的四个角点、分数、类别方便下游渲染。如果你用 Claude Code 做长期开发建议把推理服务注册成 MCP Server配置写在项目的.mcp.json里。这样你在 Claude Code 里直接说「帮我检测这张图的旋转目标」它就能调用你的服务。配置的时候三件套写全Base URL 用 https://taotoken.net/apiKey 从 console 生成Model ID 填你注册的服务标识。Cline 的配置类似在 MCP 设置里填同样的信息。对于需要长期跑批量推理的场景比如每天处理几千张遥感图建议用 Coding Plan 来管理任务调度和资源配置。Coding Plan 页面在 https://taotoken.net/api 的 coding-plan可以配置并发数、超时时间、重试策略。我实测下来把 batch size 设成 4、provider 用 CUDA单张 640x640 的图推理耗时在 15ms 左右比 CPU 快 20 倍以上。还有一个实用技巧把类别字典和 conf_thres 做成配置文件不要硬编码在脚本里。不同项目用的类别不一样DOTA 是 15 类工业质检可能只有 3 类。配置文件用 YAML 或者 JSON 都行启动时加载改起来不用动代码。最后提醒一点ONNX 模型文件通常比较大别直接塞进 git 仓库。用 git-lfs 或者对象存储管理脚本里通过环境变量指定模型路径。这样换模型的时候只改环境变量不用改代码部署也干净。整套链路跑下来从导出 ONNX 到推理服务上线核心就是三件事预处理对齐、输出格式解析、旋转框解码。把这三步做对剩下的都是工程化的事。你可以先把单张图的脚本跑通再逐步包装成服务最后接入 Agent 工作流。