简介:面向YOLO目标检测学习者与遥感图像分析人员,这份遥感油罐检测数据集来自真实场景,图片质量高、场景丰富,使用LabelImg标注且框体质量高,可直接用于YOLOv5、YOLOv8等主流目标检测模型的训练与算法效果验证。压缩包共2000个文件、约224.22MB,核心包含1000个VOC格式xml标签和990个YOLO格式txt标签,另有3个Python划分脚本、1个yaml配置文件和6个HTML环境搭建/训练教程,标签与脚本分类存放,便于按需调用。已有247人学习,附赠教程覆盖Linux与Windows双系统,从环境配置到自定义训练全程演示,配合划分脚本可灵活生成训练集、验证集与测试集,并支持自定义划分比例。资源还提供数据集详情展示与更多资源获取渠道,适合需要优质遥感数据集进行项目实践或毕业设计的开发者直接上手使用,省去数据整理与配置摸索时间。
1. YOLO遥感油罐检测数据集:1000张图和三套标签,先搞懂资源再动手
做遥感目标检测的同行应该都有体会:公开数据集里飞机、船舶、车辆满天飞,唯独油罐这种圆形工业目标,又小又密还带阴影,能找到的真实场景数据少得可怜。这份YOLO遥感油罐检测数据集一共1000张真实遥感图片,用labelimg人工标注,同时给出VOC的xml、COCO的json、YOLO的txt三种格式标签,外加三个数据集划分脚本和Linux/Windows双平台的YOLO环境搭建与训练教程。它不是一个只能喂给模型的原始图片堆,而是一条从标注格式转换到训练集划分再到跑通训练流程的完整链路。适合三类人:刚入门目标检测想找个真实数据练手的新手、需要在油罐场景上做预训练或迁移学习的从业者、以及想搞懂VOC/COCO/YOLO三种标签怎么互转的人。下面按我实际拆包和跑通的顺序,把这套资源的价值点和坑一次说清楚。
2. 三种标签格式打通:VOC、COCO、YOLO各自的坐标基准和转换逻辑
2.1 资源目录结构:先看清包里到底有什么
解压后第一件事,别急着开训练教程,先把目录结构浏览一遍。这套资源里图片和标签按格式分文件夹存放,常见的组织方式是这样:
dataset/ ├── images/ # 1000张遥感油罐图片,jpg或png ├── annotations/ │ ├── xml/ # VOC格式标签 │ ├── json/ # COCO格式标签 │ └── txt/ # YOLO格式标签 ├── 训练集、验证集、测试集划分脚本(图片标签划分写入新文件夹).py ├── 训练集、验证集划分脚本(图片标签划分写入新文件夹).py ├── split_train_val生成ImageSets下txt文件划分脚本.py └── train_list.txt为什么油罐检测场景强烈推荐直接下载这种多格式标签的资源?因为自己做标注实在太痛。油罐在遥感影像里是典型的圆形目标,边缘有弧形阴影,labelimg打框时经常要把阴影包含进去,一个图几十个罐子标注下来胳膊都是酸的。这套数据已经把标注做完,你只需要选择一种格式接入训练框架。
三个划分脚本覆盖了三种使用场景:同时切出训练、验证、测试三组,只切训练和验证两组,以及生成darknet风格ImageSets下的txt索引文件。我在实际使用中发现,第三类脚本和第一类脚本容易混淆,后者是把图片和标签物理复制到新文件夹,前者只是生成一个包含图片路径的txt清单。后面我会给出一份改良脚本,一次搞定两种需求。
2.2 VOC的xml结构:理解绝对像素坐标
三种标签格式里,VOC是原始标注格式,因为labelimg保存默认就是xml。它的核心是每个目标对应一个bndbox节点,四个值全部是像素绝对坐标:
<annotation> <folder>images</folder> <filename>oil_tank_001.jpg</filename> <size> <width>1024</width> <height>1024</height> <depth>3</depth> </size> <object> <name>oil_tank</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>132</xmin> <ymin>480</ymin> <xmax>189</xmax> <ymax>537</ymax> </bndbox> </object> </annotation>需要注意,这里xmin/ymin/xmax/ymax是相对于原始图片左上角的绝对像素值,不做任何归一化。图片宽高来自<size>节点,这个信息在后面对坐标做归一化或越界检查时非常关键。
另外一个容易忽略的地方是<name>标签的值。很多转换脚本按类别名的字母顺序排列生成类别id,如果你只有一个oil_tank类,顺序无所谓;只要后续自己添加新类别(比如从油罐扩展到炼油厂建筑),就必须固定一个类别映射表,否则训练中断后恢复时类别对不上。
2.3 COCO和YOLO的坐标系:两个完全不同的存储逻辑
COCO格式的json里,标注框的存储方式是bbox: [x, y, width, height],其中x、y是框左上角坐标,单位是像素,w和h是宽高。YOLO格式的txt每一行对应一个目标:
0 0.156738 0.496582 0.055664 0.055664五个数字依次是:类别id、归一化中心点x、归一化中心点y、归一化宽度、归一化高度。注意这里所有值都除以图片宽高做了归一化,范围在0到1之间。
从VOC转YOLO是训练前最常用的操作,转换公式是:
import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_path, classes): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text.strip() if name not in classes: continue class_id = classes.index(name) box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) center_x = (xmin + xmax) / 2.0 / img_w center_y = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h # 裁剪越界坐标,预防后续训练报错 center_x = min(max(center_x, 0.0), 1.0) center_y = min(max(center_y, 0.0), 1.0) box_w = min(max(box_w, 0.0), 1.0) box_h = min(max(box_h, 0.0), 1.0) lines.append(f"{class_id} {center_x:.6f} {center_y:.6f} {box_w:.6f} {box_h:.6f}") with open(out_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) # 类别必须和训练配置保持一致,这里用资源自带的类别名 classes = ['oil_tank'] voc_to_yolo('annotations/xml/oil_tank_001.xml', 'annotations/txt/oil_tank_001.txt', classes)这段代码里做了两件易踩坑的事:一是坐标转换时把绝对像素换算成相对于图片宽高的比例,二是对越界值做了裁剪。遥感图经常出现标注框稍微超出图像边缘的情况,裁剪后YOLO训练不会因为负数宽高报错。另一种情况是xml里类别名带空格或者大小写不一致,所以name.strip()是必要的。转到COCO的思路类似,但json要维护images、annotations、categories三个顶层数组,代码量比转txt大得多。从转换耗时角度看,COCO转YOLO比VOC转YOLO容易出问题的地方在于json里的bbox顺序是xywh,直接拆包时千万别按xyxy去用。
3. 划分脚本改造:从固定比例到可复用的训练集、验证集、测试集生成流程
3.1 资源自带脚本的定位与局限
包里三个脚本的定位不同,这里拆开说。第一个“训练集、验证集、测试集划分脚本(图片标签划分写入新文件夹).py”是完整的三划分,比例通常是8:1:1或者7:2:1,它会创建新文件夹并把图片和对应标签一并复制进去;第二个脚本只切训练集和验证集;第三个脚本生成ImageSets下的train.txt和val.txt,这种txt是darknet框架训练时读取图片路径列表用的,和train_list.txt对照着看就明白了。它们的问题也明显:脚本里路径写死,直接运行大概率因为相对路径对不上而报FileNotFoundError;如果按格式分文件夹存放(xml和txt在不同目录),划分时只复制了图片没复制标签,结果训练时找不到txt。我的习惯是拿到这类脚本第一件事就是删掉硬编码路径,改成从命令行参数读入。
3.2 改良版划分脚本:一次跑通三格式训练数据
我重构了一版实用的划分脚本,支持将图片和标签同步划分、自动生成darknet和ultralytics两种索引文件:
import os import random import shutil from pathlib import Path def split_dataset(img_dir, label_dir, output_dir, train_ratio=0.8, val_ratio=0.1, seed=42): """将图片和标签按比例划分为 train/val/test img_dir: 图片文件夹路径 label_dir: 标签文件夹路径(YOLO txt同名前缀) output_dir: 输出根目录,会在其下创建 images/train、images/val、images/test 以及 labels/train、labels/val、labels/test """ img_paths = sorted(Path(img_dir).glob('*.jpg')) + sorted(Path(img_dir).glob('*.png')) if not img_paths: raise FileNotFoundError(f'{img_dir} 下没有找到 jpg 或 png 图片') random.seed(seed) # 固定随机种子,保证结果可复现 random.shuffle(img_paths) train_end = int(len(img_paths) * train_ratio) val_end = train_end + int(len(img_paths) * val_ratio) splits = { 'train': img_paths[:train_end], 'val': img_paths[train_end:val_end], 'test': img_paths[val_end:], } output_root = Path(output_dir) for split_name, paths in splits.items(): img_out = output_root / 'images' / split_name label_out = output_root / 'labels' / split_name img_out.mkdir(parents=True, exist_ok=True) label_out.mkdir(parents=True, exist_ok=True) for img_path in paths: shutil.copy2(img_path, img_out / img_path.name) label_path = Path(label_dir) / (img_path.stem + '.txt') if label_path.exists(): shutil.copy2(label_path, label_out / label_path.name) all_images = [str(p) for p in img_paths] write_index_file(output_root / 'train.txt', all_images[:train_end]) write_index_file(output_root / 'val.txt', all_images[train_end:val_end]) write_index_file(output_root / 'test.txt', all_images[val_end:]) print('划分完成,图片总数:', len(img_paths)) def write_index_file(out_path, img_paths): with open(out_path, 'w', encoding='utf-8') as f: for p in img_paths: f.write(str(p) + '\n') if __name__ == '__main__': split_dataset( img_dir='dataset/images', label_dir='dataset/annotations/txt', output_dir='dataset/split', train_ratio=0.8, val_ratio=0.1, seed=42, )核心逻辑是先把图片路径列表随机打乱,按比例切三段,然后把图片复制到目标文件夹,同名标签文件跟着复制进去。random.seed(seed)不是可选项,不固定种子每次划分结果不同,实验对比时没法复现。比例参数按需调,数据量少的时候可以设8:1:1,数据上万张时7:2:1更稳。
这个脚本解决了一个实际问题:很多教程默认你只有图片和标签同一目录下的txt,但这套资源是xml、json、txt分开存放,如果你用COCO格式做训练,还要额外写一个json解析转换。我的建议是直接用YOLO格式的txt作为划分输入,因为ultralytics YOLO和darknet都认这种结构。脚本生成的train.txt直接喂给darknet,或者把这几个txt文件放入ultralytics数据集的ImageSets/Main目录里也能被识别。write_index_file里写的是图片文件的绝对路径,换机器后记得重新生成一次,否则路径对应不上。
3.3 从划分到训练:train_list.txt和dataset.yaml的衔接
train_list.txt本质上就是图片路径的清单,darknet训练时通过训练命令指向它。如果你用ultralytics YOLOv8,不读txt,改读dataset.yaml:
# dataset.yaml path: /your/path/dataset/split train: images/train val: images/val test: images/test nc: 1 names: 0: oil_tank把path改成你本机实际路径后,放在项目根目录,训练时指定data=dataset.yaml即可。这里最容易翻车的是path写成相对路径,而ultralytics内部会从当前工作目录解析,路径对不上直接报错。另外nc必须和names列表长度一致,不一致时训练不会报错但验证指标全是0。我第一次跑油罐数据集训练时,就是因为在yaml里多写了一个类别名导致类别总数错乱,损失函数表现正常但mAP怎么都不涨,后来逐个排查才发现是类别映射错位。
4. 避坑排查:油罐数据集训练翻车的四个真实案例
4.1 标签编码报错:UnicodeDecodeError乱码
用脚本批量转换标签时,控制台突然抛出UnicodeDecodeError: 'gbk' codec can't decode byte。这是因为标签文件里含有非GBK编码字符,多数labelimg在Linux下保存的xml是UTF-8,Windows下某些版本会混入本地编码。解决方式是读取时显式指定编码:
with open(xml_path, 'r', encoding='utf-8') as f: content = f.read()如果还报错,说明文件本身不是UTF-8,用errors='ignore'兜底。转换和训练脚本里统一字符编码是血泪经验,我在处理遥感标注文件时经常遇到文件名带中文或空格的情况,读取标签时忽略这些隐藏字符能省下大量排查时间。
4.2 图片尺寸不统一和EXIF旋转导致的框偏移
遥感图源来自不同平台时,尺寸和方向各不相同。同一批数据里,一张是512x512,另一张是4000x3000,训练时自动缩放没问题,问题出在坐标系。如果xml里记录的尺寸和实际图片解码后的宽高不一致(比如EXIF信息导致PIL读取后自动旋转),转换出的YOLO坐标就全部错位,训练loss可以降低但检测框全部偏移。
from PIL import Image img = Image.open('dataset/images/oil_tank_001.jpg') print(img.size, img.format)跑一遍上面的校验,确认img.size和xml里<size>节点一致。如果有旋转标记,建议预处理阶段统一转成RGB并去掉EXIF方向信息,再重新生成标签。遥感影像通常没有EXIF问题,但截图、扫描件经常有,这点比普通自然图像数据集更容易忽略。
4.3 坐标越界:txt里出现负数或大于1的值
这是油罐数据集最常见的问题。遥感大图经过切片后,目标正好位于切片边缘,标注框超出图像边界,转换后出现负坐标或大于1的坐标。YOLO训练时OpenCV读取标注文件不会报错,但会跳过这些框或者计算出负数宽高,导致损失函数出现NaN。处理方式有两个:一个是在转换时做clamp裁剪,把负值归零、超过1的压回1;另一个是直接丢弃标签中面积过小的框。如果目标被切得只剩5%面积,这种标注本来就失去了训练意义。我在分割超大遥感图时会使用带重叠的滑窗裁剪方案,确保边缘目标至少保留70%面积在某个切片内,而不是简单裁一刀把油罐切成两半。
4.4 类别号错位:COCO json和YOLO txt的类别映射不一致
资源里COCO json的categories数组顺序未必和YOLO txt的类别编号一致。很多人直接看json里各类别的顺序,然后用默认的classes.index(name)做转换,一旦json里类别不是按字母序排列,出来的txt就错位。比如json里oil_tank排在storage_tank后面,但脚本按字母序转出,油罐全部变成了类别1。解决方式很直接,训练前先手动检查一个xml、json、txt对应文件,确认类别映射:
with open('dataset/annotations/json/oil_tank_001.json', 'r', encoding='utf-8') as f: coco_data = json.load(f) print(coco_data['categories'])类别少的项目一眼就能看出问题,类别多的时候建议打印一个训练批次的可视化结果做验证。我刚跑遥感数据集时经常忽略这一步,直到用torchvision.utils.save_image把标注框画出来才发现全部偏移。
5. 把数据集变成可复用的训练管道:先跑通再调优
拿到这套油罐数据后,我建议先别急着追求mAP,而是用一条最短路把流程跑通:检查标签完整性、划分数据集、启动训练、验证一次、输出结果。这套流程一旦固化,以后换任何自定义数据集都能复用。我平时训练遥感油罐检测的标准操作是先用YOLOv8n做快速验证,因为环境配置最简单,单卡训练速度快,一轮迭代就能看出数据有没有问题。
# 安装ultralytics(CPU也能跑,但速度慢很多) pip install ultralytics # 训练,预训练权重用yolov8n.pt,显存不够就改batch为4 yolo detect train data=dataset.yaml model=yolov8n.pt epochs=100 batch=8 imgsz=640参数说明:imgsz=640是输入尺寸,油罐目标小,如果原图是1024或更大,可以考虑imgsz=1024提高小目标召回率,但显存占用会明显上涨,一般先用640确认数据没问题。batch=8是显存不够时的折中,能开到16就开16。epochs=100对1000张图略多,但迁移学习下一般到50轮就收敛了。跑完后看runs/detect/train/val_batch0_pred.jpg这张预测可视化图,如果检测框基本贴合油罐边缘,说明数据管道通了;如果框全偏往图片角落,回去查标签转换脚本。
训练完成后,部署阶段常遇到误检率高的问题,遥感场景背景复杂,云层、建筑阴影、球状储罐都会产生干扰。这里的常规做法是调整置信度门限,默认0.25在油田场景往往偏高,调低到0.1能找回不少被过滤的油罐,但误检也会增多:
yolo detect predict model=runs/detect/train/weights/best.pt source=test.jpg conf=0.1conf参数就是置信度门限,低于这个分数的框会被丢弃。油罐检测实践中,我一般先按0.15跑一遍预测,数一下漏检和误检的数量再决定往高调还是往低调。如果想可视化查看各类目标在精度和召回率之间的平衡关系,跑完训练后生成的PR曲线图可以直接参考,F1分数最高的点对应的置信度就是该场景的最优门限。
这套资源里包含的Linux环境搭建教程和Windows训练教程覆盖了大部分人的部署需求。我的做法是先在Windows上跑通一个epoch验证数据没问题,然后丢到Linux服务器上做正式训练。跨平台时注意路径分隔符和编码这两个变量问题。从那以后我每次拿到新数据集,都强制走一遍“标签校验、格式转换、划分、可视化验证、训练”这个流程,五个环节全绿了才敢上高epoch数训练。希望帮到你。
本文还有配套的精品资源,点击获取