VOC转YOLO:船只检测数据集格式转换与训练避坑指南

发布时间:2026/9/27 23:10:58
VOC转YOLO:船只检测数据集格式转换与训练避坑指南 简介包含VOC与YOLO两种标注格式的船只检测数据集聚焦海洋监控、航海安全与港口管理等场景中的目标识别问题适合计算机视觉研究者、算法工程师用于模型训练与效果验证。资源包内共25683个文件其中8561张jpg原图与8561份xmlVOC及8561份txtYOLO标注一一对应涵盖5000多张正面、侧面船只图像压缩后约863.6MB可按需挑选对应格式训练或转换。该数据集的标注完整、场景多样能有效帮助模型适应光照变化、视角变换与遮挡等情况读者可直接配合YOLOv3/v4、Faster R-CNN等框架开展迁移学习也可用IoU、mAP等指标系统评估检测效果免去自行采集与人工标注的高昂成本缩短项目前期准备周期。目前已有3888人学习下载适合需要快速搭建船只检测基线或扩充多样化训练样本的开发者。1. 船只检测数据集为什么“图片多”不等于“能训练”做水上目标检测的人经常陷入一个误区以为数据集只要图片数量足够大、船只样本足够多丢进 YOLO 里就能训出一个能用的模型。等你真正把数据集下载下来、解压、翻开标注文件一看才发现光是把 VOC 格式的 XML 转成 YOLO 能吃的 TXT就够你折腾一整晚。更别说类别名对不上、边界框坐标越界、图片里混着压根没有船只的负样本——这些都是船只检测数据集里最常见的“隐藏坑”。这篇笔记围绕的是一份同时提供 VOC 和 YOLO 两种格式的船只检测数据集重点解决三个问题这数据集里到底有什么、两种格式怎么切换、训练时哪些位置最容易翻车。适合正在做水上安全监控、船舶识别、港口智能管理或者拿 YOLOv5/v8 练手但不想自己从零标注的开发者。数据集本身不复杂但格式转换和训练调参之间的衔接才是真正拉开效果差距的地方。2. 拆开数据集VOC 和 YOLO 两种格式到底差在哪2.1 目录结构的差异一眼看出这份数据集“有没有诚意”一份真正可用的船只检测数据集解压之后目录结构是能直接看出门道的。VOC 风格的标准目录长这样dataset/ ├── annotations/ │ ├── 000001.xml │ ├── 000002.xml │ └── ... ├── JPEGImages/ │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... └── ImageSets/ └── Main/ ├── train.txt ├── val.txt └── test.txt而 YOLO 风格的结构则是把标注换成 txt同时多出一个 classes 文件dataset/ ├── images/ │ ├── train/ │ │ ├── 000001.jpg │ │ └── ... │ └── val/ │ ├── 000002.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 000001.txt │ │ └── ... │ └── val/ │ ├── 000002.txt │ └── ... └── classes.txt判断一份数据集是否“可用”我一般先看 annotations 和 JPEGImages 两个文件夹的数量是否一致。如果图片有 3000 张而标注只有 2800 个 XML说明存在漏标图片训练前必须筛掉。再看 ImageSets/Main 里的 train.txt 和 val.txt 是否已经帮你分好集——有的数据集偷懒不分要自己拿脚本切这本身多了一道工序但还能接受。最怕的是只有 JPEGImages 没有 annotations那基本就是半成品等于白下载。2.2 标注格式的字段差异从 XML 到 TXT 到底改了什么VOC 格式的标注文件是 XML核心信息藏在 object 节点里。一份正常的船只标注大致长这样annotation folderJPEGImages/folder filename000123.jpg/filename size width1920/width height1080/height depth3/depth /size object nameship/name bndbox xmin365/xmin ymin480/ymin xmax824/xmax ymax721/ymax /bndbox /object /annotation而 YOLO 格式是每张图片对应一个同名的 txt 文件每一行代表一个目标格式为class_id x_center y_center width height其中 x_center、y_center、width、height 全部是相对于图片宽高的归一化值取值在 0 到 1 之间。也就是说同样一个船框在 VOC 里存的是像素绝对值到 YOLO 里要除以图片宽高变成比例值。转换的核心就是这个归一化计算x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height box_width (xmax - xmin) / width box_height (ymax - ymin) / height这里有一个新手最爱踩的坑VOC 的坐标是你框出来的左上角和右下角而 YOLO 的中心点坐标是以图片左上角为原点的相对位置。转换时如果你直接用 xmin / width 当中心点结果就是框的位置整体偏到左上角训练时 loss 怎么降都降不下去。2.3 classes 文件与类别 ID 的对应关系YOLO 训练时需要一个 classes.txt有些框架叫 obj.names 或 dataset.yaml里面按行写类别名。顺序就是类别 ID从 0 开始。这份船只检测数据集如果是单类别classes.txt 里通常只有一行ship但如果你拿到的是多类别数据集比如包含 ship、boat、vessel 三个类别那转换脚本里必须先读 classes 文件建立索引映射再用索引去写 txt否则类别顺序错位会让模型学成“张冠李戴”。我见过最离谱的一次是朋友拿到一份数据集VOC 格式里类别是中文命名classes.txt 是英文转换时没做映射训完发现模型把船识别成了桥——因为类别 ID 全乱了。3. 格式转换实操把 VOC 转成 YOLO 的完整脚本与解读3.1 先做数据体检不检查直接转必翻车拿到数据集第一件事不是写转换脚本而是先跑一遍“体检”看图片和标注是否匹配、XML 里有没有异常值。我写过一个小脚本用来快速排查import os from xml.etree import ElementTree as ET img_dir JPEGImages ann_dir annotations imgs {f.split(.)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)} anns {f.split(.)[0] for f in os.listdir(ann_dir) if f.endswith(.xml)} # 找出缺失标注的图片 missing imgs - anns if missing: print(f发现 {len(missing)} 张图片没有标注: {list(missing)[:5]}...) # 检查 XML 里是否有越界坐标 for name in anns: xml_path os.path.join(ann_dir, name .xml) tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) for obj in root.iter(object): box obj.find(bndbox) xmin float(box.find(xmin).text) xmax float(box.find(xmax).text) ymin float(box.find(ymin).text) ymax float(box.find(ymax).text) if xmin 0 or ymin 0 or xmax w or ymax h: print(f{name}.xml 坐标越界: ({xmin}, {ymin}) - ({xmax}, {ymax}))这段脚本逻辑很简单但非常管用。它先对图片和标注的文件名取集合做差集找出缺标注的图然后解析每个 XML 里的 size 节点拿图片真实宽高再把每个 bndbox 的四个坐标值和图片边界比较。越界坐标这个坑在水上场景特别常见——标注员在船只靠岸、船身被码头遮挡时容易把框拉到画面外。这类坐标如果直接转换进 YOLO 格式虽然训练时一般不会报错但会在 loss 里埋下持续的噪声模型收敛变慢边界框预测也容易偏。3.2 写转换脚本一步到位生成 YOLO 格式体检通过之后进入正式转换。下面这个脚本是常见做法把 VOC 的 XML 批量转成 YOLO 的 txt同时按 Train/Val 比例自动分好目录import os import random from xml.etree import ElementTree as ET # 配置参数 xml_dir annotations # VOC XML 目录 img_dir JPEGImages # 图片目录 target_dir yolo_dataset # 输出目录 train_ratio 0.8 # 训练集比例 classes [ship, boat] # 与数据集的 classes.txt 保持一致 os.makedirs(f{target_dir}/images/train, exist_okTrue) os.makedirs(f{target_dir}/images/val, exist_okTrue) os.makedirs(f{target_dir}/labels/train, exist_okTrue) os.makedirs(f{target_dir}/labels/val, exist_okTrue) # 写 classes.txt with open(f{target_dir}/classes.txt, w) as f: for c in classes: f.write(c \n) xml_files [f for f in os.listdir(xml_dir) if f.endswith(.xml)] random.shuffle(xml_files) split_idx int(len(xml_files) * train_ratio) for i, xml_file in enumerate(xml_files): name xml_file[:-4] tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) subset train if i split_idx else val label_lines [] for obj in root.iter(object): cls obj.find(name).text if cls not in classes: continue cls_id classes.index(cls) 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) # 坐标越界则裁到边界 xmin max(xmin, 0) ymin max(ymin, 0) xmax min(xmax, img_w) ymax min(ymax, img_h) # 归一化到 YOLO 格式 x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 过滤掉宽高为 0 的目标 if w 0 or h 0: continue label_lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) # 写标注文件 with open(f{target_dir}/labels/{subset}/{name}.txt, w) as f: f.write(\n.join(label_lines)) # 拷贝图片到对应子集 src os.path.join(img_dir, name .jpg) dst f{target_dir}/images/{subset}/{name}.jpg if os.path.exists(src): os.system(fcp {src} {dst}) print(f转换完成: train {split_idx} 张, val {len(xml_files) - split_idx} 张)这段脚本有几个参数值得说清楚。train_ratio 控制训练集和验证集的分割比例0.8 是常规选择但如果你后续要做测试集评估可以改成 0.7 / 0.15 / 0.15 并在后面再加一个 test 分支。classes 列表必须和原数据集自带的类别定义一致如果你不确定原数据集有哪些类别可以先写个脚本遍历所有 XML 统计 name 节点的取值集合别凭猜。坐标越界的处理上脚本选择裁到边界而不是直接丢弃这是为了尽量减少信息损失——船只靠岸时被遮挡的框往往还有大半截有效区域裁掉比整框删掉对训练更友好。跑完脚本后建议抽查几个转了格式的文件。找一张已知坐标的图用 OpenCV 画框对比一下原图中的船只位置确认框没有偏移。3.3 验证生成的 YOLO 标注可视化这一步不能省这一步是很多人跳过的但恰恰是最能提前发现问题的一步。可视化验证的小脚本如下import cv2 img_path yolo_dataset/images/train/000123.jpg label_path yolo_dataset/labels/train/000123.txt classes [ship, boat] img cv2.imread(img_path) h, w img.shape[:2] with open(label_path, r) as f: for line in f.readlines(): cls_id, x_c, y_c, bw, bh map(float, line.split()) # 反归一化回像素坐标 x1 int((x_c - bw / 2) * w) y1 int((y_c - bh / 2) * h) x2 int((x_c bw / 2) * w) y2 int((y_c bh / 2) * h) color (0, 255, 0) if int(cls_id) 0 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, classes[int(cls_id)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 1) cv2.imwrite(check_000123.jpg, img)随机抽 10 到 20 张图做可视化如果发现框和船体重合度低、偏移明显基本可以断定是 XML 里的 size 字段和实际图片尺寸不一致——这类数据集常见于网上爬取的图片被二次压缩但标注没跟着更新。另一个高发问题是类别 ID 错位可视化时直接看文字标签就能一眼识别。4. 训练前的数据准备把数据集喂给 YOLO 的关键配置4.1 目录改名与 data.yaml 的写法如果你用的是 YOLOv5 或 YOLOv8转换完的目录还得按要求重新组织。YOLOv5 期望的完整结构是yolo_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml 是训练入口的配置文件官方示例格式如下train: ../yolo_dataset/images/train val: ../yolo_dataset/images/val nc: 2 names: [ship, boat]注意 train 和 val 的路径写的是 images 目录而非 labels 目录——YOLO 训练框架会自动去同级 labels 目录下找对应的 txt 文件。这是初学者最容易懵的地方明明路径指向 images为什么标签能被找到因为框架内部把路径字符串里的 images 替换成 labels 来定位标注文件。如果你自建目录时用了其他命名比如 imgs / anns框架就找不到了。4.2 类别不平衡如何处理船只检测场景的类别不平衡有点特殊。常见问题是“船”这一类别里大船样本多、小船样本少如果数据集是多类别的比如货轮、渔船、游艇往往货轮占比 70% 以上。直接训练的结果就是模型对大目标极其敏感对远处的小船基本无感。处理手段按优先级排列最简单的是用--hyp调整 YOLOv5 的 mosaic 和 copy-paste 增强概率让小船样本被更多组合进训练图其次是准备阶段做类别重采样对小样本类别过采样——让脚本统计每个类别的标注数给样本量少的类别提高在训练集中的占比。这种做法不需要改模型结构落地成本最低。如果数据集本身有类别注释信息也可以直接删掉占比过大的类别的部分样本让分布更均衡但这样会损失数据量看你的底线在哪里。4.3 图片分辨率与批次大小的取舍船只检测数据集的图片常见分辨率有两种一种来自公开数据集长边 640 或 416已经比较小另一种来自实地采集可能是 1920×1080 的监控截图。后一种如果直接缩到 640 训练远处的小船会缩成几个像素目标直接消失。我一般做法是先看数据集中目标的像素占比统计所有标注框的宽高分布如果大量框的宽度在 10 到 30 像素区间训练尺寸应设在 1280 而不是 640。对应地批次大小要往下调。YOLOv5 在 8 张 A100 上可以跑 batch 64但 1280 分辨率下显存消耗翻倍。一个比较稳妥的组合是分辨率 1280、batch 16、单卡 24GB 显存。显存不够就先降 batch保分辨率为了保 batch 去降分辨率是捡芝麻丢西瓜。5. 避坑手册船只检测数据集最常见的五个翻车现场5.1 图片里有“船”但标注里没有负样本污染现象训练时 loss 到后期居高不下验证集 mAP 卡在一个不高不低的水平上不去。上报的误检一大堆模型把码头上的吊机、岸边的礁石、水面反光都当成船。原因数据集里混入了大量没有标注的图片这些图片恰恰包含疑似目标是但被漏标的情况。模型在训练时看到这些“无标注目标”会学着把它们当成背景推理时自然就漏检。解决训练前做一次全量标注筛查。简易做法是训练一个初始模型对没有标注的负样本图片做推理把置信度高于 0.3 的检测框提取出来人工确认。如果确认是船补标进数据集如果确认不是留作负样本没问题。这套流程花一两个小时值得能明显改善误检。5.2 XML 中坐标超出图片边界现象转换后的 txt 里出现大于 1 的归一化坐标或者在可视化时框跑到图片外面去。原因标注工具允许标注框超出图片边界或者数据集的 XML 是程序自动生成的内部出现坐标漂移。解决转换脚本里做 clamp 到 [0, 图片宽高] 区间同时过滤掉宽高小于 0 的非法框。更严格的做法是直接筛出这类 XML 人工修正。注意裁切越界坐标是一种保留大部分信息的折中做法如果越界量超过框的 50%建议直接丢弃这个框否则会给模型引入不准确的真值。5.3 类别 ID 不连续导致训练直接报错现象YOLOv5 训练过程中报AssertionError: Class 5 is not in class list或者index out of range。原因转换脚本里按源数据集的类别顺序写 ID但 data.yaml 里的 names 列表少写了一类或者类别顺序不一致。解决在转换脚本中统一从一个 classes.txt 读入类别表确保转换和训练用的是同一份。永远不要手动在 data.yaml 里敲类别名——从 classes.txt 复制粘贴不出错才是真的快。5.4 水面反光和船体阴影被当成独立目标现象模型误检率高特别是在中午阳光强烈的水域水面耀斑区域频繁出框。原因光照条件复杂导致数据集里部分标注把阴影或反光区域一并框了进去模型的真值本身就带噪。解决检查训练集里这类样本的比例。如果标注确实框了反光区域可在转换阶段加一条规则计算标注框内平均亮度和周围区域的差异差异超过阈值就标记为可疑样本人工复核。另一种思路是靠数据增强缓解——在训练时开启更强的 HSV 扰动让模型对颜色和亮度变化更鲁棒。5.5 训练集和验证集存在重叠图片现象训练 loss 正常下降验证 mAP 高得出奇但到了真实水域视频测试时效果一落千丈。原因数据集作者在划分 train / val 时用的是文件名前缀或目录抽样而不是按视频帧的时间序列划分导致同一船舶序列的连续帧同时出现在两边。模型相当于“背”过验证集泛化能力被高估。解决按图片所属的视频片段分组划分。如果数据集文件夹里没有片段信息可以用文件名编号的连续性来判断——比如 0001 到 0100 来自同一段视频划分时应该把这些编号连续的文件归为同一侧。划分完统计验证集和训练集的图片是否存在文件名交集有就重新切。6. 进阶用数据增强和迁移学习把精度再往上顶一截先说说迁移学习。船只检测数据集通常规模有限几千张图算常见几万张算奢求。用 COCO 预训练权重做起点几乎是必须的。YOLOv5 官方提供的yolov5s.pt、yolov5m.pt等预训练权重是在 COCO 上训过的已经具备通用物体特征提取能力。你在自己数据集上微调时加载预训练权重的方式直接影响效果python train.py --data data.yaml --weights yolov5s.pt --img 1280 --batch 16 --epochs 100 --project boat_yolo用预训练权重但冻结前 10 层骨干网络的前几层可以防止小数据集上特征提取层被大幅度扰动。解冻全部层训练容易在小样本上过拟合冻结骨干则模型的底层特征保持通用性只需要训练检测头和部分高维特征层适配船只外观。数据增强方面针对水上场景有两个格外有效的操作。第一个是 Mosaic把四张训练图拼成一张变相增加每张图的船只样本数量对提升小目标检测特别有效——船只检测里大量目标就是远处水面的小点。YOLOv5 的 Mosaic 默认开启但概率建议从默认值调到 0.8 以上。第二个是 MixUp把两张图按透明系数叠加对提升模型在水面光照变化下的鲁棒性效果明显。注意 MixUp 会改变目标边界模糊度船只轮廓本身和水面反差大所以对这类目标影响不大但如果你在训练初期发现 loss 不降可以先把 MixUp 关掉等骨干特征稳定后再开启。验证阶段有个自己总结的心法不要在训练集的验证集上看 final mAP 就收工。单独留出 100 张来自不同水域、不同天气的图片做盲测如果你的数据集里全是晴天白天的样本盲测集就要包含傍晚、阴天甚至雾天的图否则模型的真实泛化能力你永远心里没底。我在一次港口项目里就吃过亏——训练集 mAP 有 0.84下了几天雨之后现场测试直接掉到 0.61后来才发现数据集里没有雨天样本。最后一个建议是粒度把“ship”这个单一类别细化成“cargo_ship”和“fishing_boat”两个类模型对不同船型的特征区分会倒逼检测头学到更细的语义表征整体精度往往不降反升。如果你的数据集类别注释只到“ship”级别就别强拆了但如果是自建数据集从一开始就分细一点后面能省不少返工。这套从格式理解、转换、验证到训练调参的流程我在三个不同的船只检测项目里完整走过。数据集的格式问题永远是小问题真正决定效果的是你愿不愿意在训练前多花两个小时做数据体检和可视化验证。希望这份笔记能帮你在船只检测这条路上少踩几个坑把精力花在真正有价值的调参和验证上。本文还有配套的精品资源点击获取