1. 从DOTA到YOLO:为什么需要转换数据集格式
第一次接触遥感图像目标检测时,我对着DOTA数据集里密密麻麻的四边形标注框发了好一会儿呆。这些被称为OBB(Oriented Bounding Box)的旋转框虽然能精准框住斜向停放的飞机或船舶,但主流的YOLOv5/v8等检测器默认只支持HBB(Horizontal Bounding Box)水平框。这就好比你想用标准螺丝刀拧三角螺丝——工具和零件不匹配。
DOTA数据集的特点确实令人印象深刻:
- 图像分辨率普遍在4000×4000像素以上
- 标注采用8个坐标点的多边形格式(x1,y1,x2,y2,...,x8,y8)
- 包含15个典型遥感目标类别(船舶、储油罐、运动场等)
- 单个图像可能包含数百个密集目标
而YOLO需要的却是简约的5参数格式:
类别索引 x_center y_center width height这个转换过程就像把复杂的立体折纸展开成平面图纸。我最初尝试手动转换时,发现直接取旋转框的外接矩形会导致大量无效背景区域被包含,特别是对于长宽比悬殊的桥梁、船舶等目标。后来通过分析DOTA_devkit源码才找到正确解法——应该计算所有顶点坐标的最小外接水平矩形。
2. 数据准备与环境配置
2.1 获取原始数据集
建议直接从DOTA官网下载基准数据集,目前主流版本有:
- DOTA-v1.0(2.8GB,15类别)
- DOTA-v1.5(6.2GB,新增集装箱起重机类别)
- DOTA-v2.0(35.4GB,18类别)
如果网络条件受限,可以使用我预处理过的HBB版本(包含已转换的YOLO格式标签):
# 百度云下载(密码:iw3w) wget https://pan.baidu.com/s/1UX7oX3_x5CrP_SxSA7XKXQ2.2 安装关键工具包
处理过程中需要以下Python包:
# 基础环境 pip install numpy opencv-python pillow # 专用工具 pip install dota-utils shapely特别提醒:建议使用Shapely 1.7.1版本,新版本在计算多边形几何时可能有API变动。我在Colab上测试时遇到过这样的报错:
AttributeError: 'Polygon' object has no attribute '_get_coords'就是版本兼容性问题导致的。
3. 核心转换流程详解
3.1 标注文件解析实战
DOTA的标注文件是这样的文本格式:
imagesource:GoogleEarth gsd:0.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 objects3.2 坐标转换关键算法
将旋转框转为水平框的核心是计算最小外接矩形。使用Shapely库的MultiPoint可以优雅实现:
from shapely.geometry import MultiPoint def obb_to_hbb(points): # 将8个坐标点转为4个顶点 vertices = [(points[i], points[i+1]) for i in range(0, 8, 2)] 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) / 2) / img_width y_center = ((y_min + y_max) / 2) / img_height width = (x_max - x_min) / img_width height = (y_max - y_min) / img_height return x_center, y_center, width, height3.3 图像分块处理技巧
DOTA图像尺寸过大(平均4000×4000),直接输入网络会显存爆炸。我推荐使用滑动窗口分块:
def split_image(img, window_size=1024, overlap=200): 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, y2))) return patches注意重叠区域(overlap)要设置合理,我测试发现200像素能较好避免目标被切割。
4. 实战中的常见问题解决
4.1 类别映射问题
DOTA的类别名称带有连字符(如"small-vehicle"),而YOLO通常用数字索引。建议创建映射文件:
# dota_classes.yaml names: 0: plane 1: ship 2: storage-tank ... 14: helicopter4.2 小目标丢失问题
在转换过程中,有些小目标(<10像素)可能因坐标取整被过滤。可以通过以下方式缓解:
# 在转换前添加过滤条件 if width * img_width < 10 or height * img_height < 10: print(f"忽略小目标:{cls} at ({x_center},{y_center})") continue4.3 图像格式兼容性
YOLO对PNG支持不如JPG稳定,建议批量转换:
# 使用Imagemagick批量转换 mogrify -format jpg -quality 90 *.png5. 完整转换脚本示例
以下是经过实战检验的完整转换脚本:
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_ok=True) os.makedirs(os.path.join(dst_dir, 'labels'), exist_ok=True) 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 = cv2.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')) cv2.imwrite(dst_img_path, img) # 其他工具方法同上...使用时只需初始化并执行:
converter = DOTA2YOLO('DOTA/train/images', 'DOTA/train/labels', 'YOLO_DOTA') converter.convert()6. 验证转换结果
转换完成后,强烈建议可视化检查:
import matplotlib.pyplot as plt import matplotlib.patches as patches def visualize(img_path, label_path, class_map): img = cv2.cvtColor(cv2.imread(img_path), cv2.COLOR_BGR2RGB) h, w = img.shape[:2] fig, ax = plt.subplots(1, figsize=(12, 8)) 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/2) * w y = (yc - bh/2) * h width = bw * w height = bh * h rect = patches.Rectangle((x,y), width, height, linewidth=2, edgecolor='r', facecolor='none') ax.add_patch(rect) plt.text(x, y, class_map[int(cls_idx)], color='white', bbox=dict(facecolor='red', alpha=0.5)) plt.show()7. 高效训练技巧
转换后的数据集可以这样配置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
- 使用更密集的锚点配置
- 添加小目标检测层