RETT100K转YOLO格式全流程:遥感目标检测数据预处理实战

发布时间:2026/10/18 16:33:58
RETT100K转YOLO格式全流程:遥感目标检测数据预处理实战 做遥感目标检测的人最近很少没听过 RETT100K 这个名字。这个新公开的大规模遥感数据集目标类别覆盖广、场景来源杂尤其难能可贵的是样本量做到了十万量级刚好补上了通用遥感检测基准里“大而全”这个空档。但拿到手的第一天绝大多数人都会面临同一个尴尬它的标注方式不是 YOLO 生态原生支持的那一套直接丢给训练脚本跑不动必须先把标注格式转换干净才能进入正常的数据加载、训练和评估流程。这篇文章就把我从 RETT100K 原始格式一路转到 YOLO 格式、再跑通训练的完整过程写下来包括坐标换算、边界条件处理、数据集划分、验证可视化以及训练阶段踩过的几个坑希望能帮你少走弯路拿到数据当天就把格式问题彻底解决。1. 为什么要把 RETT100K 搬进 YOLO而不是反过来迁就它1.1 遥感检测任务和通用检测任务的数据差异我先说结论遥感目标检测和日常图片检测在数据层面看着都是“框住目标”实际上有三个明显区别。第一是目标的尺度分布极端遥感图像里既有占据大半个画面的机场建筑群也有只有十几像素的小型车辆和船只跨尺度的幅度比普通场景大得多。第二是目标朝向任意很多地物本身是旋转的用外接水平框表达时会引入大量背景干扰这也是遥感数据集普遍喜欢用多边形或者旋转框标注的原因。第三是场景密度高一张大图里叠着成百上千个目标标注文件如果不做归一化处理训练时会出现严重的批次不平衡和读取瓶颈。RETT100K 正是在这三个维度上都做了加强图像来源多样不再是单一卫星或单一飞行高度拍出来的整齐数据而是混合了不同分辨率、不同光照、不同地形背景的遥感影像。单一标注方式很难完美适配所有场景所以它的原始标注字段也比较“生猛”既有目标类别也有坐标点集合还有一堆辅助属性信息。要把它喂给以 YOLO 为代表的单阶段检测器就必须把所有这些信息精简成 YOLO 唯一认账的纯文本格式每张图像对应一个同名 txt 文件每一行写一个目标的类别编号和归一化后的框坐标。1.2 YOLO 标注格式的“唯一真理”到底长什么样YOLO 标注格式极度简单简单到很多人刚上手时反而容易出错。一个标准的 label 文件里每一行是五个数字class_id x_center y_center width height。其中class_id是从 0 开始的整数对应你定义好的类别表x_center和y_center是目标框中心点相对于图像宽高的比例width和height是目标框宽高相对于图像宽高的比例。所有数值都在 0 到 1 之间保留六位小数就足够了。这个格式最大的优势是尺度无关。同一份标注文件不管训练时把图像缩放到 640 还是 1024框的相对位置都不会失真这也是 YOLO 训练管线能高效批量加载的原因。但它的代价也很明显一旦原始标注里的坐标是像素绝对坐标、或者带了旋转角度、或者坐标值本身超出了图像边界转换时不做规范化处理就会把一堆病变数据带进训练集导致 loss 乱跳、mAP 上不去。所以整个转换工作的本质不是“写个脚本把字符串格式换一换”而是要把 RETT100K 里所有可能的坐标表达、类别表达、异常表达统一映射到 YOLO 这种极度精简的格式里。这一步做好了后面训练都是顺水推舟这一步偷懒坑全在后头。1.3 我的转换思路一次解析两种输出三次校验为避免在转换链路里反复返工我先定了一个整体方案写一个统一的解析器把 RETT100K 原始 JSON 标注读进来统一转换成中间的内存数据结构然后从这个结构分别输出 YOLO 的 txt 标注、类别名称表、以及用于人工抽查的可视化图片。整个过程分三步校验解析阶段校验原始数据有没有缺字段转换阶段校验归一化数值是否在合法区间输出阶段校验每张图像是否有对应标注、每个标注是否落在图像范围内。这么做的好处是当你发现训出来的模型有问题时能很快定位到底是原始数据脏、转换脚本错、还是训练参数不合适。我见过很多同行图省事直接在原始数据集脚本里改两行就往外吐标注前期看起来很顺后面回来排查时发现标注文件和图像对不上号只能从头再做一遍。一次性把校验逻辑写进去看着多花两小时实际上省了后面两三天。2. 转换前的摸底工作把原始标注读懂、读透2.1 先看数据集的目录结构和标注文件 schema拿到 RETT100K 之后我第一件事不是写脚本而是把所有公开文档和目录结构先摸一遍。这个数据集通常会把图像和标注分开放标注采用 JSON 格式每个 JSON 文件对应一张图像路径命名和图像文件名一一对应。这里有一个很容易被忽略的细节JSON 里的文件名字段和实际图像文件名必须严格匹配一旦两边不一致后面做数据划分时就会出现图像有标注、标注没图像的悬空现象。我建议你拿到任何新数据集先写一个最小化的“体检脚本”统计三件事第一批标注文件里的字段列表所有类别名称去重后的总数以及每个 JSON 是否包含图像宽高字段。很多遥感数据集的图像宽高不是从图像文件里读的而是写死在 JSON 里如果你的转换脚本直接读图像文件的宽高碰上损坏或者异常编码的图像整个转换就会中断。以我这次处理的 RETT100K 子集为例原始标注字段大致是下面这个结构实际大家拿到的版本可能字段名略有差异但万变不离其宗{ image_id: 000001, file_name: 000001.jpg, width: 1024, height: 1024, objects: [ { category: ship, bbox: [120.5, 80.2, 340.7, 210.3], polygon: [120.5, 80.2, 340.7, 95.6, 355.1, 210.3, 130.2, 198.7], truncated: false, difficult: false } ] }这里的bbox字段通常是x1, y1, x2, y2的像素绝对坐标polygon是四个角点按x1, y1, x2, y3, x3, y4, x4, y4排列的多边形坐标。有些子集还带truncated和difficult标记——谨慎处理这些标记是写好转换脚本的关键一步。2.2 类别表先于转换脚本定下来否则后患无穷RETT100K 里的类别编号顺序并不一定等于你训练时想要的编号顺序。原始 JSON 里可能用字符串类别名也可能用类别编号而这个编号在不同版本的标注里还有可能对不上。我在动手之前先把全量数据里的类别名扫一遍按字母序排好再人工核对一遍手动指定一个类别表写入一个classes.txt之后所有脚本都读这个文件来做映射。这样做的好处有两个一是类别编号和训练时data.yaml里的names列表严格对应不会出现训练时显示类别名但实际标签编号错位的问题二是换数据集版本时只需要更新这个文件转换脚本本身不用动。如果你偷懒让脚本自动从 JSON 里按出现顺序生成类别表排序那一环出了问题或者两个子集类别顺序不一致训练出来的模型类别预测就会整体错乱而且这类错误不会在 loss 曲线上有显著体现排查起来非常痛苦。2.3 全量扫描异常框提前排除“脏数据”遥感数据集几乎必然存在三类异常标注坐标越界的框、宽高为零的退化框、以及目标完全落在图像外的无效框。这些异常数据如果直接参与训练轻则拉低收敛速度重则让梯度产生异常抖动。所以我在转换脚本里加入了一个“清洗层”坐标越界的框根据越界程度分两种处理轻度越界比如超出几个像素做截断重度越界比如目标中心都不在图像内直接丢弃并且把丢弃记录写入日志文件。这里我特别提醒一个原则宁可多写几个日志也不要让程序静默吞掉异常。转换脚本跑完以后我会专门看一眼废弃列表如果发现某个类别被整片丢弃那通常是解析 bug而不是数据真的全错。把日志做成 CSV 格式按类别、图像名、异常类型分组统计五分钟就能定位问题。3. 坐标换算与批量生成脚本核心转换链路拆解3.1 把像素坐标平稳换算成归一化坐标坐标换算是整个转换逻辑的核心。标准 YOLO 归一化公式很简单但细节里全是坑。假设图像宽高是W和H原始框是[x1, y1, x2, y2]那么框宽度w x2 - x1框高度h y2 - y1中心点cx (x1 x2) / 2中心点cy (y1 y2) / 2归一化后x_center cx / Wy_center cy / Hwidth w / Wheight h / H听起来毫无技术含量但有三个边界场景必须写进代码里。第一个是坐标可能出现浮点偏差导致x2比x1还小一点这时要先做排序再算宽高。第二个是坐标可能给的是多边形八个点此时仍然可以用多边形的最小外接矩形来转水平框但你要想清楚这个操作会引入多少背景噪声后文我会专门讨论旋转框的处理。第三个是归一化结果可能因为原始坐标越界而大于 1 或小于 0必须做检查、修正或剔除。基于上面这些规则我的核心转换函数长这样def convert_bbox_to_yolo(bbox, img_w, img_h): x1, y1, x2, y2 bbox # 防止坐标顶点顺序异常 x1, x2 sorted((x1, x2)) y1, y2 sorted((y1, y2)) w x2 - x1 h y2 - y1 if w 0 or h 0: return None cx (x1 x2) / 2.0 cy (y1 y2) / 2.0 return cx / img_w, cy / img_h, w / img_w, h / img_h注意这里返回的已经是归一化数值。独立封装的好处是后续如果要处理 OBB 旋转框或者要批量做数据增强改动点仅限于这一个函数内部不会牵连全局。3.2 从 JSON 到 YOLO 文件的全流程脚本有了坐标函数之后剩下的就是批量读取 JSON、映射类别、写文件。我习惯把整个脚本按“解析、转换、写出、校验”拆成几个独立的函数方便单步调试。下面这个脚本是我实际跑通过的版本注释里写了关键细节import json import logging from pathlib import Path def parse_annotation(json_path): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w data[width] img_h data[height] objects data[objects] parsed [] for obj in objects: category obj[category] bbox obj.get(bbox) if bbox is None and polygon in obj: xs obj[polygon][0::2] ys obj[polygon][1::2] bbox [min(xs), min(ys), max(xs), max(ys)] if bbox is None: logging.warning(f{json_path.name}: object with no bbox/polygon) continue parsed.append({category: category, bbox: bbox}) return img_w, img_h, parsed def write_yolo_label(parsed, img_w, img_h, category2id, out_label_path, filter_out_of_boundsTrue): lines [] for obj in parsed: cid category2id.get(obj[category]) if cid is None: # 未登记类别默认跳过并报警 logging.warning(funknown category: {obj[category]}) continue yolo convert_bbox_to_yolo(obj[bbox], img_w, img_h) if yolo is None: continue x_center, y_center, w, h yolo if filter_out_of_bounds: if x_center 0 or x_center 1 or y_center 0 or y_center 1: continue lines.append(f{cid} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) Path(out_label_path).write_text(\n.join(lines), encodingutf-8) def convert_dataset(json_dir, image_dir, label_out_dir, category2id): json_dir Path(json_dir) image_dir Path(image_dir) label_out_dir Path(label_out_dir) label_out_dir.mkdir(parentsTrue, exist_okTrue) for json_path in sorted(json_dir.glob(*.json)): img_w, img_h, parsed parse_annotation(json_path) label_path label_out_dir / f{json_path.stem}.txt write_yolo_label(parsed, img_w, img_h, category2id, label_path)这个脚本有几个细节值得注意。parse_annotation里的polygon退路很关键因为 RETT100K 部分图像只有旋转框标注不给你现成的水平框bbox字段可能是空的。write_yolo_label里对未登记类别的处理是“跳过并报警”而不是直接崩溃因为转换是大批量操作遇到一个未知类别就让整个程序停下来排查成本太高。我会在跑完之后统一看日志再决定是否补充类别表。3.3 旋转框怎么处理是转水平框还是走 OBB 模式遥感数据里旋转目标占比很大这可能是整个转换过程里最需要想清楚的一个决策点。完全照搬原始旋转框直接算外接矩形的水平框会让框内混入大量背景训练时正样本质量下降尤其是密集停靠的船只、沿道路排列的车辆这类目标外接矩形之间会大面积重叠导致收敛困难。RETT100K 的多边形标注整体上还是以物体真实轮廓为准所以你有两个选择一是把多边形转换成外接矩形再用 YOLO 的水平框模式训练二是直接用 YOLOv8 这类框架的 OBB 旋转框检测模式把标注转换成class_id cx cy w h angle的格式。我自己第一版用的是水平框方案因为通用性最好模型推理也方便但在做船只检测时发现密集场景误检明显增多后来切到 OBB 模式精度才提上去。这里提醒一句OBB 模式的angle定义在不同框架里不一样有的用弧度有的用角度有的以图像水平轴为参考有的以框自身长轴为参考转换时必须先搞清楚目标框架的内部定义否则角度一错旋转框就变成随机框。先挑选一个角度运算库抵扣框的长边方向来定义角度再对标签做一次可视化验证角度不对的话图上一眼就能看出来。4. 目录划分、data.yaml 与第一轮可视化验证4.1 用统一骨架组织 train、valid、test 三个目录标注转换完成之后下一步是数据划分。YOLO 训练框架加载数据时普遍期望目录结构是images/train、images/val、images/test、labels/train、labels/val、labels/test这种镜像结构。图像和标注如果不在对应目录里训练时就会报路径找不到的错误这类问题排查起来特别无聊却极其常见。我按 8:1:1 的比例做了划分。注意这里要按“图像文件”划分而不是按“标注文件”划分因为存在某些图像只有空标注文件的情况。划分顺序也要固定随机种子保证每次跑出的训练集、验证集完全一致否则你会发现在调参过程中“验证集变了”模型性能忽上忽下根本不知道是参数起作用还是数据变了。import random from pathlib import Path random.seed(42) root Path(datasets/RETT100K-yolo) image_src root / images_all label_src root / labels_all all_images sorted(image_src.glob(*.jpg)) sorted(image_src.glob(*.png)) random.shuffle(all_images) n len(all_images) train_cut int(n * 0.8) val_cut int(n * 0.9) splits { train: all_images[:train_cut], val: all_images[train_cut:val_cut], test: all_images[val_cut:], } for split_name, images in splits.items(): (root / images / split_name).mkdir(parentsTrue, exist_okTrue) (root / labels / split_name).mkdir(parentsTrue, exist_okTrue) for img in images: img.rename(root / images / split_name / img.name) lbl label_src / f{img.stem}.txt if lbl.exists(): lbl.rename(root / labels / split_name / lbl.name)这个脚本里我用了rename而不是copy因为原始文件已经转换完毕留着只会乱。如果你还保留着原始 RETT100K 标注建议只做复制而别动原目录方便随时回滚参照。4.2 把类别表焊进 data.yamlYOLO 训练时读取的data.yaml是最容易写错、也最容易忽略的一个文件。它通常长这样path: datasets/RETT100K-yolo train: images/train val: images/val test: images/test nc: 8 names: 0: airplane 1: ship 2: vehicle 3: harbor 4: storage_tank 5: bridge 6: tennis_court 7: baseball_field写这个文件时有一个原则names的编号顺序必须与你转换标注时用的category2id完全一致。否则你训练时看到 loss 在正常下降但可视化检测结果时明明框住了船只标签却显示成车辆而且你还不容易察觉——因为指标计算是按编号匹配的不对应就直接拉低分数。我强烈建议把生成classes.txt和生成data.yaml做成同一个入口用一个映射字典来驱动而不是分别手工维护两份文本。项目一旦超过 10 个类别手工维护早晚出差错。4.3 可视化验证宁可慢一点也不要瞎跑训练很多人转换完标注看都不看一眼就直接开训。我劝你至少在可视化验证上花半小时因为数据错误在训练早期往往被优化器的稳定性掩盖到了评估阶段才集中爆发。我的验证方法是把图像和对应标注文件读进来逐个框画在原图上输出成一组缩略图快速翻看几页。这里要格外注意两类异常一是类别和框明显不匹配的比如船只的框套在一栋楼上二是框的尺度明显不对比如全是大到出界的框、或者小到看不见的框。这类问题在纯数值检查里很难暴露人眼一扫却非常明显。我用的验证脚本大致是这样import cv2 import numpy as np from pathlib import Path label_dir Path(datasets/RETT100K-yolo/labels/train) image_dir Path(datasets/RETT100K-yolo/images/train) for label_path in sorted(label_dir.glob(*.txt))[:20]: img_path image_dir / f{label_path.stem}.jpg if not img_path.exists(): img_path image_dir / f{label_path.stem}.png img cv2.imread(str(img_path)) h, w img.shape[:2] for line in label_path.read_text().strip().splitlines(): cid, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) color (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, str(int(cid)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) cv2.imwrite(fvis/{label_path.stem}.jpg, img)如果你发现某张图里框的位置全部偏移优先检查你是不是在划分目录时把图像和标注文件名的对应关系弄错位了如果只是个别框不对优先检查那个框对应的原始 JSON 是不是有特殊字段。5. 训练阶段遇到的坑与修复经验5.1 类别编号错位一次隐蔽的“张冠李戴”第一次我用转换完的数据集训练 RETT100K训练集 loss 降到 0.03 左右验证集 mAP 却只有 0.4 出头怎么看怎么不对劲。打开验证集的可视化之后我发现模型框的位置其实很准但类别标签几乎全错了。比如明明是一个飞机场模型输出却是船舶而这两个目标的框位置都正确——这说明模型本身没毛病是标注编号和data.yaml的 names 列表不一致。问题根源出在我第一版脚本里用了category2id {name: idx for idx, name in enumerate(names)}而这个names是从一个classes.txt读进来的列的顺序和 RETT100K 原始顺序不一样。转换时我用了新顺序训练时data.yaml里却还保留着和官方文档一致的旧顺序两边各跑各的直到可视化才发现。从那之后我把classes.txt放进了datasets/RETT100K-yolo/目录所有脚本统一从同一个文件读取并加了一个启动检查如果某个类别编号和名称对不上直接抛异常拒绝训练。5.2 训练时发现大量空标注文件导致的警告风暴转换完标注以后我用训练脚本一跑日志里疯狂刷警告大意是“found no labels in xxx.jpg”。查了之后发现RETT100K 有一部分图像本来就是纯背景图官方标注就是空 JSON转换之后生成的是0字节的 txt 文件。YOLO 训练管线的数据加载器并不讨厌空标注文件而是讨厌海报上根本没标出来、却被扔进了训练集的空图像。我处理的办法是在划分数据之前先统计每个 txt 文件的行数把“行数为 0”的图像单独抽出来。如果这类图占比不大直接丢弃如果占比很大可能是解析器漏读了某个字段我不会急着删数据而是先从没解析出来的图像里抽两张人工检查 JSON 结构。这次排查就发现RETT100K 里存在少量缺少bbox但多边形字段也不是标准八点格式的目标我只能针对这种结构补了一个几何处理分支把不规则多边形拆成矩形近似后才继续跑。5.3 微调策略与缩放策略遥感大数据集怎么训才稳格式转完了训练策略上也有几个需要注意的地方。RETT100K 本身规模大、类别分布又很不均衡像飞机、船这种类别样本多而港口、桥梁这类类别相对少。直接用默认的超参数从头训很多类别会学习得很慢。我的做法是先用预训练权重做迁移学习把imgsz设成 1280这样小目标能保住更多像素然后把训练轮数设得相对保守前面 30 轮用学习率预热把分布拉平中间再用余弦退火逐步收敛。还需要留意的是遥感图像直接 resize 到固定尺寸会严重扭曲长宽比。RETT100K 里有相当一部分图像是长条形的航拍图直接拉伸会让框的位置在视觉上正确、但在归一化坐标上却偏离实际语义。如果你的 YOLO 框架支持矩形训练策略一定要打开它在批次内按长宽比分组填充能减少大量拉伸畸变。我在转换阶段就已经把长宽比记在了一个 CSV 里训练前按这个 CSV 做了分组采样效果比到处调 anchor 有用得多。5.4 最后帮你省时间的几个排查指令排到这里你其实已经可以自己处理大部分格式转换问题了。不过我还是把最终跑通整个流程时最常用到的几条命令和指令整理一下方便你直接复用# 统计所有标注文件的框数量排查空文件 find datasets/RETT100K-yolo/labels -name *.txt -type f | xargs wc -l | sort -k1 -n | head -30 # 检查是否有图像缺失对应标注 for f in datasets/RETT100K-yolo/images/train/*.jpg; do base$(basename $f .jpg) [ -f datasets/RETT100K-yolo/labels/train/$base.txt ] || echo missing $base done # 跑一轮短训练快速检验格式是否干净 yolo detect train datadatasets/RETT100K-yolo/data.yaml modelyolov8s.pt epochs3 imgsz1280 batch8这三条命令能在五分钟内暴露 80% 的格式问题。第一条看空标注第二条看路径对应关系第三条跑短训练验证能不能正常进入迭代。你只要把这套流程记录下来之后再拿到任何新数据集基本都能沿用同一套方法论只是解析函数和类别表需要换一换。转换格式本身不产生模型精度但它决定你的训练是否顺畅这个环节做到位后面的迭代才有意义。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询