做海事AI的人,大概都经历过这样一个尴尬阶段:算法模型一堆,一提数据集就沉默。去年我在做港口进出港船只统计项目时,翻遍各大公开数据集,要么是卫星图上的大船,要么是画质很差的监控截图,类别也乱七八糟。最后实在没办法,干脆自己搞了一套3000张的船舶检测数据集,全部按YOLO格式标注,跑出来的效果比预期好很多。今天就把这套数据集的构建思路、标注规范、训练过程和踩坑记录完整分享出来。如果你是做海事监测、港口安防、航道管理或者渔政执法的算法工程师,这套东西应该能帮你省下至少一个月的收集和标注时间。
1. 为什么自己动手做船舶检测数据集
1.1 海事监测场景的真实需求
船舶检测这个任务,听起来很简单,不就是框出画面里的船吗?等你真正落地就会发现问题远没有那么轻松。海面环境复杂,天气变化快,太阳反光、波浪纹理、雾霾、雨雪都会严重影响检测效果。目标船只形态也千差万别:货轮、渔船、快艇、帆船、拖轮,每种船的视角、尺寸、外观差异都很大。
在监控画面里,一个常见的场景是一艘小船出现在视野远处,可能只有几个像素大小,船上的人脸都看不清。传统目标检测模型对这种小目标非常不敏感,很容易漏检。我最初用公开的COCO数据集预训练模型直接测试,对近处的大货轮识别还好,稍微远一点的小渔船几乎全军覆没。所以做海事监测,一个贴合真实场景的船舶专用数据集是刚需。
这套3000张数据集的设计目标非常明确:覆盖内河、近海、港口、锚地四种典型环境,包含不同时间段、不同天气下的船舶样本。类别上不追求细粒度分类,只区分ship和boat两种,因为在实际工程里,更细的船舶类型识别通常需要更高分辨率的图像,而且对后续跟踪和计数任务来说,先稳定检出才是第一优先级。
1.2 数据的来源、筛选与脱敏
数据来源是构建数据集首先要解决的问题。我采用的方式是公共监控视频抽帧 + 在线开放图库筛选 + 自己拍摄补充三类结合。监控视频抽帧能拿到连续帧,船舶的多个角度都能覆盖,但需要处理画面模糊、压缩伪影等问题。开放图库适合补充不同光影条件下的大场景图片。自己拍摄则用于补充特殊场景,比如逆光、大雾、雨天等极端天气。
筛选时我会优先保留船只目标清晰、轮廓完整的图像,删掉那些船体被遮挡超过一半、或者目标过小到连人都难辨认的图片。需要特别说明的是,所有图像都经过脱敏处理,去除船名、标识、人员面部等可以识别个人和具体船只的信息。海事数据涉及航道安全和港口运营,数据合规是第一位的,这一点无论什么时候都不能省。
筛选完成后,我按照7:2:1的比例将数据集划分为训练集、验证集和测试集。训练集2100张,验证集600张,测试集300张。这个比例是经验值,对于3000张量级的数据来说,测试集300张足以给出置信区间比较窄的指标评估,验证集600张也能在训练过程中稳定地反映模型表现。
1.3 类别定义与边界情况
船舶检测数据集的标注质量直接决定模型上限,所以在正式标注之前,先要把类别定义和边界规则定死。我最终只用两个类别:ship和boat。说起来都是船,但这两者在形态特征和应用场景上有明显区别:ship一般指体型大、甲板或上层建筑结构明显的货轮、客轮、油轮、工程船;boat则指体量小、结构和边缘相对简单的快艇、渔船、小木船、皮划艇等。
边界情况是标注中最容易扯皮的部分。比如一艘中型渔轮,既有渔船的特征又有货轮的结构,怎么归类?我的规则是:如果图片中目标成像长度占图像宽度的比例超过15%,且船体上层建筑高度明显,就归为ship,否则归为boat。这个规则不完美,但至少保证了标注一致性,比标注员凭感觉判断要可靠得多。另一个边界是“浪花遮挡”:船只在高速行驶时尾部会有白色浪花,如果浪花区域和船体重合,依然要把整个船体连浪花一起框进标注框,不能只标中间露出来的部分,否则模型会学到错误的形状。
2. 数据集的结构与YOLO格式解析
2.1 目录结构与文件说明
这套数据集整理完以后,目录结构非常清晰,这也是我强烈建议大家保持的做法——把原始图片和标注文件按日期、场景编码分类存放,后面训练时只需要一个脚本就能统一处理,不用东翻西找。
ship_dataset/ ├── images/ │ ├── train/ # 2100张 │ ├── val/ # 600张 │ └── test/ # 300张 ├── labels/ │ ├── train/ # 与images/train同名.txt │ ├── val/ │ └── test/ ├── classes.txt # 类别列表 ├── dataset.yaml # Ultralytics YOLO训练配置 └── README.md # 数据集说明每个标注文件都是一个纯文本文件,文件名与对应的图片文件完全相同,只是扩展名为.txt。classes.txt里一行一个类别名,顺序和标注文件里的类别编号一一对应。dataset.yaml是YOLO训练时直接使用的配置文件,里面指定了训练集、验证集路径和类别数量,整个目录做到自包含,拿到手就能开始训练。
2.2 标注文件里的五个数字怎么读
YOLO格式的标注文件每一行代表一个目标,格式是class x_center y_center width height。后面四个数字是归一化后的坐标值,范围都在0到1之间。比如一行数据0 0.5245 0.6382 0.1264 0.0891,表示类别是0(ship),目标中心点在图片横向52.45%、纵向63.82%的位置,框的宽度占图片宽度的12.64%,高度占图片高度的8.91%。
这里最关键的是归一化坐标系。我见过不少新手直接把像素坐标填进去,训练结果惨不忍睹。YOLO要求坐标除以图片宽高,标注时如果用LabelImg这类工具,保存成YOLO格式时工具会自动转换。如果自己写脚本转换,一定要确认转换公式没有除以2这种低级错误,别问我为什么会知道。
还有一个容易被忽略的点:width和height是bounding box的宽高,不是右下角坐标。如果你从COCO等格式转换过来,COCO存的是左上角和宽高,直接复制会被当成中心点和右下角,画出来的框全是歪的。转换时务必要按x_center = (x_min + x_max) / 2 / width、y_center = (y_min + y_max) / 2 / height、w = (x_max - x_min) / width、h = (y_max - y_min) / height来算。
2.3 数据划分与增强策略
数据增强是让小数据集发挥大作用的关键手段。3000张图在深度学习里体量并不大,如果原封不动丢进训练器,过拟合几乎是必然的。我在训练时用了一组针对海事场景定制的增强策略,在Ultralytics YOLO的配置里直接开启。
基础增强包括:随机水平翻转(概率0.5)、随机缩放(0.5到1.5倍)、随机HSV色调扰动(hue 0.015,saturation 0.7,value 0.4)。海事场景最有用的是旋转增强,因为船只在海面上可能有任意朝向,我设置了旋转角度正负15度。这里不建议开大角度旋转,因为超过15度后船头和船尾的语义特征会变得模糊,反而干扰学习。
马赛克增强也是必开的,Ultralytics默认开启mosaic=1.0。马赛克把四张图拼成一张,小目标的数量在每张训练图上明显变多,对小船检测非常有帮助。不过马赛克增强在训练后期可能会让loss曲线出现抖动,建议如果发现验证集mAP不升反降,可以在后几十轮把mosaic关掉,这个细节后面会细说。
3. 用YOLO训练船舶检测模型的完整过程
3.1 环境准备与YOLO版本选型
YOLO版本现在有好几条路线,我这里选的是Ultralytics YOLOv8,原因很简单:安装简单、配置友好、训练和导出部署一条龙。做工程项目不是做学术实验,能快速验证、稳定迭代最重要。YOLOv8在COCO上预训练过,作为船舶检测的起点,特征提取层已经学会了通用的纹理和形状特征,比自己从零训练强得多。
环境安装我是在PyCharm里搞的。新建项目后创建一个conda虚拟环境,Python版本选3.9,然后直接pip install ultralytics。如果机器是NVIDIA显卡,顺手装好对应版本的CUDA和cuDNN,PyTorch用pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118这种命令装,避免用CPU版本的torch,否则训练速度会慢到怀疑人生。
这里有一个实用建议:代码里不需要手动下载预训练权重,Ultralytics会自动下载。但国内网络环境下载速度不稳定,建议提前把yolov8n.pt或yolov8s.pt下载到项目根目录,训练时直接指定本地路径。我最初用YOLOv8s作为骨干模型,输入分辨率640,batch size 16,在单张RTX 3090上从预训练权重续训,100个epoch大概跑了不到三个小时,速度完全能接受。
3.2 模型配置文件与训练参数调整
训练之前先把dataset.yaml配好,这是很多人容易漏掉的一步。文件内容大概长这样:
path: ../ship_dataset train: images/train val: images/val test: images/test nc: 2 names: 0: ship 1: boat我在训练第一版时直接把epochs设为100,imgsz设为640,batch设为16,其余参数用默认。跑完查看结果,测试集mAP50大概在0.78左右,不算差但没有达到我的要求。接着我做了两轮针对性调整:一是把近海小目标较多的验证集样本单独拿出来分析,发现boat这一类漏检很多;二是在训练脚本里把cls(分类损失权重)从默认值稍微调低,把box(边框回归损失权重)调大,让模型更关注把框“画准”。
调整后的启动命令大概是这样:
yolo train data=dataset.yaml model=yolov8s.pt epochs=120 imgsz=640 batch=16 lr0=0.01 lrf=0.01 momentum=0.937 weight_decay=0.0005 warmup_epochs=3.0 box=8.0 cls=4.0 close_mosaic=10close_mosaic=10表示最后10个epoch关闭马赛克增强。这个参数很实用,因为马赛克生成的拼图样本和真实场景分布有差异,让模型在最后阶段用正常比例的真实图片微调,能有效提升实际场景的鲁棒性。另外我把lr0设为0.01,这是迁移学习里一个比较稳妥的初始学习率,太大容易破坏预训练特征,太小收敛慢。
训练过程中我习惯观察两组曲线:一组是train/loss和val/loss,另一组是metrics/precision、metrics/recall和metrics/mAP50。如果val/loss在后期开始反弹,而train/loss继续下降,这基本就是过拟合的信号,需要提前停止或者加强正则化。我这套数据在100轮左右val/loss降到最低,所以第二轮直接调到120个epoch并开启patience=20早停,训练更省心。
3.3 训练结果评估与指标解读
最终模型在测试集上的表现如下:
| 指标 | 数值 |
|---|---|
| mAP50 | 0.894 |
| mAP50-95 | 0.641 |
| Precision | 0.916 |
| Recall | 0.872 |
| ship mAP50 | 0.925 |
| boat mAP50 | 0.861 |
mAP50是IoU阈值0.5时的平均精度,这个指标更关注“有没有检出目标”;mAP50-95则是在0.5到0.95的多个IoU阈值下求平均,对框的精准度要求更严格。船舶检测落地时,这两个指标都得看。只盯着mAP50看,容易出现框位置偏移很大但依然被判为正样本的情况。
从指标差异能看出典型的“类别不平衡”现象:ship的mAP50比boat高出0.064。原因不复杂,数据集中ship样本数量多、目标大、特征明显,标注质量和框的稳定性也更好。boat样本普遍是小目标,而且很多小渔船和背景中的白色浪花很容易混淆。后面我在标注补充数据时,特意增加了boat的占比,损失函数里的cls权重也从默认值调低到4.0,让模型更重视边框回归。
模型权重导出为ONNX后,在Jetson Orin这类边缘设备上部署时,把推理分辨率设为640、batch size设为1,单帧推理时间能控制在35毫秒左右,基本满足25帧每秒的实时监测需求。如果对实时性要求更高,可以换成yolov8n,精度会掉一些但速度能翻倍,这个取舍要看具体业务场景。
4. 训练途中踩过的坑与排查记录
4.1 小目标漏检:从网络结构和anchors入手
第一版模型在测试集上跑完后,我拿了十几张典型的码头监控画面去做验证,发现最扎眼的问题是远处的小渔船漏检。排查思路不是盲目加训练轮数,而是先看漏检目标的尺寸分布。我写了个脚本统计标注框的面积占比,发现很多boat目标在图像中的像素面积只有二三十个像素,大约是整张图的0.2%以下。
对于这种极小的目标,YOLOv8的检测头在高层特征图上可能已经被下采样得几乎没有响应了。我采用的解决思路是开启多尺度训练,让模型适应不同输入尺寸下目标大小的变化。具体命令是加multi_scale=True,同时将训练尺寸设为[480, 640, 800]随机采样。这样模型在不同特征层都能看到目标的有效信息。
另一个关键是anchor。YOLOv8是anchor-free的检测器,不再像YOLOv5那样需要手工预设anchor尺寸,但输出特征层的设计决定了小目标响应区域。如果小目标依然漏检,可以尝试把imgsz从640调到768或者896。大分辨率直接增加小目标的像素占比,效果立竿见影。代价是训练显存占用翻倍,我后来用梯度累积的方式解决了显存瓶颈。
4.2 水面反光与雾天导致的误检
海事图像里最有迷惑性的不是同类船只,而是水面本身。太阳直射时海面上会出现大片亮白色耀光,这些区域和白色船体的视觉特征高度相似,模型经常会给出高置信度的误检框。我开始时以为只要多标注几张反光图片就能解决,但后来发现治标不治本,因为反光的形态随着太阳角度和波浪变化无穷。
我最后用的办法是“反例标注”,但不是在框里标什么,而是把明显的水面反光、波浪纹理、漂浮物等区域单独收集起来,放进背景图集中。YOLO训练时通过background数据集混入负样本,让模型学会“不响应”这些区域。具体做法是在dataset.yaml里增加一个background路径,把约200张纯背景图放在那里。这招教给过几个朋友,他们反馈都有效。
雾天和雨天的问题相对好处理。数据增强里把HSV的饱和度随机降低,相当于模拟部分雾天效果。同时我在数据集中补充了一批实拍的大雾天气样本,让模型见过真实的雾天场景。最终模型在雨雾天气的漏检率下降了约20个百分点,效果非常明显。所以如果有条件,实拍样本永远是比图像增强更可靠的数据补充方式。
4.3 多尺度与密集场景的处理技巧
港口里最常见的场景之一,是拖轮围着大货轮转,小船离大船非常近,密集遮挡严重;另一个常见场景是远眺锚地,几十艘船密集停在一起,互相挨得很近。这两个场景对NMS(非极大值抑制)和后处理参数非常敏感。
我遇到的一个典型问题是:两个紧挨着的船被模型框成了一个大框。打开标签检查发现,原始标注框之间确实存在0.3左右的IoU重叠,当模型预测出两个高置信度框时,NMS会合并为一个。解决办法有两个:一是训练时降低iou这个NMS阈值,从默认0.7改成0.45,让相邻但不同的目标更容易被保留;二是在数据标注阶段就把密集场景的框尽量画精确,避免一个框把两艘船裹在一起。
密度大的场景建议把imgsz设为800或更高,这样目标间的间隔在像素层面更明显。同时可以结合SAHI(切片辅助推理)框架,对大图先切成小块再分别推理,最后合并结果。这个方法我在一张3840x2160的全景港口图上试过,小船召回率从原来的0.43提升到0.79,属于我这次项目里最值钱的经验之一。
5. 数据集的落地应用与后续扩展
5.1 海事监测场景部署建议
船舶检测模型要真正用于海事监测,不能只停留在训练出权重这一步。我在实际部署中最常用的是RTSP视频流接入方案。用FFmpeg或GStreamer把港口摄像头视频流拉取下来,解码成帧后送入YOLO模型推理,输出带检测框的画面再推送到Web端展示。
部署时硬件根据场景选型:码头岸基摄像头密度高,用一台带GPU的服务器集中推理;海上巡逻船或偏远锚地,用Jetson Orin NX这类边缘设备就近处理,避免无线传输带宽不足和延迟问题。网络结构上我会用onnxruntime或者TensorRT引擎加速,后者在NVIDIA体系下能把推理时间再压缩一半。
在业务逻辑里,检测只是第一步。我会把检测框和跟踪算法绑定,对每条船分配一个固定的轨迹ID,然后用轨迹信息判断进出港方向、停留时长。这套系统跑起来之后,港口调度人员可以实时看到各泊位的占用情况和进港船只是否排队,效果还是相当直观的。
5.2 从检测到跟踪与计数
检测模型输出的每一帧都是独立的,没有时序关联,所以必须加跟踪器才能做船舶计数和轨迹分析。我用的方案是ByteTrack或者BoT-SORT,这两个跟踪器结构简单、对检测质量依赖小。ByteTrack尤其适合船只这类缓慢运动的刚性物体。
跟踪模块接上后,可以做几件实际有用的事:一是进出港计数,在视频画面中画一条虚拟线,当轨迹ID从线的一侧移动到另一侧时计数器加一;二是停留时间统计,通过轨迹ID首次出现到消失的时间差来判断船只是否违停;三是可疑行为预警,比如船只在禁航区停留超过设定阈值时就自动报警。
这套系统的精度很大程度上取决于检测器的召回率。如果检测器在某一帧漏检了目标,跟踪器就会丢失轨迹ID,后续重识别会当成新船导致重复计数。所以我在跟踪器的输入上做了优化:检测置信度阈值从默认的0.25降低到0.15,同时跟踪器参数里把丢失轨迹的遗忘时间拉长到30帧。实测下来,进出港计数准确率从85%提升到96%。
5.3 数据集的迭代维护计划
3000张数据集只是一个起点,海事场景的多样性远不是3000张图能覆盖的。我给自己定了一个数据迭代计划,核心原则是“哪里样本少就补哪里”。当前明显不足的方向包括夜间场景、水下视角、不同类型的工程船,以及冰面或浑浊水域的船只。
每次迭代时我并不会把新数据直接混入旧集,而是先拿新采样的图片跑到现有模型上看特征分布差异。如果大部分新图都能被现有模型以一个较高置信度检测出来,说明这些图对模型的贡献不大;只有检测失败的图才值得标注和加入训练。这个方法叫“难例挖掘”,听起来高大上,做起来其实就是循环:推理、挑错、标注、重训。
数据标注规范也要随迭代不断校验。比如我现在发现ship和boat的划分标准还可以细化,像渔船可以有独立的类别,工程船(挖泥船、起重船)的特征也非常明显。后续如果有精力,我会把类别扩展到6到8个,然后把标注规范整理成一份详细文档,方便协作者接手。目前这套数据集和配套的训练推理代码统一放在一个项目仓库里,配置文件、标注脚本、训练日志全部都能复现,这本身就是比数据更宝贵的资产。
最后再分享一个小技巧:当我准备把模型迁移到新的摄像头场景时,一般不会直接用旧模型去跑,而是会取这个摄像头的一两百帧无标注数据,用训练好的模型跑一遍检测,人工确认误检和漏检情况,再决定是否需要补充标注。很多所谓“新场景不适配”的问题,实际上只是那个位置的视角、光照和船只外观与训练集差异太大,补几十张有代表性的图重训一遍,效果马上就会正常起来。这也正是数据集需要持续维护的意义所在。