ARTICLE DETAIL

资讯详情

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

9368张船只VOC数据集实战:格式转换、训练配置与切片推理

9368张船只VOC数据集实战:格式转换、训练配置与切片推理 简介这是一份面向目标检测初学者与算法工程师的Pascal VOC格式船只检测数据集可直接用于YOLO、Faster R-CNN等主流框架的训练与验证帮助解决船只识别场景中样本不足、标注不规范的问题。压缩包共约2000个文件包含9368张jpg图片与9368个同名xml标注文件另附一份使用说明txt整体大小约986.98MBxml由labelImg绘制矩形框生成仅含boat一个类别共标注20502个目标框图片与标注一一对应便于直接划分训练集与测试集。目前已有732人学习下载适合作为水面目标检测、遥感船只识别等课题的练手数据。数据集仅提供准确合理的标注不对训练所得模型或权重文件的精度作任何保证读者可据此快速搭建基线、验证数据增强与模型调参效果。1. 9368 张船只检测数据集VOC 格式到底能直接喂给哪些模型海面上一条船在监控画面里可能只占几十个像素也可能横跨半个画面这种尺度跨度极大的目标检测任务用通用数据集往往跑不出理想效果。这次拆的是一份专门针对船只的 Pascal VOC 格式数据集9368 张 jpg 图片配 9368 个同名 xml 标注文件标注类别只有一类 boat总标注框数 20502 个平均每张图约 2.2 个目标。标注工具用的是 labelImg规则就是最朴素的矩形框没有分割掩码、没有旋转框、没有关键点。这份资源适合谁如果你在做港口监控、航道船舶识别、无人机海面巡检这类方向需要一份开箱即用的单类检测数据来跑通训练流程或者做 baseline 验证它省掉了自己从零标注的几百个小时。但要注意它只包含 jpg 和 xml没有划分好的 train/val txt 文件也没有 yolo 格式的 txt所以拿到手第一件事不是直接开训而是先做格式转换和数据集划分。下面按实际落地顺序从目录结构、格式解析、转换脚本、训练配置到踩坑排查一步步拆开讲。2. VOC 目录结构与 xml 标注字段逐项拆解2.1 拿到压缩包先看什么文件命名与配对关系解压后你会看到一堆文件平铺在一起典型长这样firc_boat_817.xml、firc_boat_3968.xml、000000442822.xml、000000459673.xml、000000371595.xml、000000102190.xml、000000127279.xml、000000205996.xml、000000492492.xml外加一个使用说明.txt。命名分两种风格一种带firc_boat_前缀一种是大厂公开数据集常见的 12 位数字编号。这不影响使用但说明数据来源可能混合了多个渠道后面做类别平衡分析时值得留意。关键点是配对每个 xml 必须有一个同名 jpg。VOC 格式本身不强制目录结构但主流框架py-faster-rcnn、YOLO 转换脚本、mmdetection默认期望这样的布局VOCdevkit/ └── VOC2007/ ├── Annotations/ # 所有 xml ├── JPEGImages/ # 所有 jpg ├── ImageSets/ │ └── Main/ # train.txt val.txt trainval.txt test.txt └── SegmentationClass/ # 本数据集没有忽略我一般先跑一段脚本确认配对完整性别等到训练时报image not found才回头查。下面这段检查脚本直接可用import os img_dir JPEGImages xml_dir Annotations imgs {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)} xmls {os.path.splitext(f)[0] for f in os.listdir(xml_dir) if f.endswith(.xml)} only_img imgs - xmls # 有图无标注 only_xml xmls - imgs # 有标注无图 print(f图片总数: {len(imgs)}, 标注总数: {len(xmls)}) print(f有图无标注: {len(only_img)}, 有标注无图: {len(only_xml)}) if only_img: print(示例:, list(only_img)[:5]) if only_xml: print(示例:, list(only_xml)[:5])逻辑说明用集合差集找出不配对的样本。参数上img_dir和xml_dir按你实际解压路径改。如果only_img不为空说明这些图没有标注训练时要么剔除要么当负样本如果only_xml不为空说明标注文件对应的图丢了必须删掉对应 xml否则转换脚本会报错。2.2 xml 里到底存了什么六个必须读懂的字段VOC 的 xml 结构固定但真正影响训练的有六个字段逐个说清楚字段路径含义对本数据集的影响size/widthsize/height图片原始宽高转换时用于归一化必须和 jpg 实际尺寸一致size/depth通道数通常为 3灰度图会是 1需注意object/name类别名本数据集只有boat一个值object/bndbox框坐标 xmin/ymin/xmax/ymax左上右下绝对像素值object/difficult是否难样本部分数据为 1训练时可选择忽略object/truncated是否截断船只出画时常见影响评估策略一个典型 xml 片段annotation folderJPEGImages/folder filenamefirc_boat_817.jpg/filename size width1920/width height1080/height depth3/depth /size object nameboat/name difficult0/difficult bndbox xmin412/xminymin336/ymin xmax689/xmaxymax502/ymax /bndbox /object /annotation这里有个血泪经验VOC 的坐标是 1-based 还是 0-based 在不同标注工具下会飘。labelImg 导出的是 1-basedxmin 最小为 1而 YOLO 要求 0-based 归一化中心点。转换时如果直接减 1 又除错分母框会整体偏移几个像素小目标上尤其明显。稳妥做法是转换后随机抽 20 张画框可视化肉眼确认贴合。2.3 20502 个框意味着什么先做统计再决定增强策略9368 张图、20502 个框平均每图 2.2 个目标但分布一定不均匀。有的图可能十几条船挤在港口有的图只有一条远洋船。训练前跑一遍框尺寸统计直接决定你后面用不用 mosaic、要不要多尺度训练import os, xml.etree.ElementTree as ET import numpy as np xml_dir Annotations ws, hs, ratios [], [], [] for f in os.listdir(xml_dir): if not f.endswith(.xml): continue root ET.parse(os.path.join(xml_dir, f)).getroot() for obj in root.findall(object): b obj.find(bndbox) w float(b.find(xmax).text) - float(b.find(xmin).text) h float(b.find(ymax).text) - float(b.find(ymin).text) ws.append(w); hs.append(h); ratios.append(w / max(h, 1)) ws, hs np.array(ws), np.array(hs) print(f框宽 中位数 {np.median(ws):.1f} 均值 {ws.mean():.1f} 最大 {ws.max():.0f}) print(f框高 中位数 {np.median(hs):.1f} 均值 {hs.mean():.1f} 最大 {hs.max():.0f}) print(f宽高比 中位数 {np.median(ratios):.2f}) print(f小框(32px)占比: {(ws*hs 1024).mean()*100:.1f}%)逻辑说明ws*hs 1024对应 COCO 定义的小目标面积小于 32×32。如果小框占比超过 30%anchor 尺寸要往小调或者直接用 anchor-free 的 FCOS、YOLOv8。参数上xml_dir换成你的路径即可。这份数据里船只远小近大宽高比中位数大概率在 2 到 4 之间说明横向长条目标多anchor 的 aspect ratio 要覆盖到 3:1 甚至 5:1。3. 从 VOC 到 YOLO 格式转换脚本与划分策略3.1 为什么必须转换框架对格式的硬性要求VOC 的 xml 是给人读的训练框架要的是紧凑的数值。YOLO 系列要每张图一个 txt每行class cx cy w h全部归一化到 0~1mmdetection 虽然能直接读 VOC但配置里要指定ann_file和img_prefix且默认按 VOC2007 的 trainval/test 划分走。这份数据没有划分文件所以无论走哪条路你都得自己生成 ImageSets/Main 下的 txt或者转成 YOLO 格式。我一般优先转 YOLO txt原因是通用性强YOLOv5/v8、Ultralytics 全家桶、甚至部分 mmdetection 配置都能吃。转换脚本如下import os, xml.etree.ElementTree as ET classes [boat] # 本数据集只有一类 xml_dir Annotations out_dir labels os.makedirs(out_dir, exist_okTrue) for f in os.listdir(xml_dir): if not f.endswith(.xml): continue root ET.parse(os.path.join(xml_dir, f)).getroot() size root.find(size) W int(size.find(width).text) H int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in classes: continue cls_id classes.index(name) b obj.find(bndbox) xmin float(b.find(xmin).text) ymin float(b.find(ymin).text) xmax float(b.find(xmax).text) ymax float(b.find(ymax).text) # 裁剪到图像边界防止越界框 xmin max(0, min(xmin, W - 1)) xmax max(0, min(xmax, W - 1)) ymin max(0, min(ymin, H - 1)) ymax max(0, min(ymax, H - 1)) cx (xmin xmax) / 2.0 / W cy (ymin ymax) / 2.0 / H w (xmax - xmin) / W h (ymax - ymin) / H lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(os.path.join(out_dir, f.replace(.xml, .txt)), w) as fp: fp.write(\n.join(lines))逻辑说明classes列表顺序决定类别 id本数据集只有 boat 所以是 0。坐标裁剪那四行是后悔药——有些 xml 的框会超出图像边界标注时手滑不裁剪的话归一化后会出现负值或大于 1 的值YOLO 训练时直接报non-normalized coordinates。:.6f保留六位小数足够精度。转换完记得抽查随便挑一个 txt反算回像素坐标和原 xml 对比。3.2 划分 train/val别用随机划分糊弄小目标9368 张图常见做法是 8:1:1 或 7:2:1。但船只检测有个坑同一段视频抽帧出来的图高度相似如果纯随机划分训练集和验证集里会出现几乎一样的画面验证指标虚高上线就翻车。我一般按文件名前缀或拍摄场景做分组划分保证同一来源的图只进一个集合。如果文件名没有明显分组信息退而求其次用感知哈希去重后再随机划分import os, random, hashlib from PIL import Image img_dir JPEGImages names [os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)] # 简单去重用缩略图字节的 md5 当指纹 seen {} unique [] for n in names: im Image.open(os.path.join(img_dir, n .jpg)).resize((16, 16)).convert(L) fp hashlib.md5(im.tobytes()).hexdigest() if fp not in seen: seen[fp] n unique.append(n) random.seed(42) random.shuffle(unique) n_total len(unique) n_train int(n_total * 0.8) n_val int(n_total * 0.1) splits { train: unique[:n_train], val: unique[n_train:n_train n_val], test: unique[n_train n_val:], } for k, v in splits.items(): with open(f{k}.txt, w) as fp: fp.write(\n.join(v)) print(k, len(v))逻辑说明16×16 灰度缩略图的 md5 能过滤掉几乎完全相同的帧但不会误杀正常差异的图。random.seed(42)保证可复现。参数上 0.8/0.1/0.1 可按需调如果验证集指标波动大把 val 比例提到 0.15。划分文件里存的是不含扩展名的文件名YOLO 训练时通过路径拼接找图和标签。3.3 生成 YOLO 的 data.yaml转换和划分完成后写一个 data.yaml 指向各路径path: /data/boat_voc train: train.txt val: val.txt test: test.txt nc: 1 names: [boat]path是数据集根目录train/val/test是相对路径的 txt。nc必须和 classes 长度一致写错会直接报维度不匹配。如果走 mmdetection则改用 VOCDataset 配置ann_file指向 ImageSets/Main/train.txtimg_prefix指向 JPEGImages类别数在bbox_head.num_classes里设 1。4. 训练配置与参数单类检测的 anchor 和增强怎么设4.1 anchor 尺寸用 k-means 在这份数据上重新聚类通用 COCO 预训练的 anchor 是给 80 类设计的直接拿来训单类船只召回率会吃亏。YOLOv5/v8 自带kmeans.py但更省事的做法是用上面统计出的框宽高直接聚类。核心参数是聚类数通常 9 个和输入分辨率640 或 1280。船只目标偏小我一般把输入提到 1280anchor 相应放大python utils/autoanchor.py --data data.yaml --img-size 1280 --anchors 9如果框架没有 autoanchor手动设一组覆盖小到大的 anchor比如[10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326]是 COCO 默认船只场景可以把前三组缩小到[8,8, 12,16, 20,12]覆盖远小目标。判断标准训练几个 epoch 后看best possible recall低于 0.9 就说明 anchor 不匹配。4.2 数据增强mosaic 要开但 cutout 慎用船只检测的增强策略和通用检测有区别。mosaic 四图拼接能显著增加小目标和遮挡场景建议开但mosaic概率别拉满0.5~0.8 之间比较稳拉满会让训练前期 loss 震荡。HSV 增强对海面光照变化有帮助hsv_v可以设到 0.4。翻转方面水平翻转安全垂直翻转要谨慎——船在水面上的倒影被翻上去会变成不合理的样本除非你的实际场景里真有倒置视角。cutout随机遮挡在这份数据上要小心船只本身可能只占几十像素再挖一块黑目标直接没了模型学到的是噪声。如果一定要用遮挡面积控制在 5% 以内。常见做法是先用默认增强跑一版 baseline看验证集 mAP再逐项加增强对比。4.3 训练超参从预训练权重出发别从零训9368 张图不算大从零训容易过拟合。用 COCO 预训练的 backbone冻结前几层学习率用余弦退火初始 lr 设 0.01SGD或 1e-3AdamW。batch size 按显存来单卡 16G 用 640 分辨率可以到 161280 分辨率降到 4 或 8配合梯度累积。epoch 数 100~300早停 patience 设 30。一个容易忽略的参数是overlap_mask和mask_ratio——单类检测用不到分割头但 YOLOv8 默认会算白白吃显存。配置里把task明确设为detect别用segment。另外single_cls参数如果框架支持设为 True 能省掉类别分支的开销本数据集只有 boat正好用上。5. 避坑与排查这份数据集上最容易翻车的五件事5.1 现象训练 loss 正常下降但验证 mAP 始终为 0原因验证集的 txt 路径或类别 id 对不上。YOLO 读标签时如果找不到对应 txt会当负样本处理loss 照降但模型学不到东西。另一种可能是 data.yaml 里names写成了[boat]但转换脚本里 classes 顺序不一致导致类别 id 错位。解决训练前跑一遍verify脚本逐张检查图和标签是否配对、坐标是否在 0~1 之间。Ultralytics 自带dataset.verify()或者用第 2 章的配对检查脚本。确认 val.txt 里的文件名和 labels 目录下的 txt 一一对应。5.2 现象小目标漏检严重大船检测正常原因输入分辨率太低或者 anchor 最小尺寸偏大。9368 张图里如果小框占比高640 分辨率下几十像素的船缩到特征图上只剩几个像素卷积核根本抓不住。解决把imgsz提到 1280同时用第 4 章的 k-means 重新聚类 anchor最小 anchor 降到 8~12 像素。如果显存不够用切片推理SAHI在推理阶段切图训练阶段保持 1280。另一个检查点是strideYOLO 的 P3 特征图 stride 是 81280 输入下 P3 是 160×160对小目标足够。5.3 现象xml 解析报not well-formed或no element found原因部分 xml 文件损坏或编码异常。labelImg 在异常退出时可能写出截断的 xml。这份数据混合了多个来源个别文件出问题不奇怪。解决批量校验 xml 可解析性把坏文件挑出来import os, xml.etree.ElementTree as ET bad [] for f in os.listdir(Annotations): if not f.endswith(.xml): continue try: ET.parse(os.path.join(Annotations, f)) except Exception as e: bad.append((f, str(e))) print(f损坏文件数: {len(bad)}) for f, e in bad[:10]: print(f, e)坏文件要么修复手动补全闭合标签要么连同对应 jpg 一起剔除。别硬塞进训练解析失败会让整个 dataloader 崩掉。5.4 现象训练到一半显存溢出OOM原因1280 分辨率 batch 16 mosaic 增强显存峰值远超预期。mosaic 会把四张图拼成一张实际输入尺寸不变但预处理缓存翻倍。解决降 batch 到 4 或 8开梯度累积补回等效 batch。或者用amp自动混合精度显存能省 30%~40%。另一个隐藏因素是 dataloader 的workers设太大比如 16每个 worker 都缓存一批图显存和内存一起爆设成 CPU 核数的一半比较稳。5.5 现象验证集指标很好实际部署误检一堆原因训练集和验证集同源划分时没做去重模型记住了背景而不是船。海面背景单一模型很容易学到“有水就是船”或者“有白色块就是船”。解决按第 3.2 节的感知哈希去重后再划分确保验证集里有训练集没见过的场景。另外在验证集里单独统计误检率FP不只看 mAP。如果 FP 高加负样本没有船的海面图进训练集比例控制在 10% 左右。这份数据集全是正样本适当补负样本能显著降误检。6. 进阶用切片推理把远小船只的召回再拉一截训练完模型部署时最头疼的还是远小目标。1280 输入下一条 20 像素的船在 P3 特征图上只剩 2~3 个像素检测头很难响应。我一般会在推理阶段加一层切片推理SAHI 思路把大图切成带重叠的小块分别检测再合并代价是推理时间翻几倍但小目标召回能提 10~20 个点。核心逻辑是滑动窗口切图每块单独送模型最后用 NMS 合并跨块的框。下面是一个最小实现import numpy as np from PIL import Image def slice_inference(model, img_path, slice_size640, overlap0.2, conf0.25, iou0.5): img np.array(Image.open(img_path).convert(RGB)) H, W img.shape[:2] step int(slice_size * (1 - overlap)) boxes, scores [], [] for y in range(0, H, step): for x in range(0, W, step): patch img[y:yslice_size, x:xslice_size] if patch.shape[0] 32 or patch.shape[1] 32: continue # model.predict 返回该块的框坐标是块内坐标 res model.predict(patch, confconf, iouiou, verboseFalse)[0] for b in res.boxes: x1, y1, x2, y2 b.xyxy[0].tolist() boxes.append([x1 x, y1 y, x2 x, y2 y]) scores.append(float(b.conf[0])) if not boxes: return [] # 跨块 NMS boxes np.array(boxes) scores np.array(scores) keep nms(boxes, scores, iou) return boxes[keep], scores[keep]逻辑说明slice_size是每块边长overlap控制重叠比例0.2 意味着相邻块重叠 20%防止目标正好卡在切缝上被截断。step是滑动步长。每块检测完把坐标加回全图偏移最后统一 NMS。参数上slice_size一般设成训练分辨率640 或 1280overlap0.2~0.3 之间太小会漏切缝目标太大推理慢。conf可以比正常推理低一点0.2因为切片后小目标置信度普遍偏低靠后续 NMS 过滤。验证切片推理有没有效果别只看 mAP单独统计小目标面积 32×32的召回率。我习惯在验证集上跑两遍一遍整图推理一遍切片推理对比小目标召回。如果提升不明显说明瓶颈不在分辨率而在标注质量或 anchor回头查第 5 章。从那以后我每次拿到新数据集都强制先跑配对检查、框尺寸统计、去重划分这三步再动手写训练配置。这份 9368 张的船只 VOC 数据集底子干净标注类别单一适合快速验证检测 pipeline但远小目标和背景误检这两个坎得靠切片推理和负样本补足。希望帮到你。本文还有配套的精品资源点击获取
返回列表