
1. 遥感旋转框检测的数据困局与破局思路搞遥感目标检测的朋友大概率都经历过这样一个阶段兴冲冲下载了DOTA数据集打开标注文件一看坐标是x1 y1 x2 y2 x3 y3 x4 y4这种四点多边形格式每个目标还带一个imagesource和difficult标记。然后你转头看看手头的YOLO训练脚本它要的是class x_center y_center width height这种归一化后的水平框。两边根本对不上直接开训就是一堆报错或者模型完全学不到东西。这个问题的本质在于遥感图像里的目标几乎都是任意朝向的。航拍视角下一架飞机可能斜着停在跑道上一艘船可能以任意角度航行一个储油罐从俯视图看是圆形但标注时可能用旋转矩形框住。如果你强行用水平框去套这些目标框里会混入大量背景像素模型学到的特征被严重污染。举个例子一个45度倾斜的飞机它的水平外接框面积可能是真实旋转框的1.5到2倍多出来的区域全是跑道或者草地。这就是为什么做遥感检测不能简单套用自然图像那一套。那为什么还要转成YOLO格式直接上MMRotate或者别的旋转框检测框架不行吗行但有几个现实问题第一YOLO系列尤其是v5、v8、v11这些的工程化程度极高部署链路成熟从训练到ONNX导出到TensorRT加速到边缘设备落地整套工具链非常完善第二很多实际项目对精度的要求没有学术 benchmark 那么苛刻用旋转框转水平框加上一些后处理策略完全能满足业务需求第三YOLO的训练速度快、显存占用低在同等硬件条件下能迭代更多轮次。所以“旋转框转YOLO格式”这个需求在工程实践中非常普遍。但这里有个关键决策点你是要做旋转框检测还是水平框检测如果你的业务场景里目标朝向对结果影响很大比如港口船只检测需要知道船头朝向那转YOLO水平框就是自废武功应该老老实实上MMRotate或者YOLO的OBB版本。如果你只是要检测“有没有飞机”“有没有油罐”不关心朝向那转水平框完全够用而且训练和部署都更简单。我个人的经验是先明确业务需求再决定标注格式不要为了用某个框架而强行转换。接下来我会从DOTA数据集的原始格式讲起一步步拆解坐标转换的数学原理、代码实现、可视化验证、以及训练时需要注意的坑。整个过程我会用Python实现不依赖MMRotate的重型依赖核心逻辑用numpy和opencv就能搞定。你跟着走一遍基本能掌握遥感数据格式转换的通用方法论。1.1 DOTA数据集到底长什么样DOTADataset for Object deTection in Aerial images是遥感检测领域最常用的公开数据集之一由武汉大学发布。它的标注格式和COCO、VOC都不一样采用的是四点标注法。每个目标用四个点的坐标表示按顺时针或逆时针顺序排列形成一个任意四边形。标注文件是纯文本每行格式如下x1 y1 x2 y2 x3 y3 x4 y4 category difficult其中x1 y1到x4 y4是四个角点的像素坐标category是类别名称如plane、ship、storage-tank等difficult是0或1表示该目标是否难以检测通常忽略difficult1的样本。DOTA-v1.0有15个类别v1.5和v2.0类别更多这里不展开。关键点在于这四个点不一定构成矩形。虽然大多数标注是矩形但理论上可以是任意四边形。这就给转换带来了一个核心问题如何从一个任意四边形得到一个“最优”的水平外接矩形最直接的做法是取四个点的x坐标最小值和最大值作为矩形的左右边界y坐标最小值和最大值作为上下边界。这样得到的水平框一定能包住原始旋转框但会引入额外的背景区域。我实测过对于长宽比很大的目标比如桥梁这种外接框会非常“胖”背景占比可能超过60%。这时候你可能需要考虑是不是应该把长条形目标拆分成多个小段来标注或者直接用旋转框检测这个后面会详细讨论。另外DOTA数据集还有一个特点图像尺寸非常大。原始图像通常是4000x4000甚至更大直接训练不现实。所以官方提供了切图工具把大图裁成1024x1024或者800x800的小图同时更新标注坐标。如果你下载的是已经切好的版本标注文件里的坐标就是相对于小图的直接用就行。如果你拿到的是原始大图需要先切图再转换否则YOLO的输入尺寸根本放不下。1.2 为什么不能直接拿DOTA标注训练YOLO这个问题看起来简单但背后涉及几个层面的不兼容。首先是格式层面YOLO要的是归一化的中心点坐标和宽高DOTA给的是绝对像素坐标的四个角点两者之间需要做坐标变换和归一化。其次是语义层面DOTA的旋转框包含朝向信息YOLO的水平框丢失了这个信息这是一个不可逆的信息损失。最后是训练层面YOLO的损失函数比如CIoU、DIoU是基于水平框设计的直接拿旋转框的四个点去算IoU会非常复杂而且YOLO的anchor机制也是为水平框设计的。所以转换的本质是在保留检测能力的前提下尽可能减少信息损失。具体来说我们需要做三件事第一把四点坐标转成水平外接矩形第二把绝对坐标归一化到0-1之间第三把类别名称映射成数字索引。这三步做完YOLO才能正常读取。但这里有个隐藏的坑DOTA的类别名称和YOLO的类别索引需要一一对应。如果你自己写转换脚本一定要确保类别映射表在训练和推理时保持一致。我见过有人训练时用的是{plane: 0, ship: 1}推理时忘了改结果把所有飞机都识别成船。这种低级错误在工程中并不少见建议把类别映射写成一个独立的配置文件训练和推理都从同一个文件读取。2. 坐标转换的数学原理与代码实现坐标转换听起来简单但实际写代码时会遇到各种边界情况。比如四个点不构成凸四边形怎么办坐标超出图像边界怎么办归一化时除以的宽高是原始图像尺寸还是网络输入尺寸这些问题如果不处理好训练时就会出现loss震荡或者模型完全不收敛。下面我逐个拆解。2.1 从四点到水平框的几何推导假设DOTA标注的四个点为(x1,y1), (x2,y2), (x3,y3), (x4,y4)我们要得到一个水平矩形(x_min, y_min, x_max, y_max)。最直接的方法是x_min min(x1, x2, x3, x4) x_max max(x1, x2, x3, x4) y_min min(y1, y2, y3, y4) y_max max(y1, y2, y3, y4)然后中心点和宽高为x_center (x_min x_max) / 2 y_center (y_min y_max) / 2 width x_max - x_min height y_max - y_min最后归一化x_center_norm x_center / img_width y_center_norm y_center / img_height width_norm width / img_width height_norm height / img_height这就是YOLO需要的格式。看起来很简单对吧但这里有几个细节需要注意。第一个细节坐标是否包含边界如果x_max等于图像宽度那么width_norm就是1.0YOLO在计算损失时可能会出问题。通常建议把坐标裁剪到[0, img_width-1]和[0, img_height-1]范围内。我一般会在转换前加一行裁剪代码x_coords [max(0, min(x, img_width - 1)) for x in x_coords] y_coords [max(0, min(y, img_height - 1)) for y in y_coords]第二个细节宽高为0的情况。如果四个点退化成一条线比如标注错误width或height可能为0。这种样本必须过滤掉否则YOLO计算IoU时会除以0。我通常设置一个最小阈值比如width 2 or height 2就丢弃。第三个细节归一化基准。如果你用的是切图后的DOTA图像尺寸是1024x1024那归一化就除以1024。但如果你在训练时把图像resize到了640x640那归一化应该除以640还是1024答案是标注归一化应该基于原始图像尺寸而不是网络输入尺寸。因为YOLO在训练时会自己把图像resize到网络输入尺寸同时也会相应地缩放标注。如果你提前用640归一化YOLO再缩放一次坐标就全乱了。这一点很多新手会搞错。2.2 完整转换脚本与逐行解析下面是我在实际项目中反复使用的一个转换脚本核心逻辑用numpy实现不依赖任何重型框架。你可以直接复制到自己的项目里用。import os import numpy as np from pathlib import Path # DOTA类别列表根据你的数据集版本调整 DOTA_CLASSES [ plane, ship, storage-tank, baseball-diamond, tennis-court, basketball-court, ground-track-field, harbor, bridge, large-vehicle, small-vehicle, helicopter, roundabout, soccer-ball-field, swimming-pool ] def dota_to_yolo(dota_line, img_width, img_height, class_map): 将一行DOTA标注转换为YOLO格式 返回: (class_id, x_center, y_center, width, height) 或 None parts dota_line.strip().split() if len(parts) 9: return None # 解析四个角点坐标 coords list(map(float, parts[:8])) x_coords coords[0::2] # x1, x2, x3, x4 y_coords coords[1::2] # y1, y2, y3, y4 category parts[8] difficult int(parts[9]) if len(parts) 9 else 0 # 跳过difficult样本 if difficult 1: return None # 跳过不在类别列表中的目标 if category not in class_map: return None # 裁剪坐标到图像范围内 x_coords [max(0, min(x, img_width - 1)) for x in x_coords] y_coords [max(0, min(y, img_height - 1)) for y in y_coords] # 计算水平外接矩形 x_min, x_max min(x_coords), max(x_coords) y_min, y_max min(y_coords), max(y_coords) # 计算中心点和宽高 x_center (x_min x_max) / 2.0 y_center (y_min y_max) / 2.0 width x_max - x_min height y_max - y_min # 过滤无效框 if width 2 or height 2: return None # 归一化 x_center_norm x_center / img_width y_center_norm y_center / img_height width_norm width / img_width height_norm height / img_height # 再次检查归一化后的值是否在合理范围内 if not (0 x_center_norm 1 and 0 y_center_norm 1): return None if not (0 width_norm 1 and 0 height_norm 1): return None class_id class_map[category] return (class_id, x_center_norm, y_center_norm, width_norm, height_norm) def convert_dota_folder(dota_dir, output_dir, img_width1024, img_height1024): 批量转换DOTA标注文件夹 dota_dir: 包含labelTxt文件夹的目录 output_dir: 输出YOLO标签的目录 class_map {name: idx for idx, name in enumerate(DOTA_CLASSES)} label_dir Path(dota_dir) / labelTxt output_dir Path(output_dir) output_dir.mkdir(parentsTrue, exist_okTrue) total_files 0 total_objects 0 skipped_objects 0 for label_file in label_dir.glob(*.txt): yolo_lines [] with open(label_file, r) as f: for line in f: result dota_to_yolo(line, img_width, img_height, class_map) if result is not None: class_id, xc, yc, w, h result yolo_lines.append(f{class_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) total_objects 1 else: skipped_objects 1 # 写入YOLO格式标签文件 output_file output_dir / label_file.name with open(output_file, w) as f: f.write(\n.join(yolo_lines)) total_files 1 print(f转换完成: {total_files} 个文件, {total_objects} 个目标, 跳过 {skipped_objects} 个)这段代码有几个设计决策值得说明。第一为什么用min/max而不是cv2.minAreaRectcv2.minAreaRect会返回最小面积旋转矩形但它的角度范围是[-90, 0)而且对于接近正方形的目标角度可能不稳定。用min/max得到的是轴对齐外接矩形虽然面积更大但计算简单、结果稳定而且YOLO本来就是要水平框所以没必要用minAreaRect。第二为什么设置width 2的阈值这是经验值。遥感图像里有些小目标比如车辆可能只有几个像素宽如果阈值设得太高会漏掉真实目标设得太低又会引入噪声。2像素是一个比较平衡的值你可以根据自己数据集的统计分布调整。第三为什么归一化后还要再检查一次因为浮点数计算可能有精度问题比如x_center_norm算出来是1.0000001这种边界情况会导致YOLO报错所以加一层保险。2.3 可视化验证别等到训练才发现转错了代码写完不代表转换正确。我强烈建议在训练前做一次可视化验证把转换后的YOLO框画回原图上肉眼检查是否对齐。这一步花不了几分钟但能帮你避免几个小时的无效训练。import cv2 import numpy as np def visualize_yolo_label(img_path, label_path, class_names, output_path): 将YOLO格式标签画到图像上用于验证转换正确性 img cv2.imread(img_path) h, w img.shape[:2] with open(label_path, r) as f: lines f.readlines() for line in lines: parts line.strip().split() if len(parts) ! 5: continue class_id int(parts[0]) xc, yc, bw, bh map(float, parts[1:]) # 反归一化 xc * w yc * h bw * w bh * h # 计算左上角和右下角 x1 int(xc - bw / 2) y1 int(yc - bh / 2) x2 int(xc bw / 2) y2 int(yc bh / 2) # 画框 color (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) # 写类别名 label class_names[class_id] if class_id len(class_names) else str(class_id) cv2.putText(img, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) cv2.imwrite(output_path, img) print(f可视化结果已保存到 {output_path})跑完这个脚本打开输出图像重点检查三件事框是否包住了目标、有没有明显偏移、类别标签是否正确。如果发现框偏了大概率是归一化基准搞错了比如用了resize后的尺寸而不是原始尺寸。如果发现某些目标没框检查是不是被difficult过滤掉了或者类别名不在映射表里。注意可视化时用的图像必须是和标注对应的原始图像。如果你用的是切图后的DOTA确保图像和标签文件名一一对应。DOTA切图后的命名规则通常是P0001__1024__0___0.png这种标签文件同名但扩展名是.txt。3. 训练配置与数据加载的实战细节转换完格式只是第一步真正训练时还有一堆坑等着。YOLO的数据加载器对标签文件的位置、命名、内容都有严格要求稍有不慎就是“找不到标签”或者“标签格式错误”。这一章我按训练流程的顺序把每个环节的关键配置和常见问题讲清楚。3.1 数据集目录结构与YAML配置YOLO训练时需要一个YAML文件来描述数据集路径和类别信息。标准结构如下# dataset.yaml path: /data/dota_yolo # 数据集根目录 train: images/train # 训练图像相对路径 val: images/val # 验证图像相对路径 nc: 15 # 类别数 names: # 类别名称列表顺序必须和转换时的class_map一致 0: plane 1: ship 2: storage-tank 3: baseball-diamond 4: tennis-court 5: basketball-court 6: ground-track-field 7: harbor 8: bridge 9: large-vehicle 10: small-vehicle 11: helicopter 12: roundabout 13: soccer-ball-field 14: swimming-pool目录结构应该是这样的dota_yolo/ ├── dataset.yaml ├── images/ │ ├── train/ │ │ ├── P0001__1024__0___0.png │ │ └── ... │ └── val/ │ └── ... └── labels/ ├── train/ │ ├── P0001__1024__0___0.txt │ └── ... └── val/ └── ...关键点图像和标签的文件名必须完全一致除了扩展名。YOLO的数据加载器会自动把images/train/xxx.png映射到labels/train/xxx.txt。如果你把标签放在别的目录需要在YAML里额外配置labels路径但我不建议这么做容易出错。另一个容易踩的坑是类别顺序。YAML里的names顺序必须和转换脚本里的DOTA_CLASSES完全一致。我见过有人转换时用的是字母序YAML里写的是DOTA官方顺序结果训练出来的模型把所有类别都搞混了。建议把类别列表写成一个单独的Python文件或者JSON转换脚本和YAML生成脚本都从同一个源读取。3.2 训练参数设置与显存优化DOTA数据集切图后通常有1万到2万张1024x1024的图像目标数量在20万到30万之间。这个规模用单卡训练是可行的但需要合理设置batch size和输入尺寸。以下是我在RTX 309024GB显存上的实测配置参数值说明imgsz640输入尺寸1024太大640是精度和速度的平衡点batch163090上跑640尺寸可以到16再大就OOMepochs100DOTA数据量大100轮基本收敛optimizerSGDYOLO官方推荐momentum0.937lr00.01初始学习率lrf0.01最终学习率系数warmup_epochs3预热轮次mosaic1.0马赛克增强对小目标检测很关键mixup0.1混合增强不要设太高copy_paste0.1复制粘贴增强对遥感小目标有效如果你显存不够优先降batch而不是imgsz。因为遥感图像里小目标很多输入尺寸降到512以下会严重损失小目标特征。我试过用416训练小车辆small-vehicle的召回率直接掉了15个百分点。如果实在跑不动640可以考虑用YOLOv8n或者YOLOv11n这种轻量模型参数量小显存占用低。还有一个技巧是冻结骨干网络。如果你用的是预训练模型前几轮可以冻结backbone只训练检测头这样显存占用能降低30%左右而且收敛更快。等loss稳定后再解冻全部参数微调。YOLOv8的命令行参数是freeze10表示冻结前10层。3.3 数据增强策略的取舍遥感图像的数据增强和自然图像有很大不同。自然图像常用的随机裁剪、旋转、翻转在遥感里要谨慎使用。水平翻转和垂直翻转基本可以放心用因为航拍视角下目标朝向本来就是任意的翻转不会产生不合理的样本。90度旋转也可以用但任意角度旋转要小心因为旋转后图像边缘会出现黑边而且标注框需要重新计算容易引入误差。Mosaic增强对遥感小目标非常有效。它把四张图拼成一张相当于变相增加了小目标的出现频率。我实测下来开启Mosaic后小车辆的AP能提升5到8个点。但Mosaic也有副作用拼接处的目标可能被截断导致标注框不完整。YOLOv8默认的Mosaic概率是1.0我建议在训练后期最后10到20轮关掉Mosaic让模型在真实分布上微调这样能提升最终精度。HSV增强要适度。遥感图像的颜色分布和自然图像不同过度调整色调可能让模型学到无关特征。我一般把hsv_h设为0.015hsv_s设为0.7hsv_v设为0.4比默认值保守一些。提示如果你用的是DOTA-v1.5或v2.0类别数更多小目标比例更高建议把imgsz提到768甚至896同时把batch降到8。精度提升明显但训练时间会翻倍。4. 常见问题排查与避坑经验实录这一章是我踩过的坑和帮别人排查过的问题的汇总。每个问题都附带了排查思路和解决方法你可以当成速查表用。4.1 标签格式错误与数据加载失败问题现象训练启动时报错Label format invalid或者No labels found。排查思路首先检查标签文件是否存在文件名是否和图像一一对应。然后打开几个标签文件确认每行是5个值class_id 4个归一化坐标且坐标在0到1之间。常见错误包括坐标没归一化值大于1、类别ID从1开始而不是0、每行有多余的空格或换行符。解决方法写一个校验脚本遍历所有标签文件统计每行的字段数和坐标范围。下面这个脚本我每次转换完都会跑一遍def validate_yolo_labels(label_dir, num_classes): 校验YOLO标签文件的合法性 issues [] for label_file in Path(label_dir).glob(*.txt): with open(label_file, r) as f: for line_num, line in enumerate(f, 1): parts line.strip().split() if len(parts) ! 5: issues.append(f{label_file}:{line_num} 字段数{len(parts)}) continue try: class_id int(parts[0]) coords list(map(float, parts[1:])) except ValueError: issues.append(f{label_file}:{line_num} 解析失败) continue if class_id 0 or class_id num_classes: issues.append(f{label_file}:{line_num} 类别ID越界: {class_id}) for c in coords: if c 0 or c 1: issues.append(f{label_file}:{line_num} 坐标越界: {c}) if issues: print(f发现 {len(issues)} 个问题:) for issue in issues[:20]: print(f {issue}) else: print(所有标签文件校验通过) return issues避坑经验DOTA原始标注里有些目标的四个点可能不是按顺序排列的甚至可能自相交。虽然min/max方法不受点顺序影响但如果你用其他方法比如计算多边形面积点顺序就很重要了。建议在转换前先检查一下标注文件看看有没有异常行。4.2 训练loss不收敛或震荡问题现象训练开始后box_loss和cls_loss一直很高或者剧烈震荡mAP不上升。排查思路先确认标签是否正确用上面的校验脚本。然后检查学习率是否太大DOTA数据集用0.01的初始学习率通常没问题但如果你用的是小batch比如4或8学习率要相应降低。另外检查数据增强是否过猛特别是Mosaic和Mixup同时开启时早期训练可能不稳定。解决方法我一般会先用一个很小的子集比如100张图跑10轮确认模型能过拟合。如果连100张图都学不会那肯定是数据或配置有问题。过拟合测试通过后再上全量数据。另外YOLOv8默认的close_mosaic10表示最后10轮关闭Mosaic这个设置对稳定收敛很有帮助建议保留。避坑经验遥感图像里背景占比很高有些图可能一个目标都没有。YOLO对空标签的处理是当作负样本但如果负样本比例太高模型会偏向于预测背景。我建议统计一下每张图的目标数量如果平均少于1个考虑过滤掉一些空图或者用focal loss来平衡正负样本。4.3 小目标漏检严重问题现象训练完成后大目标如飞机、储油罐检测效果很好但小目标如车辆召回率很低。排查思路小目标漏检通常有三个原因输入尺寸太小、anchor尺寸不匹配、正样本分配策略不合适。YOLOv8用的是无anchor的检测头所以anchor问题不存在但输入尺寸和正样本分配仍然是关键。解决方法第一提高输入尺寸到768或896小目标的像素面积会增大特征更明显。第二调整正样本分配阈值YOLOv8的box参数控制正样本的IoU阈值默认是7.5可以降到5.0让更多小目标被分配为正样本。第三在数据增强里增加小目标的复制粘贴概率YOLOv8的copy_paste参数就是干这个的设到0.3左右效果明显。避坑经验DOTA里的小车辆small-vehicle在1024x1024的图上可能只有10到20个像素宽转成640输入后只剩6到12个像素。这个尺寸已经接近YOLO下采样32倍后的极限了。如果业务场景对小目标要求很高建议用YOLOv8x或者YOLOv11x这种大模型或者用切片推理SAHI的方式在推理时放大图像。4.4 类别不平衡与长尾分布问题现象某些类别如直升机、游泳池的AP远低于其他类别。排查思路统计每个类别的实例数量DOTA数据集本身就有长尾问题。直升机可能只有几百个实例而飞机有几千个。这种不平衡会导致模型偏向于预测高频类别。解决方法第一在损失函数里给稀有类别更高的权重YOLOv8支持通过cls参数调整分类损失的权重但更精细的类别权重需要改源码。第二对稀有类别做过采样在数据加载时提高包含稀有类别的图像被选中的概率。第三用focal loss替代默认的BCE lossfocal loss对难样本和稀有类别更友好。避坑经验我试过用类别权重的方式但效果不如直接过采样。因为YOLO的损失是多个样本平均的单个样本的权重调整会被平均掉。过采样更直接但要注意不要过拟合稀有类别。一般把稀有类别的采样倍率控制在2到3倍就够了。4.5 推理时框重叠与NMS调参问题现象推理结果里同一个目标出现多个重叠框或者相邻目标被合并成一个框。排查思路这是NMS非极大值抑制参数的问题。YOLO默认的conf阈值是0.25iou阈值是0.45。如果目标密集比如港口里停了很多船0.45的IoU阈值会导致相邻目标被误删。解决方法对于密集场景把iou阈值提高到0.6甚至0.7让NMS更宽松。同时适当提高conf阈值到0.4过滤掉低置信度的误检。如果目标特别密集可以考虑用Soft-NMS或者DIoU-NMS但这些需要改推理代码。避坑经验遥感图像里有些目标天然就靠得很近比如停车场里的车。这种情况下水平框NMS很难做到完美。如果业务允许可以考虑用旋转框检测YOLOv8-OBB来替代旋转框的IoU计算更准确NMS效果更好。YOLOv8-OBB是官方支持的训练流程和普通YOLOv8几乎一样只是标签格式变成了class x_center y_center width height angle。5. 从转换到部署的完整链路复盘把上面这些环节串起来一个完整的DOTA转YOLO训练流程大概是这样的下载DOTA数据集切图如果还没切运行转换脚本生成YOLO标签校验标签配置YAML启动训练监控loss和mAP训练完成后导出ONNX用TensorRT或OpenVINO加速推理。整个链路我跑过不下十次每次都会遇到一些新问题但核心逻辑是不变的。有一个容易被忽视的环节是验证集的选择。DOTA官方提供了train/val的划分但如果你自己切图划分可能不一致。我建议按照图像级别划分而不是随机划分目标。因为同一张大图切出来的小图之间有重叠区域如果随机划分训练集和验证集可能包含同一区域的不同切片导致验证指标虚高。正确的做法是按原始大图的ID划分确保验证集的大图在训练集中没出现过。另一个经验是保存转换脚本和配置。我习惯把转换脚本、类别映射、YAML配置、训练命令都写在一个README.md里和数据集放在一起。过几个月再回来复现时不用重新回忆当时是怎么转的。这个习惯帮我省了很多时间特别是在团队协作时别人接手你的项目能快速上手。最后说一个部署时的坑YOLO导出的ONNX模型输出的是归一化坐标但不同版本的YOLO输出格式不一样。YOLOv5的输出是[batch, num_anchors, 5nc]YOLOv8是[batch, 4nc, num_anchors]YOLOv11又变了。如果你用ONNX Runtime或者TensorRT部署一定要确认输出张量的形状和含义。我一般会写一个简单的Python脚本用ONNX Runtime跑一张测试图打印输出形状确认无误后再写C部署代码。提示如果你用的是AMD显卡YOLO的官方训练脚本默认走CUDA需要改成ROCm或者DirectML。ROCm版本的PyTorch安装比较麻烦建议用Docker镜像。DirectML在Windows上可以用但性能不如CUDA。如果只是推理ONNX Runtime支持DirectML后端速度还可以。这个内容后续还可以这样扩展如果你需要做旋转框检测可以把转换脚本改成生成YOLO-OBB格式只需要在最后加一个角度计算。角度可以从四个点的坐标用arctan2算出来但要注意角度的周期性和边界情况。YOLO-OBB的角度范围是[-90, 0)和OpenCV的minAreaRect一致可以直接用。