从标注文件看门道:手把手教你用Python解析UCAS-AOD、DOTA、FAIR1M的txt/xml标签
从标注文件看门道:手把手教你用Python解析遥感目标检测数据集标签
第一次打开UCAS-AOD数据集时,我盯着那些密密麻麻的坐标数字完全摸不着头脑。x1,y1,x2,y2...这些数字到底代表什么?为什么有的数据集用txt,有的用xml?更让人困惑的是,同样的飞机目标,在不同数据集里标注格式竟然完全不同。这就是大多数初学者面对遥感目标检测数据集时的真实写照——数据拿到了,却不知道如何"拆封"使用。
本文将用最直白的方式,带你破解UCAS-AOD、DOTA和FAIR1M这三种典型数据集的标注密码。不同于单纯罗列数据集参数的介绍文章,我们会用可运行的Python代码,一步步教你如何:
- 解析不同格式的标注文件(txt/xml)
- 提取关键坐标信息(包括普通框和旋转框)
- 用OpenCV和matplotlib实现标注可视化
- 理解不同标注格式的设计逻辑
1. 解析UCAS-AOD的水平旋转框(HBB)
UCAS-AOD采用了一种独特的"水平旋转框"标注方式。打开任意一个.txt标注文件,你会看到类似这样的内容:
128.45 356.21 156.78 342.15 164.32 368.77 136.12 382.54 0.45 150.23 362.45 32.67 40.12这串数字到底怎么解读?其实它们对应的是:
x1,y1, # 旋转框第一个顶点 x2,y2, # 旋转框第二个顶点 x3,y3, # 旋转框第三个顶点 x4,y4, # 旋转框第四个顶点 theta, # 旋转角度(弧度) x,y, # 水平外接矩形中心点 width,height # 水平外接矩形宽高关键点:前8个数字定义了一个旋转矩形,后5个数字则是该旋转矩形的水平外接矩形参数。这种双重标注方式既保留了目标的方向信息,又提供了传统检测算法需要的水平框。
下面这段代码展示了如何解析并绘制这种特殊标注:
import numpy as np import cv2 def parse_ucas_aod_line(line): data = list(map(float, line.strip().split())) points = np.array(data[:8]).reshape(4, 2) # 提取四个顶点 angle = data[8] # 旋转角度 return points, angle def draw_rotated_box(img, points, color=(0,255,0), thickness=2): # 绘制旋转四边形 points = points.reshape((-1,1,2)).astype(int) cv2.polylines(img, [points], isClosed=True, color=color, thickness=thickness) # 标记顶点顺序 for i, (x,y) in enumerate(points.reshape(-1,2)): cv2.putText(img, str(i+1), (x,y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,0,0), 1) # 使用示例 img = cv2.imread('P0001.png') with open('P0001.txt') as f: points, angle = parse_ucas_aod_line(f.readline()) draw_rotated_box(img, points) cv2.imshow('UCAS-AOD', img) cv2.waitKey(0)注意:UCAS-AOD的旋转角度theta定义为机尾指向机头的向量与x轴正向的夹角,顺时针方向为正。这与OpenCV的坐标系定义一致。
通过这段代码,我们能直观看到标注框如何贴合飞机目标。下图展示了实际解析效果:
图示:绿色轮廓为旋转框,红色数字标记顶点顺序,蓝色箭头表示机头方向
2. 破解DOTA的任意四边形标注(OBB)
DOTA数据集采用了更通用的OBB(Oriented Bounding Box)标注格式。每个目标的标注信息如下:
x1 y1 x2 y2 x3 y3 x4 y4 category difficult与UCAS-AOD不同,DOTA的四个顶点可以构成任意四边形(不一定是矩形),且顶点按顺时针顺序排列。这种标注方式对不规则目标(如港口、立交桥)的表征更加精确。
解析DOTA标注的关键代码如下:
from matplotlib import pyplot as plt def parse_dota_annotation(ann_file): objects = [] with open(ann_file) as f: for line in f: if line.startswith('imagesource'): # 跳过文件头 continue parts = line.strip().split() if len(parts) < 9: # 确保有足够数据 continue points = list(map(float, parts[:8])) category = parts[8] difficult = int(parts[9]) if len(parts) > 9 else 0 objects.append({ 'points': np.array(points).reshape(4, 2), 'category': category, 'difficult': difficult }) return objects def plot_dota_objects(img_path, ann_path): img = plt.imread(img_path) objects = parse_dota_annotation(ann_path) fig, ax = plt.subplots(figsize=(10,10)) ax.imshow(img) for obj in objects: points = obj['points'] # 绘制四边形 poly = plt.Polygon(points, fill=False, edgecolor='red', linewidth=1) ax.add_patch(poly) # 标记类别 center = points.mean(axis=0) ax.text(center[0], center[1], obj['category'], bbox=dict(facecolor='yellow', alpha=0.5)) plt.show() # 使用示例 plot_dota_objects('P0006.png', 'P0006.txt')DOTA标注的几个特点值得注意:
- 顶点顺序一致性:所有四边形的顶点必须按顺时针排列,这对后续计算旋转角度很重要
- difficult标志:标记难以识别的目标(如遮挡严重或非常小的目标)
- 多尺度兼容:同一张图片中可能同时存在极大和极小的目标
下表对比了UCAS-AOD和DOTA的标注差异:
| 特征 | UCAS-AOD | DOTA |
|---|---|---|
| 标注形状 | 旋转矩形 | 任意四边形 |
| 顶点顺序 | 无严格要求 | 必须顺时针 |
| 角度信息 | 显式提供 | 需通过顶点计算 |
| 类别粒度 | 2类(飞机/汽车) | 15类 |
| 适用场景 | 方向敏感目标 | 任意形状目标 |
3. 解析FAIR1M的XML结构标注
FAIR1M作为目前最精细的遥感数据集,采用了XML格式存储标注信息。一个典型的标注片段如下:
<object> <coordinate>pixel</coordinate> <type>rectangle</type> <possibleresult> <name>Liquid Cargo Ship</name> </possibleresult> <points> <point>1275.0,458.0</point> <point>1494.0,88.0</point> <point>1417.0,43.0</point> <point>1199.0,414.0</point> <point>1275.0,458.0</point> </points> </object>与之前两种数据集相比,FAIR1M的标注特点包括:
- 层次化结构:XML格式天然支持嵌套数据
- 细粒度分类:如船舶细分为Liquid Cargo Ship等子类
- 闭合多边形:第五个点与第一个点相同,形成闭合路径
使用Python的xml.etree.ElementTree解析FAIR1M标注:
import xml.etree.ElementTree as ET def parse_fair1m_xml(xml_path): tree = ET.parse(xml_path) root = tree.getroot() objects = [] for obj in root.findall('objects/object'): category = obj.find('possibleresult/name').text points = [] for point in obj.findall('points/point'): x, y = map(float, point.text.split(',')) points.append([x, y]) # 移除重复的闭合点 if len(points) > 1 and points[-1] == points[0]: points = points[:-1] objects.append({ 'category': category, 'points': np.array(points) }) return objects def visualize_fair1m(img_path, xml_path): img = cv2.imread(img_path) objects = parse_fair1m_xml(xml_path) for obj in objects: points = obj['points'] # 绘制多边形 cv2.polylines(img, [points.astype(int)], isClosed=True, color=(0,255,255), thickness=2) # 标记类别 center = points.mean(axis=0).astype(int) cv2.putText(img, obj['category'], tuple(center), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,0), 1) cv2.imshow('FAIR1M', img) cv2.waitKey(0)FAIR1M的XML解析需要注意几个细节:
- 坐标类型检查:
<coordinate>标签可能指定像素坐标或地理坐标 - 头部标记:某些类别(如飞机)的第一个点表示头部位置
- 多目标支持:一个XML文件可能包含数百个对象实例
4. 三种数据集标注转换实战
实际项目中,我们经常需要统一不同数据集的标注格式。下面提供三种常见转换方法:
4.1 旋转框转水平框
def rotated_to_horizontal(points): """将旋转框转换为水平外接矩形""" x_min = points[:,0].min() x_max = points[:,0].max() y_min = points[:,1].min() y_max = points[:,1].max() return np.array([ [x_min, y_min], [x_max, y_min], [x_max, y_max], [x_min, y_max] ])4.2 四边形转旋转矩形
def quad_to_rotated_rect(points): """将任意四边形转换为旋转矩形""" rect = cv2.minAreaRect(points) box = cv2.boxPoints(rect) return box4.3 XML转YOLO格式
def fair1m_to_yolo(xml_path, img_size=(1024,1024)): """将FAIR1M XML转换为YOLO格式""" objects = parse_fair1m_xml(xml_path) yolo_lines = [] for obj in objects: points = obj['points'] # 计算水平外接矩形 x_min, y_min = points.min(axis=0) x_max, y_max = points.max(axis=0) # 转换为YOLO格式(中心点+宽高,归一化) x_center = ((x_min + x_max) / 2) / img_size[0] y_center = ((y_min + y_max) / 2) / img_size[1] width = (x_max - x_min) / img_size[0] height = (y_max - y_min) / img_size[1] yolo_lines.append(f"0 {x_center} {y_center} {width} {height}") return "\n".join(yolo_lines)提示:实际转换时需要考虑类别ID映射,上述代码假设所有对象都属于类别0
下表总结了三种数据集标注的互换性:
| 转换方向 | 可行性 | 信息损失 |
|---|---|---|
| OBB → HBB | 高 | 丢失旋转信息 |
| HBB → OBB | 低 | 无法恢复原始形状 |
| XML → TXT | 高 | 可能丢失层级信息 |
| TXT → XML | 中 | 需要补充元数据 |
5. 标注可视化进阶技巧
基础的框绘制可能无法满足分析需求,下面介绍几种增强可视化效果的方法:
5.1 带透明度的填充
def draw_transparent_polygon(img, points, color, alpha=0.3): overlay = img.copy() cv2.fillPoly(overlay, [points.astype(int)], color) cv2.addWeighted(overlay, alpha, img, 1-alpha, 0, img)5.2 方向箭头标记
def draw_orientation(img, points, color=(255,0,0)): """根据顶点顺序绘制方向箭头""" head = points[0] # 假设第一个点是头部 tail = points[2] # 对角点作为尾部 cv2.arrowedLine(img, tuple(tail.astype(int)), tuple(head.astype(int)), color, 2)5.3 多类别颜色编码
COLOR_MAP = { 'plane': (0,255,0), 'ship': (0,0,255), 'vehicle': (255,0,0), # 其他类别... } def draw_by_category(img, objects): for obj in objects: color = COLOR_MAP.get(obj['category'], (255,255,255)) draw_rotated_box(img, obj['points'], color)5.4 复杂场景下的可视化优化
当图像中存在大量密集目标时,常规绘制会导致视觉混乱。解决方法包括:
- 分层显示:按目标尺寸或类别分层显示
- 交互式查看:使用matplotlib的缩放功能
- 热力图叠加:用不同颜色表示目标密度
def interactive_plot(img_path, ann_path): img = plt.imread(img_path) objects = parse_annotation(ann_path) # 通用解析函数 fig, ax = plt.subplots(figsize=(15,15)) ax.imshow(img) # 按类别分组绘制 categories = set(obj['category'] for obj in objects) colors = plt.cm.get_cmap('tab20', len(categories)) for i, cat in enumerate(categories): cat_objs = [obj for obj in objects if obj['category']==cat] for obj in cat_objs: poly = plt.Polygon(obj['points'], fill=False, edgecolor=colors(i), linewidth=1, label=cat if i==0 else "") ax.add_patch(poly) plt.legend() plt.show()6. 常见问题与解决方案
在实际解析过程中,可能会遇到各种意外情况。以下是几个典型问题及解决方法:
6.1 坐标越界处理
def clip_coordinates(points, img_width, img_height): """确保坐标不超出图像边界""" points[:,0] = np.clip(points[:,0], 0, img_width-1) points[:,1] = np.clip(points[:,1], 0, img_height-1) return points6.2 无效标注过滤
def validate_annotation(points): """检查标注是否有效""" # 检查面积是否过小 area = cv2.contourArea(points) if area < 4: # 小于2x2像素 return False # 检查是否为凸多边形 hull = cv2.convexHull(points) if not np.array_equal(points, hull.squeeze()): return False return True6.3 顶点顺序校正
def ensure_clockwise(points): """确保顶点按顺时针排列""" if cv2.contourArea(points) < 0: return points[::-1] return points6.4 多线程解析加速
对于大型数据集(如FAIR1M),可以使用多线程加速解析:
from concurrent.futures import ThreadPoolExecutor def batch_parse_xml(xml_files, workers=8): with ThreadPoolExecutor(max_workers=workers) as executor: results = list(executor.map(parse_fair1m_xml, xml_files)) return results7. 从解析到训练:构建数据管道
掌握了标注解析方法后,下一步是构建完整的数据加载管道。以PyTorch为例:
from torch.utils.data import Dataset class RemoteSenseDataset(Dataset): def __init__(self, img_dir, ann_dir, transform=None): self.img_dir = img_dir self.ann_dir = ann_dir self.transform = transform self.img_files = [f for f in os.listdir(img_dir) if f.endswith(('.png','.jpg'))] def __len__(self): return len(self.img_files) def __getitem__(self, idx): img_path = os.path.join(self.img_dir, self.img_files[idx]) img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 根据扩展名选择解析方式 ann_path = os.path.join(self.ann_dir, os.path.splitext(self.img_files[idx])[0] + ('.txt' if 'DOTA' in self.ann_dir else '.xml')) if ann_path.endswith('.txt'): objects = parse_dota_annotation(ann_path) else: objects = parse_fair1m_xml(ann_path) # 转换为模型需要的格式 boxes = [] labels = [] for obj in objects: boxes.append(obj['points'].flatten().tolist()) labels.append(CLASS_DICT[obj['category']]) target = { 'boxes': torch.as_tensor(boxes, dtype=torch.float32), 'labels': torch.as_tensor(labels, dtype=torch.int64) } if self.transform: img, target = self.transform(img, target) return img, target关键设计考虑:
- 统一接口:处理不同数据集的差异
- 延迟加载:只在需要时读取图像和标注
- 转换支持:集成数据增强管道
- 内存效率:避免一次性加载所有数据
8. 标注质量检查与修复
低质量标注会严重影响模型性能。以下是几种自动化检查方法:
8.1 重叠检测
def find_overlaps(objects, iou_thresh=0.3): overlaps = [] for i, obj1 in enumerate(objects): for j, obj2 in enumerate(objects[i+1:], i+1): iou = calculate_polygon_iou(obj1['points'], obj2['points']) if iou > iou_thresh: overlaps.append((i,j,iou)) return overlaps8.2 小目标检测
def find_small_objects(objects, min_area=16): return [i for i, obj in enumerate(objects) if cv2.contourArea(obj['points']) < min_area]8.3 类别平衡分析
def analyze_class_balance(objects_list): counter = Counter() for objects in objects_list: counter.update(obj['category'] for obj in objects) plt.bar(counter.keys(), counter.values()) plt.xticks(rotation=45) plt.title('Class Distribution') plt.show()8.4 自动修复工具
对于常见问题,可以编写自动修复脚本:
def auto_fix_annotation(objects, img_size): fixed = [] for obj in objects: # 修复坐标越界 obj['points'] = clip_coordinates(obj['points'], *img_size) # 过滤无效标注 if not validate_annotation(obj['points']): continue # 校正顶点顺序 obj['points'] = ensure_clockwise(obj['points']) fixed.append(obj) return fixed9. 自定义标注工具开发
理解标注格式后,我们可以开发专用标注工具。核心功能包括:
- 格式转换器:不同标注格式互转
- 可视化检查器:带缩放和筛选功能的查看器
- 质量分析仪:统计标注分布和质量指标
- 半自动标注:基于模型预测辅助标注
import tkinter as tk from PIL import Image, ImageTk class AnnotationViewer: def __init__(self, master, img_path, ann_path): self.master = master self.img = Image.open(img_path) self.objects = self.parse_annotation(ann_path) self.canvas = tk.Canvas(master, width=self.img.width, height=self.img.height) self.canvas.pack() self.tk_img = ImageTk.PhotoImage(self.img) self.canvas.create_image(0,0, anchor=tk.NW, image=self.tk_img) self.draw_annotations() def draw_annotations(self): for obj in self.objects: points = obj['points'].flatten().tolist() self.canvas.create_polygon(points, outline='red', fill='', width=2) center = obj['points'].mean(axis=0) self.canvas.create_text(center[0], center[1], text=obj['category'], fill='yellow')这个简单的Tkinter应用展示了如何构建一个基础标注查看器。实际项目中,可以考虑使用专业的图像处理库如OpenCV或PyQt构建功能更完善的工具。
10. 性能优化与最佳实践
处理大规模遥感数据集时,性能优化至关重要:
内存映射文件:对于超大型图像
def read_big_image(img_path): return np.memmap(img_path, dtype='uint8', mode='r', shape=(height, width, channels))标注缓存机制:避免重复解析
from functools import lru_cache @lru_cache(maxsize=1000) def cached_parse(ann_path): return parse_annotation(ann_path)批量图像处理:利用GPU加速
def batch_visualize(img_paths, ann_paths): imgs = [cv2.imread(p) for p in img_paths] anns = [parse_annotation(p) for p in ann_paths] # 使用GPU加速绘制 with torch.no_grad(): batch = torch.stack([torch.from_numpy(img) for img in imgs]) batch = batch.cuda().permute(0,3,1,2).float() # ... GPU上的批量绘制逻辑 ...分布式处理:适用于超大规模数据集
from multiprocessing import Pool def parallel_process(args): img_path, ann_path = args img = cv2.imread(img_path) objects = parse_annotation(ann_path) # 处理逻辑... return result with Pool(processes=8) as pool: results = pool.map(parallel_process, zip(img_paths, ann_paths))
11. 实际项目经验分享
在多个遥感目标检测项目中,我总结了以下几点实用建议:
混合数据集训练:当单个数据集样本不足时,可以合并多个数据集。这时需要:
- 统一标注格式(建议转换为DOTA格式)
- 处理类别名称不一致问题(如"car" vs "vehicle")
- 平衡不同数据集的样本分布
处理标注不一致:不同数据集的标注标准可能不同,例如:
- UCAS-AOD标注整个飞机,而FAIR1M可能只标注可见部分
- 对于被遮挡目标,有的数据集标注完整轮廓,有的只标注可见部分
数据增强策略:遥感图像的特殊性需要考虑:
- 旋转增强要同步更新角度标注
- 裁剪时要注意不要切掉重要目标
- 色彩增强要适度,保持光谱特性
处理极端长宽比目标:如桥梁、跑道等目标需要:
- 特殊的数据增强方法
- 定制化的锚框设计
- 考虑旋转不变性的网络结构
多尺度训练技巧:遥感图像中的目标尺度差异巨大:
- 使用FPN等多尺度网络结构
- 实施分尺度训练策略
- 对极小目标使用特殊处理(如超分辨率)
12. 扩展应用:基于标注的分析
标注数据不仅可以用于训练,还能支持多种分析:
数据集统计分析:
def analyze_dataset(ann_dir): sizes = [] ratios = [] for ann_file in glob.glob(os.path.join(ann_dir, '*.txt')): objects = parse_annotation(ann_file) for obj in objects: w = obj['points'][:,0].max() - obj['points'][:,0].min() h = obj['points'][:,1].max() - obj['points'][:,1].min() sizes.append(w * h) ratios.append(max(w,h)/min(w,h)) plt.figure(figsize=(12,4)) plt.subplot(121) plt.hist(sizes, bins=50) plt.title('Object Size Distribution') plt.subplot(122) plt.hist(ratios, bins=50) plt.title('Aspect Ratio Distribution') plt.show()目标分布热力图:
def plot_density_heatmap(img_size, objects): heatmap = np.zeros(img_size) for obj in objects: points = obj['points'].astype(int) cv2.fillPoly(heatmap, [points], 1) plt.imshow(heatmap, cmap='hot') plt.colorbar() plt.title('Object Density Heatmap') plt.show()方向分布分析(对旋转目标尤为重要):
def analyze_orientation(objects): angles = [] for obj in objects: rect = cv2.minAreaRect(obj['points']) angles.append(rect[2]) plt.hist(angles, bins=36, range=(0,180)) plt.title('Object Orientation Distribution') plt.xlabel('Angle (degrees)') plt.show()类别共现分析:
def analyze_cooccurrence(objects_list): cooccur = defaultdict(int) for objects in objects_list: categories = [obj['category'] for obj in objects] for cat1, cat2 in combinations(set(categories), 2): cooccur[(min(cat1,cat2), max(cat1,cat2))] += 1 # 转换为矩阵显示 cats = sorted(set(cat for pair in cooccur for cat in pair)) matrix = np.zeros((len(cats), len(cats))) for i, cat1 in enumerate(cats): for j, cat2 in enumerate(cats[i:], i): matrix[i,j] = cooccur.get((cat1,cat2), 0) plt.imshow(matrix, cmap='Blues') plt.xticks(range(len(cats)), cats, rotation=90) plt.yticks(range(len(cats)), cats) plt.title('Category Co-occurrence Matrix') plt.colorbar() plt.show()
13. 前沿标注格式探索
随着技术进步,新的标注格式不断涌现:
COCO格式:已成为通用目标检测基准
{ "info": {...}, "licenses": [...], "images": [...], "annotations": [ { "id": 1, "image_id": 1, "category_id": 1, "segmentation": [[x1,y1,x2,y2,...]], "area": 123.45, "bbox": [x,y,width,height], "iscrowd": 0 } ], "categories": [...] }YOLOv8格式:支持旋转目标
class x_center y_center width height angleLIDAR融合标注:结合点云数据
<object> <camera_bbox>x1,y1,x2,y2,x3,y3,x4,y4</camera_bbox> <lidar_bbox>center_x,center_y,center_z,length,width,height,yaw</lidar_bbox> </object>时序标注:用于视频分析
{ "video_id": "001", "frames": [ { "frame_id": 0, "objects": [ {"track_id": 1, "points": [...]}, ... ] }, ... ] }
14. 标注规范建议
根据实际项目经验,推荐以下标注规范:
文件结构:
dataset/ ├── images/ │ ├── train/ │ └── val/ └── annotations/ ├── train/ │ ├── P0001.txt │ └── ... └── val/ ├── P1001.txt └── ...命名规则:
- 图像:
[集合][序号].[扩展名](如P0001.png) - 标注:与图像同名,扩展名根据格式变化
- 图像:
标注内容:
- 每个目标一行
- 包含足够的元数据(如difficult标志)
- 明确顶点顺序约定
版本控制:
- 使用Git管理标注文件
- 每次修改保留变更记录
- 对大规模数据集使用DVC
质量检查清单:
- [ ] 所有目标都被标注
- [ ] 无遗漏的小目标
- [ ] 边界框紧贴目标边缘
- [ ] 无重叠框(特殊要求除外)
- [ ] 类别标签正确
15. 总结与进阶方向
通过本文的代码示例和分析,你应该已经掌握:
- 三种主流遥感数据集的标注解析方法
- 标注可视化和质量检查技巧
- 不同标注格式间的转换策略
- 高效处理大规模标注数据的优化方法
进一步学习方向包括:
- 自动化标注工具:研究SAM等模型辅助标注
- 标注质量提升:开发自动检查和修复工具
- 多模态标注:融合光学、SAR、LiDAR等多源数据
- 动态标注:处理视频时序数据
- 众包标注系统:设计高效的分布式标注平台
在实际项目中,我建议先从DOTA数据集入手,因为它的标注格式兼顾了灵活性和简洁性。当遇到性能瓶颈时,可以考虑使用多进程解析和内存映射技术。对于需要长期维护的项目,建立严格的标注规范和版本控制流程至关重要。
