去年做港口船舶监测项目的时候,我在数据上整整耗了两周。翻遍了公开数据集,要么是航空俯拍的远海大场景,要么是公园游船特写,真正贴合岸基监控视角的样本少得可怜。后来我把一套3000张的船舶检测数据集按YOLO格式整理干净,跑通了整个训练流程。今天就把这件事从头到尾拆开讲:数据集到底怎么构成的、标注格式有哪些坑、怎么用它把YOLO模型训练出来,以及海事场景下部署前必须想清楚的边界情况。
无论你是刚接触船舶目标检测的学生,还是已经在做海事监控落地的工程师,这份内容都能帮你少走不少弯路。文中的所有步骤我都实际跑过,代码和配置也都是可以直接拿来用的。
1. 海事船舶检测为什么这么缺公开数据集
1.1 通用数据集里的"船"和海事监控里的"船"不是一回事
很多人刚入行时会想:检测船还不简单?COCO数据集里不就有船这个类别吗?真上手后才发现完全不是这么回事。
COCO里的船类实例确实不少,但绝大多数是观光游艇、公园里的脚踏船、码头边停靠的小型帆船,拍摄视角以游客视角和旅游航拍为主,背景是城市海岸线或风景区水面。而海事监测系统的实际输入是岸基固定镜头、船载摄像头、近海无人机等设备拍摄的画面,目标往往只有几十到几百像素,货船、渔船、拖轮在远距离下轮廓模糊,海雾、逆光、浪花又进一步拉低画质。
更麻烦的是类别定义。海事业务里需要区分货船、渔船、客船、拖轮、驳船,这在COCO的单一"boat"类别里根本做不到。我最初的模型把集装箱船识别成"船",把拖轮也识别成"船",下游的航迹跟踪和船舶分类逻辑全都乱了。
1.2 缺数据背后的三个现实原因
公开数据集少,不是大家不愿意做,而是做这件事的门槛确实高。
第一是数据敏感。港口航道、锚地、进出港水域的视频数据,涉及安防和运营信息,港口方、海事部门都不会把原始监控录像公开出来,能公开的也大多是做过脱敏的片段。
第二是标注成本高。海事标注需要标注员认识船型。让普通标注员区分"散货船"和"杂货船",区分"客滚船"和"渡轮",不培训几天根本做不到。而专业标注的人力成本比通用目标检测高出一大截。
第三是场景单一。现有的一些海洋数据集大多集中在某个固定水域,比如新加坡海域、欧洲某港口,摄像头位置固定,船型分布和国内港口的实际情况差异很大。你拿它在国内沿海港口做验证,泛化效果通常不太理想。
1.3 这份数据集的定位和第一印象
这套3000张的船舶检测数据集,主要覆盖的就是国内沿海港口和近海水域常见的场景:港口靠泊、航道航行、锚地停泊、近岸捕捞混行等。图片来自岸基固定摄像头、近岸无人机和巡航艇船载摄像头,分辨率和视角都更接近真实海事监测系统的输入。
第一眼看完之后我的判断是:它不算大,但胜在贴近实际业务。3000张的量级,配合YOLO的预训练权重,足够把一套检测流程从零跑通,并且能训练出一个在特定场景下有实用价值的模型。相比从COCO里硬抠船类再重新标注,省的时间不是一星半点。
2. 3000张数据的构成拆解:场景、类别与标注口径
2.1 图片来源与分辨率规格
数据集的图片以岸基监控和近岸无人机航拍为主,少量来自船载摄像头。分辨率的分布大致是:
| 分辨率 | 占比 | 对应场景 |
|---|---|---|
| 1920×1080 | 约70% | 岸基固定监控、船载摄录 |
| 3840×2160 | 约20% | 近岸无人机航拍 |
| 1280×720及以下 | 约10% | 老旧监控设备、视频抽帧 |
以1920×1080为主的构成比较适合直接用YOLO默认的640×640输入训练。4K图像占的比例不高,但数量足够测试"大图小目标"场景下的检测策略,这部分后面会专门讲。
数据集按train/val/test分成2400张、300张、300张。类别文件为7类船舶,每张图的平均标注实例数约2.8个,也就是说有相当一部分图是单目标或空目标,这对训练模型抑制"无船误报"反而有好处。
2.2 场景分布与天气覆盖
我对场景做了统计,分布大致如下:
| 场景 | 占比 | 典型画面 |
|---|---|---|
| 港口靠泊 | 25% | 货船停靠码头、多船并排 |
| 航道航行 | 25% | 船舶沿航道行驶、尾迹明显 |
| 锚地停泊 | 20% | 多船锚泊、排列松散 |
| 近岸捕捞作业 | 15% | 小型渔船聚集、拖网作业 |
| 进出港密集航道 | 10% | 多船交汇、航向交错 |
| 远海开阔水域 | 5% | 单船或空海面 |
天气覆盖方面,晴天约占40%、多云25%、阴天15%、雨雾天15%、夜间5%。雨雾天气和逆光条件的样本虽然不算多,但正好可以检验模型在恶劣环境下的衰减程度。夜间样本最少,这也是海事监测最容易翻车的场景,后面我会单独说。
2.3 船舶类别定义
7个类别的定义如下:
| 类别ID | 名称 | 说明 | 典型特征 |
|---|---|---|---|
| 0 | cargo | 货船(集装箱船/散货船/杂货船) | 船体大、甲板平坦 |
| 1 | fishing | 渔船 | 中小型、常有作业装置 |
| 2 | passenger | 客船(客滚船/渡轮/观光艇) | 上层建筑明显、航速较快 |
| 3 | tug | 拖轮 | 矮小紧凑、常在港内作业 |
| 4 | sailboat | 帆船 | 有桅杆或帆面 |
| 5 | barge | 驳船 | 无动力、常被拖带 |
| 6 | other | 工作船/执法船/其他 | 不好归类的杂项 |
从数据上看,cargo和fishing占了超过一半,这是国内沿海港口的典型构成。tug和sailboat数量相对少,属于类别不平衡的部分。训练时如果要追求各类别均衡的mAP,可以考虑对少样本类别做过采样,不过那会牺牲一部分整体精度,需要取舍。
2.4 目录结构与标注文件组织
数据集的目录结构是标准的YOLO布局:
marine_ship_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── classes.txt └── data.yamlimages和labels目录一一对应,同一张图片文件名去掉后缀后,在labels下能找到同名txt文件。比如images/train/001234.jpg对应labels/train/001234.txt。classes.txt里按行写了7个类别名,顺序与标注文件中的类别ID一致。这个目录组织形式在YOLOv5/v8里都是通用的,省去重新折腾数据加载的时间。
2.5 标注质量口径
整理数据时我发现,标注质量控制比数量更关键。这套数据集在标注时遵循了几个明确的规则:
- 目标像素尺寸低于15×15的实例不标注,这类目标在640输入下实际上已经退化到2-3个像素,训练时反而会引入噪声。
- 遮挡超过80%的目标不标注,只保留完整可见或大部分可见的目标。
- 海面反光强烈导致轮廓难以辨认的目标,原则上不标,避免标注员主观判断带来的不一致。
- 所有标注框均为水平矩形框。部分靠泊船只存在明显倾斜角度,如果要训练旋转框检测(OBB),就需要自行转换格式。
注意:水平框检测在密集靠泊场景会有明显的框重叠问题。如果下游业务需要区分"哪条船靠在哪条泊位",建议预留OBB旋转框的升级路径。
3. 把标注转成YOLO格式:格式坑与校验方法
3.1 YOLO标注的本质:类别+归一化坐标
拿到原始标注后,最绕不开的一步就是转成YOLO格式。YOLO的txt标注每行对应一个目标,格式是:
class_id x_center y_center width height其中x_center、y_center是目标框中心点的归一化坐标,范围0~1;width、height是归一化宽高。归一化的意思是除以图片本身的宽和高。
举个例子:一张1920×1080的图片上,某个目标的像素坐标是左上角(400, 300)、右下角(800, 600)。那么:
- 宽 = 800 - 400 = 400
- 高 = 600 - 300 = 300
- 中心点x = 400 + 400/2 = 600
- 中心点y = 300 + 300/2 = 450
归一化后:
- x_center = 600 / 1920 = 0.3125
- y_center = 450 / 1080 = 0.4167
- width = 400 / 1920 = 0.2083
- height = 300 / 1080 = 0.2778
对应的标注行是:
0 0.3125 0.4167 0.2083 0.2778这个计算看起来简单,但数据规模一上去,手算显然不现实,必须写脚本批量处理。
3.2 批量转换脚本的编写思路
我用Python写过一个VOC XML转YOLO格式的脚本,核心逻辑是解析XML里的bndbox四个点坐标,然后做归一化。代码如下:
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, img_w, img_h, class_map): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in class_map: continue 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 = x2 - x1 h = y2 - y1 if w <= 0 or h <= 0: continue cx = (x1 + x2) / 2.0 cy = (y1 + y2) / 2.0 cx_norm = max(0.0, min(1.0, cx / img_w)) cy_norm = max(0.0, min(1.0, cy / img_h)) w_norm = max(0.0, min(1.0, w / img_w)) h_norm = max(0.0, min(1.0, h / img_h)) lines.append( f"{class_map[name]} {cx_norm:.6f} {cy_norm:.6f} {w_norm:.6f} {h_norm:.6f}" ) return lines这里有一个关键点:归一化结果要做clip,限制在[0,1]范围内。因为部分标注框边缘紧贴图像边界,四舍五入后可能会变成1.000001这种越界的值,训练时YOLO会直接报"box out of bounds"错误。
如果你拿到的原始标注不是VOC XML而是COCO的JSON,转换思路是类似的:COCO的bbox字段本身就是[x, y, width, height],只是不需要再计算宽高,直接归一化即可。如果是多边形分割标注(segmentation),则需要先计算所有顶点的最小外接矩形,再输出水平框。
3.3 转换后的三道质量检查
转完格式不算完。我的习惯是转换后过三关检查,这套流程救过我很多次:
第一关是脚本检查。写个小脚本遍历所有txt文件,逐行解析五个数值,检查class_id是否在合法范围内、四个坐标值是否在[0,1]区间内、width/height是否大于0。
第二关是漏检检查。遍历txt文件,比对图片文件名,确保每张图都有对应标注文件,且标注文件不为空。如果一张图没有标注文件,训练时YOLO会报"image without labels"警告,但你未必能第一时间定位到是哪张图。
第三关是可视化验证。用OpenCV把标注框画回原图,抽查100张,重点关注:
- 框是否紧贴目标边缘,有没有明显偏移
- 类别标签是否对得上目标外观(比如把客船标成货船)
- 小目标框是否被意外过滤掉
可视化这一步最花时间,但也最值得。我见过一个项目里20%的标注框中心点约偏移了50像素,就是因为转换脚本里把bbox的x和w搞混了。这类问题靠代码逻辑检查发现不了,只能靠肉眼抽检。
4. 基于3000张数据集训练YOLO的完整流程
4.1 数据集划分:别让验证集"作弊"
拿到数据的第一件事不是训练,而是划分数据集。很多人直接对图片列表做随机random.shuffle,这是个隐患很大的做法。
如果图片里有相当一部分是从同一段监控视频里按帧抽取的,随机划分会把同一视频不同帧拆到训练集和验证集。这些帧之间的差异可能只有几百毫秒,画面高度相似,最终验证集mAP会"虚高",让你误以为模型泛化很好,一部署到新场景就现原形。
正确做法是先按摄像头ID或视频片段做分组,同一个来源的帧整体划入同一个集合,再做组级别的随机分配。建议比例是train 80%、val 10%、test 10%,也就是2400/300/300。如果你手里的视频片段特别多,也可以考虑按70/15/15划分,给验证和测试留更充足的多样性。
另外,抽帧间隔也很重要。同一段视频抽帧间隔低于10秒的话,画面基本没变化,等于在训练集里重复了同一张图。这个数据集在做抽帧时已经考虑了间隔问题,但你自己扩充数据时一定要记得这点。
4.2 写一份能一次性跑通的data.yaml
YOLO训练前需要一份data.yaml,内容很简单:
path: /mnt/data/marine_ship_dataset train: images/train val: images/val test: images/test nc: 7 names: 0: cargo 1: fishing 2: passenger 3: tug 4: sailboat 5: barge 6: other这里有两个容易踩的坑。
第一个是path字段。如果你写相对路径,YOLO会把路径拼到当前工作目录下,一旦你在别的目录执行训练命令就会找不到数据。建议直接写绝对路径,省得排查半天。
第二个是names顺序。names列表的顺序必须和classes.txt一致,也必须和标注文件里的class_id一致。我之前在一个项目里把"fishing"和"passenger"的顺序写反了,结果训练完模型把渔船全部识别成客船,指标还看似正常,最后检查names才发现问题。
4.3 模型选型与训练参数设置
3000张的数据量在目标检测里属于中等偏小,选模型时没必要一上来就上最大的。我的建议是:
- 显存8GB左右:用yolov8s,batch 16,imgsz 640
- 显存16GB及以上:用yolov8m,batch 16,imgsz 640,后期可尝试1280
- 不要一上来用yolov8n,海事目标普遍偏小,n模型的特征提取能力在远处小目标上衰减明显
训练命令我建议这样写:
yolo detect train \ data=/mnt/data/marine_ship_dataset/data.yaml \ model=yolov8s.pt \ epochs=200 \ batch=16 \ imgsz=640 \ patience=20 \ device=0几个参数的理解:
model=yolov8s.pt表示加载COCO预训练权重继续训练。这点很重要,海事场景虽然特殊,但"船舶"的底层视觉特征在全世界是通用的,从预训练权重起步收敛速度和最终精度都明显优于从零训练。patience=20是早停参数,连续20个epoch验证集指标没有提升就停止训练。这能帮你省时间,尤其是训练到150个epoch以后。imgsz=640是训练输入尺寸。数据集里有1920×1080的原图,YOLO在训练时会自动缩放,但小目标会因此缩小到几个像素。后面我单独讲如何应对这个问题。
训练过程中如果loss出现NaN,优先检查数据集里有没有损坏的图片(常见原因是下载中断或格式异常)。如果验证集mAP长期在0.2以下不动,先别调参,回去看标注文件和names顺序。
4.4 训练完怎么评估:mAP、PR曲线、混淆矩阵
训练结束后不要只看一个总mAP就完事。YOLO训练完成会自动生成results.png、confusion_matrix.png等图表在runs/detect/train/目录里,我一般按这个顺序看:
第一看mAP@0.5和mAP@0.5:0.95。mAP@0.5是IOU阈值0.5下的平均精度,更贴近"能不能框中目标"的实用判断。mAP@0.5:0.95则从0.5到0.95取十个阈值平均,更严格,也更能反映框的定位精度。
第二看PR曲线(Precision-Recall曲线)。海事监测场景我更看重召回率。漏掉一艘船在港口监控里可能导致严重的下游问题,误报则可以用后续的跟踪逻辑过滤。所以我会选择置信度阈值向高召回一边倾斜,即使牺牲一些精确率。
第三看混淆矩阵。重点关注哪些类别容易互相混。我实际训练中遇到最多的混类是tug和passenger,两种船都是矮小紧凑的上层建筑,远距离下轮廓接近。如果你的业务对这两类区分要求高,需要额外收集这两个类别的特写数据。
注意:验证集mAP高不等于部署效果好。别忘了做一次跨摄像头泛化测试,用你没参与训练的其他摄像头画面去验证模型,才能真正看出泛化能力。
5. 海事监测训练中容易忽略的边界情况
5.1 小目标与远距离:目标只有几十像素怎么办
锚地停泊场景里,远处的货船在1920×1080画面上可能只有40×20像素。YOLO在640输入下,模型下采样32倍,特征图最小一格代表原图32×32像素。40×20的目标在特征图上不到一格半,这几乎等于检测"半个特征点",效果可想而知。
应对手段有三个,按投入产出比排序:
第一是把训练和推理的imgsz提高到1280。代价是显存占用大约翻两倍,训练时间也线性增加。如果你的GPU显存够,这是最省事、收益最明显的方案。
第二是切片推理(tiling)。把1280以上的大图按320或640的步长切成若干patch,分别送进模型推理,再把结果映射回原图坐标。这个方案能显著提升小目标召回,但需要写后处理逻辑,还增加了推理耗时。
第三是用P2检测头。YOLOv8官方有yolov8-p2模型变体,在原始特征图上额外加了一层高分辨率检测头,专门为小目标优化。代价是计算量增加,但比起手工调tiling还是省心不少。
这三种方法可以在同一份数据集上做实验对比。我的经验是:tiling对岸基广角镜头的提升最明显,P2头对无人机航拍的提升更明显,1280训练是万金油但显存压力大。
5.2 天气与光照:逆光、海雾、雨夜
海事数据的标注难度在天气面前都是小事。逆光状态下船身变成一团黑影,海雾把目标和海面融成一片灰白,雨夜的雨线在画面里形成大量纹理噪点。这些环境干扰会让模型的误报率明显上升。
数据增强层面可以做针对性模拟:
- 逆光:在HSV增强中加大对比度变化范围,把部分训练图随机调暗
- 海雾:随机叠加高斯模糊和亮度抬升,模拟"灰蒙蒙"效果
- 雨夜:随机加噪声和亮度降低,注意夜间样本要单独评估
但说实话,增强只是补救手段。如果部署场景里夜间监控是刚需,最可靠的做法还是单独收集夜间数据,哪怕只收集500张,也比靠白天模型硬扛强得多。夜视相机和可见光相机的成像特性差异太大,不是数据增强能拉平的。
5.3 标注噪声:漏标是训练崩溃的隐形原因
标注噪声里最隐蔽、危害最大的是漏标。远距离小船在画面上只有十几个像素,人眼很容易漏掉。如果一张图里80%的船都标了,唯独漏了一条小船,训练时模型就会学到"这条小船是背景",等于把正样本当负样本喂进去。
Mosaic增强让这个问题更严重。YOLO的Mosaic会把4张图拼在一起,如果其中一张图存在漏标目标,拼图后这个目标就出现在新图上但标签缺失,且不同来源的目标相互靠近,模型收到的信号是混乱的。
处理办法是在标注阶段做交叉审核,两张独立的标注结果对比差异,重点检查小目标。训练前再用可视化脚本对每张图的标注框数量做统计,标注数明显偏低的图优先抽查。
6. 数据集扩充与后续进阶方向
6.1 适合海事场景的数据增强组合
YOLO默认的增强策略里,Mosaic和MixUp对绝大多数场景有效。但海事目标有自己的特点,增强时要注意几个调整:
| 增强项 | 建议值 | 说明 |
|---|---|---|
| 水平翻转 | 0.5 | 船舶方向性不影响语义,可用 |
| 垂直翻转 | 0 | 船舶不可能倒着航行,禁用 |
| 旋转 | 小角度±15° | 大角度会让船型高宽比失真 |
| HSV-H | 0.015 | 色相变化要小,避免船体颜色失真 |
| HSV-S | 0.3 | 适当变化饱和度,模拟不同天气 |
| HSV-V | 0.2 | 亮度变化,覆盖逆光场景 |
| 透视 | 0.3 | 模拟不同摄像头安装角度 |
有一点容易被忽略:90度旋转不能随意开。货船是典型的"长条形"目标,旋转90度后宽度变成高度,模型会学到错误的形状语义。无人机航拍场景可以用小角度旋转,但岸基固定摄像头画面请保持水平。
6.2 半自动标注扩充:让模型帮你标
3000张数据跑通流程没问题,但部署到新场景后,最好补充新场景的数据再做一次增量训练。
扩充数据最省钱的方式是半自动标注。用第一轮训练好的模型对新增视频抽帧做预测,选出置信度高于0.7的框,人工只负责修正漏检和错框。实际操作中注意两个问题:
- 别让模型"自己骗自己"。如果抽帧画面的光照条件和训练集差异很大,预测结果置信度普遍偏低,就需要人工全量标注,不要硬用半自动。
- 新增样本的类别分布要和原数据集保持一致。如果新场景里渔船特别多,而原有数据里渔船只占15%,训练时就需要对渔船做加权,防止模型偏科。
我记得有一次扩充数据只录了港口白天的监控,结果模型对傍晚逆光场景的误报率从3%涨到15%。后来补了不同时段的数据做重训才压下来。
6.3 从检测走向跟踪与多模态
检测只是第一步。海事监测系统真正要解决的是目标跟踪和航迹分析。你可以在这份数据集的基础上,配合ByteTrack或DeepSORT做多目标跟踪,在连续帧上关联同一艘船的检测框。这一步能把"检测到的孤立目标"变成"持续稳定的航迹",误报和漏报都能被时间一致性过滤掉一大半。
更进一步,可以把AIS(船舶自动识别系统)数据和视觉检测结果做对齐。AIS提供船舶身份和位置,视觉检测提供实时画面位置,两者融合可以解决目标身份确认问题,也让"报上船名"这件事从查数据库变成实时识别。要做到这一步,检测模型提供准确的类别和位置是基础,分类质量直接决定了上层应用的可靠性。
最后说几句实在话
做这套数据集训练流程的时候,最深的体会是:数据永远比模型更花时间,也永远更值得花时间。3000张图听起来不算多,但它把岸基监控、无人机航拍、不同天气光照这些真实变量都覆盖到了,这份多样性比数量翻倍但场景单一更有价值。
如果你准备拿这份数据集做落地部署,我的建议是先在你的目标水域单独采集2-3天的视频,做少量补充标注,重点覆盖你实际场景的摄像头角度和天气条件,然后和这份数据集混合训练。训练时把验证集比例留足,别被漂亮的mAP骗了,多跑几次跨摄像头测试才是检验模型的硬标准。
另外一个实际的心得:训练中途如果发现某个类别的AP特别拉胯,比如sailboat只有0.1,不要急着堆数据,先去看它的标注框画得对不对。我遇到过好几次,AP低纯粹是因为部分标注框框进了船体周围的水域,框的宽高比严重失真,模型永远学不对这个目标的边界。
这套流程整体跑下来,从拿到数据到产出第一版模型,大约三天时间。后续的扩充、调优和部署还有不少细节,等下次再单独聊聊在Jetson设备上做推理加速和量化的话题。