ARTICLE DETAIL

资讯详情

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

舰船检测实战:boat数据集训练YOLOv5全流程解析

舰船检测实战:boat数据集训练YOLOv5全流程解析 简介面向舰船检测与YOLOv5模型训练的专用数据集包适合计算机视觉学习者、算法工程师以及海洋监控、智能航海等场景开发者使用。资源提取自VOCtrainval2012中的boat类别共1648个文件包含549张jpg原图、549个xml标签和550个txt标注压缩包大小约56.16MB。xml提供边界框坐标与类别信息txt为紧凑标注格式可灵活转换为YOLO训练所需格式。目前已有1483人学习下载省去自行筛选和整理VOC公开数据的繁琐环节。借助这套资源用户能快速完成从数据预处理、格式转换到模型训练评估的完整实验流程也可针对海面小目标、复杂背景等检测难点进行专门调优同时适用于海岸线监控、船舶交通管理等实际项目中的模型验证与部署参考。1. 舰船检测数据集 boat单类别为什么比多类别更抗造先讲一个反直觉的现象用公开 COCO 权重跑 yolov5 做舰船检测验证集 mAP 曲线挺好看可真把摄像头架到港口看两周误报率能到两三成——集装箱边缘、岸桥阴影、甚至浪花的纹理都可能被框成船。原因不玄COCO 里的 boat 类别尺寸跨度极大游艇、皮划艇、货轮混在一起模型学出来的是「水域里有长条物体」这种粗糙特征。这份 boat 舰船检测数据集把语义收敛到单一的 boat 类别所有标注框都落在真实舰船实例上专门补通用数据集在密集停泊、近岸背景上的短板。标注是标准 VOC xml 格式原图与标注一一对应目标就是让在做 yolov5 训练自己的数据集的工程师不必从零攒标注就能跑通整条链路。如果你在做海事监控、港口安防或无人机水面巡检手里有 yolov5 环境但差一份能直接喂给训练的数据这份数据集就是给你准备的。下面从目录结构、xml 解析、格式转换、超参数调整到树莓派部署把整个落地过程拆开讲。2. 标注数据不是黑匣子拆解 VOC xml 与目录结构2.1 JPEGImages 与 Annotations目录规约比想象中重要解压之后是典型的 VOC 风格目录短结构就三层boat_dataset/ ├── JPEGImages/ # 原始图像jpg ├── Annotations/ # 同名的 xml 标注 └── ImageSets/ └── Main/ # 可选的 train/val 划分第一件事不是急着开训练而是确认 JPEGImages 里每个 jpg 在 Annotations 里有同名的 xml。这句话听着像废话但我在项目里见过有人用文件管理器批量重命名图片结果 xml 全部对不上。命名规约通常是 6 位数字补零000001.jpg 对应 000001.xmlyolov5 的训练链路靠的是「同名不同后缀」的约定破坏了它后面所有 glob 匹配都会出错。检查方法很简单一个循环就够。ls JPEGImages/ | sed s/\.jpg$// | sort /tmp/img_ids.txt ls Annotations/ | sed s/\.xml$// | sort /tmp/xml_ids.txt diff /tmp/img_ids.txt /tmp/xml_ids.txtdiff 有输出说明缺图或缺标注先补齐再进下一步。别嫌这个检查琐碎xml 转 yolo、划分数据集、后续部署导出的每一步都在依赖文件名一致性这里翻车后面全是连锁的。ImageSets/Main 下的 txt 如果存在里面写的是按空格分隔的图片 id这份数据集带了可用的划分就直接用没带就按第 3 章的脚本自己划。不要默认 ImageSets 就是权威先看它和 Annotations 里实际存在的文件对不对得上。2.2 读懂 xml 标注中的 bndbox 与标签语义随机挑一个 xml 打开结构非常直白。以一张港口全景图为例annotation folderJPEGImages/folder filename000123.jpg/filename size width1280/width height720/height /size object nameboat/name bndbox xmin354/xmin ymin228/ymin xmax876/xmax ymax534/ymax /bndbox difficult0/difficult /object /annotation重点理解三个字段。size 下的 width 和 height 是原始图像尺寸后面换算归一化坐标必须用它object 里的 name 是类别标签这份数据集的核心类别就是 boatbndbox 四个值都基于原始像素坐标xmin/ymin 是左上角xmax/ymax 是右下角但有些标注工具会导出 xmax 小于 xmin 的情况后面脚本里要有保护。difficult 字段表示目标是否难以辨认VOC 时代用它跳过难例yolov5 的 txt 标签格式没有对应字段我的做法是转换时把 difficult1 的框单独备份留作后续难例挖掘不删原图。还有一个容易被忽略的点一张图多个目标的 xml 里 object 节点会有多个但 size 只有一个。港口停泊区那种并排十几条船的图一个 xml 里可能挂十多个 object这类图正是本数据集价值最高的部分——它把 COCO 里低压力的单船场景拉回到了真实频次的密集场景。folder、source、segmented 这些字段对目标检测无用直接忽略。2.3 数据体检脚本统计类别、框数与框面积分布动手转格式之前建议先做一次数据体检。目标有三个确认类别没有混入 ship、vessel 这类同义词确认每张图框数分布没有离谱的异常样本统计小目标占比这个数字直接决定后面 imgsz 用 640 还是 1280。解析用 Python 自带的 xml.etree.ElementTree不需要装第三方库。import xml.etree.ElementTree as ET import glob xml_list sorted(glob.glob(Annotations/*.xml)) cls_count {} box_count [] small_box 0 total_box 0 for xml_path in xml_list: tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) n 0 for obj in root.findall(object): name obj.find(name).text cls_count[name] cls_count.get(name, 0) 1 box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) w, h x2 - x1, y2 - y1 if w * h 32 * 32: # 粗略小目标阈值32x32 像素以下 small_box 1 n 1 total_box 1 box_count.append(n) print(类别分布:, cls_count) print(图片数量:, len(xml_list)) print(平均每张图框数:, sum(box_count) / len(box_count)) print(小目标占比: {:.2f}%.format(small_box / total_box * 100))参数口径说一下。root.find(size/width) 用的是 XML 路径表达式比先 find(size) 再 find(width) 少一层嵌套。小目标阈值 32×32 只是个粗略参考我更推荐用「框面积除以整图面积小于 0.01」来定义因为数据集里图片分辨率不统一有的图是 4K 航拍有的图是 720p 监控截图绝对像素阈值会失真。跑完如果发现 ship 或 vessel 混入类别必须先统一成 boat 再做下一步否则类别 id 映射全部错位。3. 把 VOC 转成 YOLO 格式转换脚本与四个边界坑3.1 坐标归一化与类别 id 映射yolov5 训练读取的不是 xml而是每个 txt 里按行的「class cx cy w h」归一化坐标。归一化必须基于 xml 里读出的真实宽高而不是训练时传入的 imgsz。这两者一旦混用坐标会整体偏移训练出来的模型推理时框全部偏到左上角这是个很隐蔽的坑你在验证阶段看 UI 可能一切正常因为 yolov5 推理时把标签重新缩放到输入图尺寸但如果 xml 转 txt 时除以的是 1280 而不是原图宽数据从源头就错了。另一个关键点是 class id 不是「boat 永远等于 0」而是取决于 data.yaml 里 names 列表的顺序。data.yaml 里写 nc:1、names:[boat]boat 的 id 才是 0。我习惯让转换脚本从同一个 yaml 读 names避免两处维护不一致。3.2 xml 转 txt 完整脚本下面是完整的转换脚本边界坑都做了防护。import xml.etree.ElementTree as ET import os, glob CLASSES [boat] # 与 data.yaml 的 names 完全一致 OUT_DIR labels os.makedirs(OUT_DIR, exist_okTrue) def convert(xml_path): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/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) box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) if x2 x1: x1, x2 x2, x1 # 防 xmax xmin if y2 y1: y1, y2 y2, y1 x1 max(0, min(x1, img_w)) # 坐标越界保护 x2 max(0, min(x2, img_w)) y1 max(0, min(y1, img_h)) y2 max(0, min(y2, img_h)) if (x2 - x1) 1 or (y2 - y1) 1: continue # 退化框丢弃避免 loss NaN cx (x1 x2) / 2.0 / img_w cy (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h cx min(max(cx, 0.0), 1.0) # 归一化后兜底 cy min(max(cy, 0.0), 1.0) w min(w, 1.0) h min(h, 1.0) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) out os.path.join(OUT_DIR, os.path.basename(xml_path).replace(.xml, .txt)) with open(out, w) as f: f.write(\n.join(lines)) for x in glob.glob(Annotations/*.xml): convert(x)参数说明按顺序走。CLASSES 顺序不能拍脑袋必须和 data.yaml 的 names 完全一致否则类别错位是静默的——loss 照常下降mAP 看着正常可视化推理时才发现框和类别对不上。第二个坑是退化框当标注框只有 1 像素宽归一化后出现 cx 和 w 相同这种异常会直接导致某次迭代的 box loss 变成 NaN然后整个训练崩掉。第三个坑是坐标整数化导致的越界xmin 或 xmax 偶尔会比图像宽大一点必须 clamp 到 [0, img_w] 区间内。第四个坑是归一化精度6 位小数足够写成 8 位不会提升精度反而在浮点到字符串转换时引入无意义抖动。3.3 train/val 划分与文件清单生成转换完 labels接下来做两件事把图片路径按比例切成 train 和 val 清单再把 images 和 labels 整理成 yolov5 期望的同层结构。yolov5 的 data.yaml 里 train 和 val 接受的是清单文件路径每行一个图片路径。import glob, random, os random.seed(42) # 固定随机种子保证可复现 images sorted(glob.glob(JPEGImages/*.jpg)) random.shuffle(images) split int(len(images) * 0.9) # 90% 训练 / 10% 验证 def write_list(path, items): with open(path, w) as f: f.write(\n.join(items) \n) write_list(train.txt, images[:split]) write_list(val.txt, images[split:])固定 random.seed(42) 的原因换机器或重跑脚本shuffle 结果会变导致训练验证集划分不一致回看实验记录时你以为在比超参数实际在比数据划分。比例我用 9:1舰船场景标注质量整体稳定不需要拿 20% 做验证如果后续要频繁对比 mAP改 8:2 也行别低于 8:2。清单写绝对路径还是相对路径取决于训练时从哪个目录起跑。我建议写成相对路径从数据集所在目录执行 train.py换机器时不用重新生成清单只要目录结构不动就能直接跑。4. 从零训练自己的 yolov5 模型配置、超参数与网络结构4.1 data.yaml路径与类别数对不上会直接报错进入训练前先把 data.yaml 和模型配置文件准备好。data.yaml 是训练管线的入口最少四行。# data.yaml train: ./train.txt val: ./val.txt nc: 1 names: [boat]train 和 val 指向第 3 章生成的清单文件这里用相对路径是刻意的。数据集目录整体迁到新机器时不用逐个改绝对路径配合 conda 环境重装只要相对位置不变就能直接跑。nc 是类别数量这里等于 1是最简场景names 列表和转换脚本里的 CLASSES 顺序一致。nc 与 len(names) 不等时 yolov5 会在加载数据阶段报出类似 ERROR dataset 的信息但不够直接最好启动时自己确认一遍 names 数量和 nc 完全对得上。4.2 模型选择yolov5s 还是 yolov5n先从哪个入手模型配置文件决定网络结构。yolov5 给 n/s/m/l/x 五档模型 yaml 里改 nc 就能适配自己的数据。结论放前面舰船检测第一版永远用 yolov5s别贪 yolov5l 的大感受野也别用 yolov5n 图快。舰船目标的特点是尺度跨度极端远处一条船只有十几个像素近处货轮占满整幅图。看 yolov5 网络结构图时抓三个要点backbone 输出的 P3、P4、P5 特征分别对应 8 倍、16 倍、32 倍下采样P3 负责小目标P5 负责大目标。yolov5n 把通道数砍到最薄P3 的特征表达能力弱小目标召回掉得明显。在 GPU 显存够用的情况下yolov5s 是训练时间和精度的平衡点如果计划把模型部署到树莓派 5训练阶段先用 s 拿到高水位 baseline最后再考虑换 n 或剪枝别让部署需求提前绑架训练选择。改模型配置文件只动 nc 一行# yolov5s_boat.yaml基于 models/yolov5s.yaml 修改 nc: 1 depth_multiple: 0.33 width_multiple: 0.50 anchors: - [10,13, 16,30, 33,23] # P3/8 - [30,61, 62,45, 59,119] # P4/16 - [116,90, 156,198, 373,326] # P5/32depth_multiple 和 width_multiple 是网络缩放系数s 档默认 0.33/0.50不需要改。anchors 列的是 COCO 原始值舰船目标偏长条后面训练时让 yolov5 自动开 anchor 聚类它会基于你的标签重新统计比手工微调靠谱这个在第 5 章展开。4.3 train.py 关键参数与超参数文件怎么改训练命令是整条流水线里参数最多的部分。第一次跑这套数据建议从这条命令起步。python train.py \ --data data.yaml \ --weights yolov5s.pt \ --img 1280 \ --batch 16 \ --epochs 300 \ --device 0 \ --project runs/ship_train参数口径逐个说。--img 1280 基于第 2 章体检结果如果小目标占比超过 15%imgsz 低于 1280 时十几个像素的船在输入图里小于 P3 特征的一个网格训练阶段就学不进去。--batch 16 在单卡 24G 显存下配合 --img 1280 差不多能跑。如果是 8G 卡别硬扛降到 8 然后小幅提高 epochsyolov5 对 batch 的敏感度低不需要为调大 batch 牺牲稳定性。--epochs 300 搭配早停千张级别的舰船数据集通常 150 到 250 个 epoch 收敛跑满 300 是给边界情况留余量。超参数文件以 data/hyps/hyp.scratch-low.yaml 为底改几个值。# hyp.finetune.yaml lr0: 0.005 lrf: 0.1 mosaic: 1.0 copy_paste: 0.2 fliplr: 0.5lr0 初始学习率从默认的 0.01 压到 0.005原因是迁移学习加全图增强的组合下0.01 对单类别数据集偏激进loss 曲线开局容易震荡最后收敛结果差不多但排查过程很折磨。copy_paste 从 0 提到 0.2这个增强会把目标从一张图里抠出来贴到另一张图对港口密集停泊场景的正样本扩增效果直接建议别超过 0.5否则贴上来的船和被遮挡的船会产生大量语义冲突。mosaic 保持 1.0它是 yolov5 最强的增强手段小目标数据尤其依赖它不要关。4.4 迁移学习与冻结 backbone数据少时保命的手段如果数据集规模只有几百张直接全量微调容易过拟合验证 mAP 高开低走。先冻结 backbone 训一批再解冻全量微调是数据少时的标准操作。python train.py \ --data data.yaml \ --weights yolov5s.pt \ --img 1280 \ --batch 16 \ --epochs 50 \ --freeze 10--freeze 10 表示冻结模型前 10 个模块yolov5s 的 backbone 大约覆盖 model.0 到 model.9正好锁死。这个阶段只训练 head显存占用少lr 维持 0.005。50 个 epoch 后看 val loss 是否进入平台期然后去掉 --freeze 10 全量再跑 80 到 100 个 epoch让 backbone 特征参与微调。这里有条血泪经验冻结阶段 val loss 不降反升大概率不是参数问题是数据划分里验证集混进了和训练集高度相似的连续帧具体排查放到第 5 章。5. 避坑手册舰船检测实战中的五个翻车现场这一章按翻车现场走每条都按现象、原因、解决三段式写清楚。5.1 训练 loss 不降先查标签不查网络现象train loss 开头就很高几十个 epoch 几乎不动val loss 同步高企mAP 停在个位数。 原因排序标签类别 id 与 data.yaml 不一致、标签归一化坐标全部越界、txt 里有空行或非法列数。不要先怀疑模型结构yolov5 默认配置在正常标签下极少出现这种问题。排查方法很笨但管用随机挑 3 个训练 batch 的可视化图如果框全部偏到图外或角上就是标签问题。 解决先扫描 labels 目录里每个 txt 的合法性。awk {if(NF!5){print 列数错误:, FILENAME, $0}; for(i1;i5;i){if($i0||$i1){print 越界:, FILENAME, $0}}} labels/*.txtawk 一行命令把所有不合法的行打出来。然后回到第 3 章确认 CLASSES 顺序重新生成 labels。注意重跑前先清空旧 labels不然旧的错误文件不会被覆盖。5.2 港口船挤船anchor 聚类与 NMS 的连锁反应现象并排停靠的密集船只漏检一半以上有时一个框里塞了两三条船。 原因默认 anchor 来自 COCO 多类别统计值对舰船这种宽高比偏长的目标不合适推理时 NMS 在拥挤场景里相互抑制置信度稍低的正确框被高置信度相邻框压掉。 解决训练阶段不要手工微调 anchoryolov5 会在训练开始时自动对标签做 k-means 聚类并重算 anchor前提是别加 --noautoanchor。验证阶段把 conf-thres 从默认 0.25 调到 0.15~0.2iou-thres 从 0.45 调到 0.3拥挤场景会少丢很多正确框。这个组合听起来像玄学实测在密集停泊图上 mAP 能涨 3 到 5 个点。5.3 远处小船召回率低imgsz 和 mosaic 的取舍现象近处大船全部检出远处十几像素的小船一个不剩图上肉眼能看见。 原因训练输入 640 时小船在输入图中只剩 3~4 个像素P3 层即便是 8 倍下采样也会丢掉目标信息这是物理极限不是模型问题。 解决imgsz 提到 1280mosaic 保留 1.0 但确认增强没有把小船裁出边界。还有一条实务技巧如果 imgsz 1280 显存不够先降到 1024然后开 --multi-scale 随机多尺度训练让模型在 640 到 1280 之间随机伸缩远船召回能撑住一部分。实测从 640 升到 1280小目标召回率涨 7 到 11 个点代价是训练时间接近翻倍自己权衡。5.4 conda 下环境配置翻车yolov5 版本对齐的坑现象conda 创建好环境装完 requirements 直接训练报错一堆AttributeError、CUDA 版本不匹配、no module named torchvision。 原因yolov5 不同 commit 对应不同 torch/torchvision 组合直接 pip install torch 会拿到最新版新版 torch 的 API 与旧版 yolov5 分支对不上。 解决不要追新版本锁一套稳定组合。conda create -n yolov5 python3.9 -y conda activate yolov5 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118装完先跑最小例子验证环境再动训练python detect.py --weights yolov5s.pt --source data/images/bus.jpg能看到框住 bus 和 person 就说明环境通。别一上来就跑 train.py训练报错信息量太大排查成本高。5.5 验证集混入连续帧mAP 虚高却不实用现象训练时 val mAP 90 分以上部署到视频上表现稀碎。 原因视频抽帧后同一条船的连续帧被随机分到 train 和 val模型在 val 上见到的是训练帧的「下一帧」评估分数虚高。 解决按视频片段分组划分 train/val不按单帧随机切。先用文件名前缀判断是否有连续帧特征连续抽帧时保证每个视频片段整体落在一个集合里。这个检查应该在第 3 章划分脚本之前做不要等训练完才回头。6. 上板验证detect.py、ONNX 导出与树莓派 5 上的最后一百米6.1 detect.py 验证与置信度阈值测试训练完成拿到 runs/ship_train/exp/weights/best.pt做推理验证时有一件事必须做用没见过的视频或照片验证别从训练集抽几张图看效果。命令上我建议打开 --save-txt把原始输出也存下来。python detect.py \ --weights runs/ship_train/exp/weights/best.pt \ --source test_videos/ \ --conf-thres 0.2 \ --iou-thres 0.3 \ --save-txt--conf-thres 0.2 配合第 5 章结论在密集场景验证时更合适--save-txt 会输出每个检测框的类别 id、置信度和归一化坐标方便逐帧统计召回而不是只看渲染出来的框。我习惯拿 30 秒海面视频逐帧过记录漏检帧的时刻反推是目标太小还是遮挡比看静态图直观。6.2 导出 ONNX 并在树莓派 5 上推理部署到树莓派 5 这类边缘设备先导出 ONNX。python export.py \ --weights runs/ship_train/exp/weights/best.pt \ --include onnx \ --opset 12opset 12 与 onnxruntime 兼容性比较好。导出后用 onnxruntime 跑 CPU 推理。import onnxruntime as ort import numpy as np session ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) input_name session.get_inputs()[0].name input_shape session.get_inputs()[0].shape # image 是预处理后的 1280x1280 RGB 数组归一化到 0~1 outputs session.run(None, {input_name: image.astype(np.float32)})输出是一个 1×25200×7 的数组25200 是三个尺度输出总的候选框数7 列分别是 cx、cy、w、h、objectness、class confidence。很多人会问要不要做 NMS 后处理yolov5 的 ONNX 导出默认不带 NMS但我强烈建议别在树莓派上用 Python 写 NMS单帧从几十毫秒拖到几百毫秒处理不过来。真要上板用 onnxruntime 自带的 NMS 算子或者在 PC 上先离线过滤。实话说树莓派 5 上 CPU 推理 1280 输入单帧还是要几百毫秒。做 PTQ 量化能把延迟压到几十毫秒但量化校准集要精心挑我那次拿采好的清晰大船图做校准量化后远处小船直接消失血亏。自那以后每次导出 ONNX 我都强制走一遍混合校准集——近景大船、远景小船、港口密集停泊各占三分之一量化稳了才上板。你如果也遇到小目标消失先别怀疑算子回去换校准集。希望帮到你。本文还有配套的精品资源点击获取
返回列表