核心内容摘要
天堂草原Ww777777:一场沉醉于自然的视觉盛宴与心灵涤荡
从DOTA到YOLO为什么需要转换数据集格式第一次接触遥感图像目标检测时我对着DOTA数据集里密密麻麻的四边形标注框发了好一会儿呆。
这些被称为OBBOriented Bounding Box的旋转框虽然能精准框住斜向停放的飞机或船舶但主流的YOLOv5/v8等检测器默认只支持HBBHorizontal Bounding Box水平框。
这就好比你想用标准螺丝刀拧三角螺丝——工具和零件不匹配。
DOTA数据集的特点确实令人印象深刻图像分辨率普遍在4000×4000像素以上标注采用8个坐标点的多边形格式x1,y1,x2,y2,...,x8,y8包含15个典型遥感目标类别船舶、储油罐、运动场等单个图像可能包含数百个密集目标而YOLO需要的却是简约的5参数格式类别索引 x_center y_center width height这个转换过程就像把复杂的立体折纸展开成平面图纸。
我最初尝试手动转换时发现直接取旋转框的外接矩形会导致大量无效背景区域被包含特别是对于长宽比悬殊的桥梁、船舶等目标。
后来通过分析DOTA_devkit源码才找到正确解法——应该计算所有顶点坐标的最小外接水平矩形。
数据准备与环境配置
1 获取原始数据集建议直接从DOTA官网下载基准数据集目前主流版本有DOTA-v
1.
0
8GB15类别DOTA-v
1.
5
2GB新增集装箱起重机类别DOTA-v
2.
0
4GB18类别如果网络条件受限可以使用我预处理过的HBB版本包含已转换的YOLO格式标签# 百度云下载密码iw3w wget https://pan.baidu.com/s/1UX7oX3_x5CrP_SxSA7XKXQ
2 安装关键工具包处理过程中需要以下Python包# 基础环境 pip install numpy opencv-python pillow # 专用工具 pip install dota-utils shapely特别提醒建议使用Shapely
1.
1版本新版本在计算多边形几何时可能有API变动。
我在Colab上测试时遇到过这样的报错AttributeError: Polygon object has no attribute _get_coords就是版本兼容性问题导致的。
核心转换流程详解
1 标注文件解析实战DOTA的标注文件是这样的文本格式imagesource:GoogleEarth gsd:
146 ... 1 128 256 384 512 ... large-vehicle 0我们需要提取的是每行末尾的8个坐标点和类别信息。
用Python处理时要注意def parse_dota_label(label_path): with open(label_path) as f: lines [l.strip() for l in f.readlines()] objects [] for line in lines: if line.startswith(imagesource): continue parts line.split() if len(parts) 9: continue # 提取8个坐标点x1,y1,...,x4,y4 points list(map(float, parts[:8])) # 获取类别和difficult标志 cls parts[8] difficult int(parts[9]) if len(parts) 9 else 0 objects.append({points: points, class: cls, difficult: difficult}) return objects
2 坐标转换关键算法将旋转框转为水平框的核心是计算最小外接矩形。
使用Shapely库的MultiPoint可以优雅实现from shapely.geometry import MultiPoint def obb_to_hbb(points): # 将8个坐标点转为4个顶点 vertices [(points[i], points[i1]) for i in range(0, 8,
] multipoint MultiPoint(vertices) # 获取最小外接矩形 hbb multipoint.minimum_rotated_rectangle # 返回矩形四个顶点 return list(hbb.exterior.coords)[:4]但YOLO需要的是归一化的中心坐标和宽高还需要进行二次转换def hbb_to_yolo(vertices, img_width, img_height): # 计算边界 x_coords [p[0] for p in vertices] y_coords [p[1] for p in vertices] 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) /
/ img_width y_center ((y_min y_max) /
/ img_height width (x_max - x_min) / img_width height (y_max - y_min) / img_height return x_center, y_center, width, height
3 图像分块处理技巧DOTA图像尺寸过大平均4000×4000直接输入网络会显存爆炸。
我推荐使用滑动窗口分块def split_image(img, window_size1024, overlap
: height, width img.shape[:2] patches [] for y in range(0, height, window_size - overlap): for x in range(0, width, window_size - overlap): # 计算实际裁剪区域 x1 max(0, x) y1 max(0, y) x2 min(width, x window_size) y2 min(height, y window_size) patch img[y1:y2, x1:x2] patches.append((patch, (x1, y1, x2, y
)) return patches注意重叠区域overlap要设置合理我测试发现200像素能较好避免目标被切割。
实战中的
常见问题解决
1 类别映射问题DOTA的类别名称带有连字符如small-vehicle而YOLO通常用数字索引。
建议创建映射文件# dota_classes.yaml names: 0: plane 1: ship 2: storage-tank ... 14: helicopter
2 小目标丢失问题在转换过程中有些小目标10像素可能因坐标取整被过滤。
可以通过以下方式缓解# 在转换前添加过滤条件 if width * img_width 10 or height * img_height 10: print(f忽略小目标{cls} at ({x_center},{y_center})) continue
3 图像格式兼容性YOLO对PNG支持不如JPG稳定建议批量转换# 使用Imagemagick批量转换 mogrify -format jpg -quality 90 *.png
完整转换脚本示例以下是经过实战检验的完整转换脚本import os import cv2 from tqdm import tqdm from shapely.geometry import MultiPoint class DOTA2YOLO: def __init__(self, src_img_dir, src_label_dir, dst_dir): self.src_img_dir src_img_dir self.src_label_dir src_label_dir self.dst_dir dst_dir os.makedirs(os.path.join(dst_dir, images), exist_okTrue) os.makedirs(os.path.join(dst_dir, labels), exist_okTrue) def convert(self): img_files [f for f in os.listdir(self.src_img_dir) if f.lower().endswith((.png, .jpg))] for img_file in tqdm(img_files): # 处理图像 img_path os.path.join(self.src_img_dir, img_file) img cv
imread(img_path) h, w img.shape[:2] # 处理对应标注 label_file img_file.replace(.png, .txt).replace(.jpg, .txt) label_path os.path.join(self.src_label_dir, label_file) if not os.path.exists(label_path): continue yolo_labels [] objects self.parse_dota_label(label_path) for obj in objects: hbb self.obb_to_hbb(obj[points]) xc, yc, bw, bh self.hbb_to_yolo(hbb, w, h) yolo_labels.append(f{obj[class]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) # 保存结果 dst_label_path os.path.join(self.dst_dir, labels, label_file) with open(dst_label_path, w) as f: f.write(\n.join(yolo_labels)) # 转换并保存图像 dst_img_path os.path.join(self.dst_dir, images, img_file.replace(.png, .jpg)) cv
imwrite(dst_img_path, img) # 其他工具方法同上...使用时只需初始化并执行converter DOTA2YOLO(DOTA/train/images, DOTA/train/labels, YOLO_DOTA) converter.convert()
验证转换结果转换完成后强烈建议可视化检查import matplotlib.pyplot as plt import matplotlib.patches as patches def visualize(img_path, label_path, class_map): img cv
cvtColor(cv
imread(img_path), cv
COLOR_BGR2RGB) h, w img.shape[:2] fig, ax plt.subplots(1, figsize(12,
) ax.imshow(img) with open(label_path) as f: lines f.readlines() for line in lines: cls_idx, xc, yc, bw, bh map(float, line.strip().split()) # 转换回像素坐标 x (xc - bw/
* w y (yc - bh/
* h width bw * w height bh * h rect patches.Rectangle((x,y), width, height, linewidth2, edgecolorr, facecolornone) ax.add_patch(rect) plt.text(x, y, class_map[int(cls_idx)], colorwhite, bboxdict(facecolorred, alpha
0.
) plt.show()
高效训练技巧转换后的数据集可以这样配置YOLOv5训练# dota.yaml train: ../YOLO_DOTA/images/train val: ../YOLO_DOTA/images/val nc: 15 names: [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]启动训练时建议调整锚点参数python train.py --img 1024 --batch 8 --epochs 100 --data dota.yaml \ --weights yolov5s.pt --hyp data/hyps/hyp.scratch-low.yaml我在RTX 3090上测试发现使用--img 1024配合--batch 8能在显存占用和检测效果间取得平衡。
对于小目标密集的场景可以尝试以下改进增加--img-size到1536使用更密集的锚点配置添加小目标检测层