news 2026/10/1 11:55:51

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSDD遥感舰船检测数据集:VOC标注转YOLO格式的完整实践指南

简介:面向YOLO目标检测训练的SSDD遥感检测数据集已打包为rar直接可用,特别适合计算机、电子信息、数学等专业学生用于课程设计、期末大作业或毕业设计中的目标检测模型训练与验证。压缩包共2000个文件,其中JPG遥感图像1160张,辅以XML标注文件,主要文件类型为图像与标注,覆盖典型遥感目标检测场景,可省去自行收集与标注数据的繁琐流程。资源大小约48.12MB,轻量紧凑,便于快速下载和本地实验;参数化编程思路和清晰的注释说明也让使用者能方便调整配置、理解数据组织方式,适合后续二次开发与算法改进。已有2787人浏览学习,社区认可度较高,尤其适合需要高质量遥感数据支撑的初学者或毕业生。借助该数据集,可以围绕YOLO系列模型开展训练、验证与对比实验,为遥感目标识别方向的课程项目、竞赛或论文研究提供可靠数据支撑。

1. 遥感舰船检测数据集到手,先别急着跑YOLO

从网盘或某个技术群里下载到SSDD(SAR 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> <folder>SSDD</folder> <filename>000001.jpg</filename> <size> <width>796</width> <height>638</height> <depth>3</depth> </size> <object> <name>ship</name> <bndbox> <xmin>187</xmin> <ymin>204</ymin> <xmax>341</xmax> <ymax>249</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", encoding="utf-8") as f: f.write("\n".join(lines)) def convert_all(xml_dir, label_dir, img_dir): os.makedirs(label_dir, exist_ok=True) 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并写TXT,convert_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(f"dataset/images/{split}", exist_ok=True) os.makedirs(f"dataset/labels/{split}", exist_ok=True) 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, f"dataset/images/{split}/{img}") if os.path.exists(label_src): shutil.copy(label_src, f"dataset/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单类就是1,names列表里的名字要跟3.1里CLASS_MAP的键对应上,这里的ship是小写的,类名大小写不一致是后面mAP为0的高频原因。

预训练权重这块,YOLO官方release里提供了yolov8n.pt、yolov8s.pt这些权重文件,训练SSDD这种规模的数据集,n或s起步就够了。下载预训练权重只看官方release,别从第三方网盘拿,权重版本跟代码不匹配会导致load失败,更麻烦的是静默加载了旧参数,训练半天精度上不去还找不到原因。

4.2 输入分辨率与锚框:小目标场景下别再迷信640

SSDD里的舰船目标在原始图像里往往只占几十乘几十甚至十几个像素,属于典型的遥感小目标场景。输入分辨率直接决定小目标在特征图上的尺寸,640分辨率下很多舰船只剩十几个特征点,检测器分不清目标和噪点。常见做法是训练时把imgsz提高到1280,显存不够就降到960或800,但不要低于640。

提高分辨率比增加epoch更有效,这是我用SSDD和类似遥感数据集跑出来的实际感受。加了50个epoch,mAP可能只涨零点几个点;把输入从640提到1280,mAP@0.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才是决定mAP@0.5:0.95的关键。如果box_loss下降特别慢,先怀疑标注框本身不准,再考虑学习率是不是设大了。

4.3 batch size与早停:一张卡怎么设不翻车

训练命令的典型写法:

yolo detect train data=ssdd.yaml model=yolov8s.pt epochs=200 imgsz=640 batch=16 patience=50 seed=42

参数说明:epochs设200,SSDD数据量小,100轮基本能过拟合,300轮属于浪费算力;patience是早停轮数,val_loss连续50轮不降就自动停,不用人盯着;seed跟3.3划分脚本里的种子保持一致,保证实验可复现。

batch size要结合显存和分辨率一起看。8G显存跑640分辨率,batch设16没问题;分辨率提到1280,batch降到8甚至4。有些人的做法是强行保持大batch,结果batch size超过显存直接OOM,或者触发YOLO自动降低batch,反而打乱了学习率的配合。我一般先把batch设成显存能承受的最大值,再去调学习率。

另外提一句不相关的坑:好几个人问过我v100上训练SSDD为什么比预期慢,看了半天发现是数据加载没开num_workers,GPU在那干等CPU喂数据。训练命令里把workers设成4或8,能明显改善单卡利用率。

5. 常见问题避坑:五条血泪经验

5.1 训练loss正常但验证mAP为0:类名大小写与编号对不上

现象:训练日志里loss稳定下降,验证集mAP@0.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坐标超过图像尺寸,比如xmax=801而width=796,归一化后边界框宽度大于1,YOLOv8的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 舰船小目标漏检多:只看mAP@0.5忽略mAP@0.5:0.95

现象:验证集mAP@0.5到0.9,看着不错,但把模型推到整幅遥感大图上,一条船都找不出来。

原因:SSDD图像本身就是从大图中截取的切片,目标在切片里占比没那么小,所以mAP@0.5虚高。实际遥感大图里舰船目标更小,训练时用的640分辨率把目标直接缩没了。

解决:提升推理分辨率,或者做切片推理,常见思路是把大图切成重叠的patch分别检测再合并结果。训练阶段也可以把mosaic增强关掉最后10个epoch,YOLOv8的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_ok=True) 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全是旧的。从那以后,我拿到任何下载来的数据集,第一件事永远是先做可视化质检,不做可视化不训练。这个习惯帮我挡掉了不少数据上的暗坑,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 11:55:26

Java开源量化交易平台:CTP接入、事件驱动与回测实盘一致性设计

简介&#xff1a;面向程序员与量化交易开发者的基于Java的AI开源量化交易平台&#xff0c;可替代文华、MC、金字塔等商业软件&#xff0c;支持期货、股票、外汇、数字货币等多种交易场景&#xff0c;具备历史回放、策略研发、模拟交易和实盘交易能力&#xff0c;兼顾全自动与半…

作者头像 李华
网站建设 2026/10/1 11:54:12

Spring Cloud Alibaba与Spring Boot、Nacos版本对应关系及避坑指南

先说我自己的翻车经历。几年前第一次搭 Spring Cloud Alibaba 环境&#xff0c;Spring Boot 用的 2.6.2&#xff0c;Spring Cloud 用的 2021.0.1&#xff0c;Nacos Server 下的 1.4.2&#xff0c;结果服务就是注册不上去。控制台打开好几遍&#xff0c;服务列表空空如也&#x…

作者头像 李华
网站建设 2026/10/1 11:54:04

java 爬虫大型教程(一)

java 爬虫大型教程&#xff08;一&#xff09;在开始之前, 鉴于这是一份大型的教程, 我们应当从最基础的环境变量配置环节说起, 逐步展开详细的搭建过程。关于电脑环境这块, 因为我的这台设备是 pro 型号嘛, 所以系统环境变量配置的方式, 和标准版本的那些机器, 它是不一样的。…

作者头像 李华
网站建设 2026/10/1 11:53:00

python多线程并发测试过程

进行多线程并发测试的过程。更新时间为2025年05月27日15点25分35秒, 作者是姑娘别秃头。这篇文章主要的内容是介绍了关于多线程并发的测试过程, 这样的资料是具有非常好的参考价值的, 希望能够帮助到大家, 如果文章中存在错误或没有考虑完全的情况, 希望读者能够不吝赐教。一、…

作者头像 李华
网站建设 2026/10/1 11:52:35

Ember-1模型:大模型推理Token压缩40%的工程实践

1. 项目概述&#xff1a;一场被低估的推理效率革命Fireworks AI 这次发布的 Ember-1 模型&#xff0c;表面看只是“基于 Kimi K3 的后训练模型”&#xff0c;但真正值得所有开发者、算法工程师和产品技术负责人坐直身体细读的&#xff0c;是那句轻描淡写的“推理 Token 减少约 …

作者头像 李华