ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSDD遥感舰船检测数据集:VOC标注转YOLO格式的完整实践指南

SSDD遥感舰船检测数据集:VOC标注转YOLO格式的完整实践指南 简介面向YOLO目标检测训练的SSDD遥感检测数据集已打包为rar直接可用特别适合计算机、电子信息、数学等专业学生用于课程设计、期末大作业或毕业设计中的目标检测模型训练与验证。压缩包共2000个文件其中JPG遥感图像1160张辅以XML标注文件主要文件类型为图像与标注覆盖典型遥感目标检测场景可省去自行收集与标注数据的繁琐流程。资源大小约48.12MB轻量紧凑便于快速下载和本地实验参数化编程思路和清晰的注释说明也让使用者能方便调整配置、理解数据组织方式适合后续二次开发与算法改进。已有2787人浏览学习社区认可度较高尤其适合需要高质量遥感数据支撑的初学者或毕业生。借助该数据集可以围绕YOLO系列模型开展训练、验证与对比实验为遥感目标识别方向的课程项目、竞赛或论文研究提供可靠数据支撑。1. 遥感舰船检测数据集到手先别急着跑YOLO从网盘或某个技术群里下载到SSDDSAR Ship Detection Dataset压缩包的人第一反应基本都是解压、改路径、直接开始训练YOLO目标检测模型。1160张图像全部带XML标注压缩包描述也写着已标注可以直接使用但直接拿它喂给YOLO的后果多半是训练日志里loss一路下降验证集的mAP却纹丝不动。SSDD是遥感图像目标检测里绕不开的舰船检测基准数据集它的标注沿用了PASCAL VOC的XML格式而YOLO训练默认读的是每张图一个TXT的归一化坐标文件中间隔着转换、校验、数据集划分三道工序。这篇笔记把这个流程完整走一遍适合手里拿到标注数据但不知道怎么喂给YOLO的人也适合想用SSDD做遥感小目标检测入门、却不想在数据准备阶段反复翻车的从业者。2. 拆开SSDD数据集标注格式与质量核查2.1 XML标注里到底记录了哪些信息SSDD的标注格式沿用了PASCAL VOC每张图像对应一个同名的XML文件。挑一个典型的标注文件去掉头部公共字段后核心结构长这样annotation folderSSDD/folder filename000001.jpg/filename size width796/width height638/height depth3/depth /size object nameship/name bndbox xmin187/xmin ymin204/ymin xmax341/xmax ymax249/ymax /bndbox /object /annotation其中size节点记录图像的宽、高、通道数bndbox里是目标的左上角和右下角像素坐标name是类别名。SSDD常见版本里类别基本就是ship单类但网上流传的副本里也出现过Ship、ship-ship、vessel这样的变体后面转换时会处理。另外有些XML里还带difficult字段标记模糊或极小的目标这个字段在转换为YOLO格式时会被丢弃所以训练时那些难例框其实是被忽略的这会直接拉低召回。查看和编辑XML别用浏览器浏览器只展示树形结构没法快速定位哪条记录缺了字段。我一般用VS Code打开配上XML格式化插件一眼就能扫出异常。用IDEA社区版的同行注意IDEA默认会对XML做自动格式化改完记得另存别直接覆盖原标注文件——格式变化不影响解析但误改动bndbox里的值会影响训练精度。2.2 一条Python脚本统计图像数、标注框数与类别分布拿到数据集的第一件事不是转换而是统计。我一般用ElementTree把整个标注目录扫一遍确认图像数、标注框数和类别分布import os import xml.etree.ElementTree as ET xml_dir annotations total_images 0 total_boxes 0 class_counter {} for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue total_images 1 tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() for obj in root.findall(object): name obj.find(name).text class_counter[name] class_counter.get(name, 0) 1 total_boxes 1 print(f图像数: {total_images}) print(f标注框数: {total_boxes}) print(f类别分布: {class_counter})这段代码只读不改输出三类信息。图像数用来和压缩包描述里的1160对账数量对不上说明文件传输过程中丢了文件标注框数用来排除空标注的XML如果某张图一个object都没有检查是不是漏标类别分布用来发现类别名变体如果打印出来除了ship还有Ship后面的转换脚本就要做大小写归一化。参数只需要改xml_dir一个指向解压后的标注目录。如果输出里的图像数为0先确认路径对不对再看文件名后缀是不是大写.XML——os.listdir返回的文件名是大小写敏感的.XML不会匹配.endswith(.xml)。2.3 图像与标注文件配对检查文件名不一致是第一个坑SSDD传播过程中出现过多个版本有的一刀切改了文件名有的把目录嵌套层级改了。图像文件在images目录下叫000001.jpg标注在annotations目录下叫000001.xml这是最常见的情况但也见过标注文件名是000001_ship.xml这种带后缀的版本。文件名对不上YOLO训练时会按标签文件名去找同名图像找不到直接报image not found。写个配对检查脚本用集合做差集快速定位import os img_dir images xml_dir annotations img_stems {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith((.jpg, .png, .jpeg, .bmp))} xml_stems {os.path.splitext(f)[0] for f in os.listdir(xml_dir) if f.endswith(.xml)} print(有图无标注:, len(img_stems - xml_stems)) print(有标注无图:, len(xml_stems - img_stems)) print(完全配对:, len(img_stems xml_stems))这里用后缀匹配列出了四种常见图像格式因为网上流传的SSDD版本里jpg和png都有甚至有转成bmp的。如果有图无标注不为0优先检查是不是图像格式不在列表里如果有标注无图不为0多半是标注文件被重命名过。配对数量等于1160才是健康的开头。3. VOC格式XML转YOLO训练格式TXT转换脚本与四个边界坑3.1 转换脚本读取XML并输出归一化YOLO坐标YOLO训练需要的数据集处理方式是把每张图像的标注写进同名TXT每行一个目标格式是类别ID x_center y_center width height四个坐标值全部归一化到0到1之间。SSDD原本的XML标注没法直接用转换这一步绕不开。下面这个脚本是我在多个数据集上反复用过的版本直接放到SSDD解压目录的上级运行import os import xml.etree.ElementTree as ET from PIL import Image CLASS_MAP {ship: 0} # 类别名统一小写后映射到ID def convert_one(xml_path, label_path, img_width, img_height): tree ET.parse(xml_path) root tree.getroot() size root.find(size) if size is not None: img_w int(size.find(width).text) img_h int(size.find(height).text) else: img_w, img_h img_width, img_height lines [] for obj in root.findall(object): name obj.find(name).text.strip().lower() if name not in CLASS_MAP: print(f跳过未知类别 {name} 在 {xml_path}) continue cls_id CLASS_MAP[name] bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 坐标越界直接裁剪面积过小直接丢弃 xmin max(0.0, min(xmin, img_w)) xmax max(0.0, min(xmax, img_w)) ymin max(0.0, min(ymin, img_h)) ymax max(0.0, min(ymax, img_h)) if xmax - xmin 2 or ymax - ymin 2: continue x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(label_path, w, encodingutf-8) as f: f.write(\n.join(lines)) def convert_all(xml_dir, label_dir, img_dir): os.makedirs(label_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] xml_path os.path.join(xml_dir, xml_file) label_path os.path.join(label_dir, stem .txt) img_width, img_height 0, 0 for suffix in [.jpg, .png, .jpeg]: img_candidate os.path.join(img_dir, stem suffix) if os.path.exists(img_candidate): with Image.open(img_candidate) as img: img_width, img_height img.size break convert_one(xml_path, label_path, img_width, img_height) convert_all(annotations, labels, images)逻辑说明convert_one负责解析单个XML并写TXTconvert_all遍历整个标注目录。关键点在三个地方。第一类别名先strip再lower解决2.2里发现的类名大小写变体第二坐标归一化前先做clip防止xmax比图像宽度还大第三面积小于2像素的框直接丢弃这种框在YOLO训练里只会贡献噪声甚至让loss变成NaN。参数说明CLASS_MAP是类别名到ID的映射SSDD单类场景就是{ship: 0}如果统计脚本发现有其他类别名在这里补上xml_dir、label_dir、img_dir分别是标注目录、TXT输出目录、图像目录按实际路径改。img_width和img_height是兜底参数仅在XML缺失size节点时使用优先从PIL读取图像真实尺寸避免以XML里可能错误的宽高做归一化。3.2 坐标归一化的边界条件越界框与零面积框转换过程中最常见的两个边界问题一个是坐标越界一个是零面积框。越界框的典型场景是xmax写成了801而图像宽只有796。这种数据多半是标注工具自动补边或者人工标注手滑产生的。如果不处理归一化后box_w是1.006超过1的宽度出现在YOLO标签里虽然不一定会报错但训练时匹配锚框的IoU计算会乱掉检测框整体偏大。3.1脚本里的clip逻辑就是干这个的。零面积框更隐蔽xmin和xmax相等或者ymin和ymax相等这种框在YOLOv8的DFL损失里会引起梯度异常轻则某几个batch的loss跳变重则训练过程直接发散成NaN。过滤条件xmax - xmin 2和ymax - ymin 2实际上是留了2像素的余量把宽或高只有1像素的噪点框也一并清理了。这两个问题不是每个SSDD版本都有但网上二次打包的数据集很难保证干净。转换后再跑一遍统计脚本对比转换前后的框数如果框数少了很多说明数据里异常框比例高这时候要回头检查原始XML而不是直接开始训练。3.3 训练集/验证集划分保证图像不泄漏SSDD只有1160张图像训练集和验证集的划分直接影响实验结果的可比性。划分的第一原则是图像级划分同一张图像只能出现在训练集或验证集中不能既训练又验证。SSDD图像是SAR切片相邻切片之间场景可能高度相似如果划分不当验证集里混进训练集内容相近的图像mAP会虚高。我习惯用随机划分加固定种子import os import random import shutil random.seed(42) image_files [f for f in os.listdir(images) if f.endswith((.jpg, .png, .jpeg))] random.shuffle(image_files) val_count int(len(image_files) * 0.2) val_stems {os.path.splitext(f)[0] for f in image_files[:val_count]} for split in [train, val]: os.makedirs(fdataset/images/{split}, exist_okTrue) os.makedirs(fdataset/labels/{split}, exist_okTrue) for img in image_files: stem os.path.splitext(img)[0] img_src os.path.join(images, img) label_src os.path.join(labels, stem .txt) split val if stem in val_stems else train shutil.copy(img_src, fdataset/images/{split}/{img}) if os.path.exists(label_src): shutil.copy(label_src, fdataset/labels/{split}/{stem}.txt) else: print(f警告: 标签不存在 {label_src})参数说明random.seed(42)固定随机种子保证每次划分结果一致做对比实验时不同模型的训练集验证集完全相同val_count是验证集数量按20%算出来是232张剩下的进训练集。脚本会把图像和标签复制到YOLO标准目录结构下train和val两个子目录各自包含images和labels后续YOLOv5和YOLOv8都能直接认这个结构。划分时如果打印出标签不存在的警告说明3.1的转换遗漏了部分图像回头检查该图像对应的XML文件而不是继续往下走。4. YOLO训练配置三个必调参数与遥感小目标场景设置4.1 data.yaml与路径配置绝对路径还是相对路径数据集目录结构搭好之后先写data.yaml。这是YOLO训练入口文件指定数据路径、类别数和类别名path: /home/user/ssdd_dataset train: images/train val: images/val nc: 1 names: [ship]参数说明path是数据集根目录train和val是相对于path的路径。YOLOv8要求path写成绝对路径相对路径在多机训练或换机器时很容易踩坑我现在一律写绝对路径。nc是类别数SSDD单类就是1names列表里的名字要跟3.1里CLASS_MAP的键对应上这里的ship是小写的类名大小写不一致是后面mAP为0的高频原因。预训练权重这块YOLO官方release里提供了yolov8n.pt、yolov8s.pt这些权重文件训练SSDD这种规模的数据集n或s起步就够了。下载预训练权重只看官方release别从第三方网盘拿权重版本跟代码不匹配会导致load失败更麻烦的是静默加载了旧参数训练半天精度上不去还找不到原因。4.2 输入分辨率与锚框小目标场景下别再迷信640SSDD里的舰船目标在原始图像里往往只占几十乘几十甚至十几个像素属于典型的遥感小目标场景。输入分辨率直接决定小目标在特征图上的尺寸640分辨率下很多舰船只剩十几个特征点检测器分不清目标和噪点。常见做法是训练时把imgsz提高到1280显存不够就降到960或800但不要低于640。提高分辨率比增加epoch更有效这是我用SSDD和类似遥感数据集跑出来的实际感受。加了50个epochmAP可能只涨零点几个点把输入从640提到1280mAP0.5能涨三到五个点。代价是训练时间和显存占用翻倍一张12G显存的卡batch要减半。锚框参数要分框架讨论。YOLOv5默认开启autoanchor训练启动时会基于训练集标签重新做k-means聚类SSDD里舰船边界框长宽比普遍偏大聚类出来的锚框跟COCO默认值差异明显autoanchor这个功能千万别关。YOLOv8走的是anchor-free路线没有锚框超参数要调但DFL损失对边界框定位更敏感所以输入分辨率的影响更直接。说到损失函数训练日志里主要盯box_loss、cls_loss和dfl_loss三项。SSDD是单类场景cls_loss很快就能降到接近0这时候别觉得模型已经收敛了box_loss才是决定mAP0.5:0.95的关键。如果box_loss下降特别慢先怀疑标注框本身不准再考虑学习率是不是设大了。4.3 batch size与早停一张卡怎么设不翻车训练命令的典型写法yolo detect train datassdd.yaml modelyolov8s.pt epochs200 imgsz640 batch16 patience50 seed42参数说明epochs设200SSDD数据量小100轮基本能过拟合300轮属于浪费算力patience是早停轮数val_loss连续50轮不降就自动停不用人盯着seed跟3.3划分脚本里的种子保持一致保证实验可复现。batch size要结合显存和分辨率一起看。8G显存跑640分辨率batch设16没问题分辨率提到1280batch降到8甚至4。有些人的做法是强行保持大batch结果batch size超过显存直接OOM或者触发YOLO自动降低batch反而打乱了学习率的配合。我一般先把batch设成显存能承受的最大值再去调学习率。另外提一句不相关的坑好几个人问过我v100上训练SSDD为什么比预期慢看了半天发现是数据加载没开num_workersGPU在那干等CPU喂数据。训练命令里把workers设成4或8能明显改善单卡利用率。5. 常见问题避坑五条血泪经验5.1 训练loss正常但验证mAP为0类名大小写与编号对不上现象训练日志里loss稳定下降验证集mAP0.5永远打印0或者训练结束后所有检测框都预测成背景。原因XML里类别名是Ship转换时CLASS_MAP只写了ship导致该目标被跳过标签文件是空的另一种情况是data.yaml里names写了[Ship]而TXT里类别ID是0模型按索引找类别名对不上。解决转换前先跑2.2的统计脚本看真实类别字符串统一小写后映射转换后抽一个标签文件打开检查如果是空文件说明类别名没匹配上。修改CLASS_MAP或data.yaml保证两边完全一致。5.2 XML坐标越界导致loss出现NaN现象训练到中途某个batch的loss变成NaN之后所有指标全部失效。原因某个XML的bndbox坐标超过图像尺寸比如xmax801而width796归一化后边界框宽度大于1YOLOv8的DFL分支对这类输入会产生无效梯度。解决转换时做clip也就是3.1脚本里对四个坐标值先约束到图像范围内再归一化。已经生成的TXT里如果混入大于1的坐标写个小脚本重新clip一遍别偷懒重训。5.3 用PIL读出的图像尺寸和XML里size不一致框整体偏移现象验证集可视化时检测框整体往右下偏移框越大偏移越明显小目标反而看不太出来。原因数据集的图像在传播过程中被重新压缩或裁剪过但XML里的size没有同步更新。转换时如果以XML的size做归一化而YOLO加载图像后按实际尺寸letterbox两套尺寸不一致坐标自然偏移。解决以实际图像尺寸为准重新生成标注。做法是用PIL遍历所有图像读取真实宽高对比XML里记录的宽高不同的就替换XML的size节点再重新运行转换脚本。遇到过整个压缩包里三分之一图像的XML尺寸和实际不符的情况不对比根本发现不了。5.4 舰船小目标漏检多只看mAP0.5忽略mAP0.5:0.95现象验证集mAP0.5到0.9看着不错但把模型推到整幅遥感大图上一条船都找不出来。原因SSDD图像本身就是从大图中截取的切片目标在切片里占比没那么小所以mAP0.5虚高。实际遥感大图里舰船目标更小训练时用的640分辨率把目标直接缩没了。解决提升推理分辨率或者做切片推理常见思路是把大图切成重叠的patch分别检测再合并结果。训练阶段也可以把mosaic增强关掉最后10个epochYOLOv8的close_mosaic参数就是干这个的避免大图缩放到训练分辨率时小目标被裁掉。5.5 训练启动报Dataset not found目录结构被自定义搞坏了现象启动训练直接报数据集路径不存在但文件明明在那个位置。原因很多人保留压缩包内嵌的目录层级比如dataset/SSDD/images而data.yaml里写的是images/train。更常见的是把原来的annotations目录改名成labels但里面还是XML文件没有放转换后的TXT。解决训练前用tree命令看一眼目录结构确认images和labels两个目录各自包含train和val子目录TXT文件已经和图像同名。YOLO只认这个约定自己定义的目录结构再漂亮也没用。6. 让标注数据多一道保险把TXT框可视化回原图格式转换和数据划分都做完之后别急着训练先做一个可视化质检。把TXT里的归一化坐标还原回像素坐标画到原图上人眼扫一遍就能发现转换环节的系统性错误。import cv2 import os label_dir labels image_dir images output_dir debug_vis os.makedirs(output_dir, exist_okTrue) for txt_file in os.listdir(label_dir): if not txt_file.endswith(.txt): continue stem os.path.splitext(txt_file)[0] img_path None for suffix in [.jpg, .png, .jpeg]: if os.path.exists(os.path.join(image_dir, stem suffix)): img_path os.path.join(image_dir, stem suffix) break if img_path is None: continue img cv2.imread(img_path) h, w img.shape[:2] with open(os.path.join(label_dir, txt_file), r) as f: lines f.read().strip().splitlines() for line in lines: parts line.strip().split() if len(parts) ! 5: continue _, x_center, y_center, box_w, box_h map(float, parts) x1 int((x_center - box_w / 2) * w) y1 int((y_center - box_h / 2) * h) x2 int((x_center box_w / 2) * w) y2 int((y_center box_h / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(os.path.join(output_dir, stem .jpg), img)逻辑说明读TXT的四个归一化坐标乘以图像的宽高还原成像素坐标cv2.rectangle直接画框。检查方法很简单随机挑十张图看框和舰船的贴合程度。如果框大面积偏到背景上或者画出来的框明显宽高异常说明XML记录的尺寸和图像实际尺寸不一致回到5.3处理。我自己有过一次偷懒的教训拿到一个二手遥感数据集直接转换训练到第50轮发现验证集mAP比训练集低二十多个点排查到后面才发现是某个版本的图像被人重采样过XML里的size全是旧的。从那以后我拿到任何下载来的数据集第一件事永远是先做可视化质检不做可视化不训练。这个习惯帮我挡掉了不少数据上的暗坑希望帮到你。本文还有配套的精品资源点击获取
返回列表