做目标检测这些年,我最怕听到的一句话是“场景很特殊”。水上监控就是典型例子,很多项目团队拿通用目标检测模型直接套船舶检测,结果不是漏检就是误检。船舶检测数据集用得不顺手,不是YOLO不行,而是COCO里那几类船远不足以覆盖海事监控的实际情况。这套3000张YOLO海事监测数据集,就是为港口、航道、锚地这类真实监控场景准备的,解决的是船舶目标检测模型从“能出框”到“能告警”的最后一公里问题。无论你是准备入门YOLO的算法工程师,还是已经在做水上安防项目的开发者,这套数据集的构建思路、标注规范和训练调优路径,都值得完整看一遍。
1. 数据集设计的三个关键决策
1.1 为什么是3000张,而不是更多
很多初学者以为检测数据集越大越好,一开口就是“准备搞几万张”。但做真实项目时你会发现,数据量不是越多越好,而是越“对”越好。3000张图像,按每张图平均4到5个目标来算,大约能提供12000到15000个船舶标注框,配合YOLO的预训练权重做迁移学习,已经完全够用。
关键在于海事监控场景的特殊性:船舶目标分布稀疏,很多画面里只有一两艘船,大量背景是水面、码头设施和岸线。如果盲目堆到上万张,图像之间容易高度相似,尤其是监控摄像头固定机位拍摄的画面,不同时间段的重复帧比例非常高。这种“看似量大、实则冗余”的数据,对模型精度的提升非常有限,反而增加标注和清洗成本。
我实际整理这套数据集时做了严格的去重处理。先按图像感知哈希排掉相似度太高的样本,再把同一个机位连续几秒内抓取的画面只保留一张。最终3000张图覆盖的是不同港口、不同时段、不同天气、不同拍摄角度,而不是同一个水面反复截画面。这也给项目复盘时留了个经验:数据集质量的第一指标不是数量,而是场景覆盖度。
1.2 类别设定:六类船舶够不够用
船舶检测的类别定义,不能机械照搬公开数据集。COCO里只有boat这一类,做海事项目时根本没法区分集装箱船和渔船,而不同船型的违法捕捞、禁区闯入等业务判断,恰恰依赖类别信息。这套数据集的标注类别我收敛为六类:cargo_ship(货船)、container_ship(集装箱船)、fishing_ship(渔船)、passenger_ship(客船/渡轮)、tugboat(拖船)、sailboat(帆船)。
为什么定这六类?因为它们是内河和近海监控中出现频率最高的船型,互相之间在外观上有明显区分度,模型学起来比较轻松。类别定太多也不是好事,比如“驳船”和“货船”从俯视监控画面看差别极小,强制分成两类只会让标注团队纠结,训出来的模型也会在两者之间摇摆。类别定太少又会让模型无法支撑业务逻辑。所以六类是我反复权衡后的选择:既能支撑船型识别和告警规则,又不会给标注和训练制造太多噪音。
每类船的数量也不是平均分配的。数据集中集装箱船和货船的图像占比最高,因为港口场景里它们最常见;帆船最少,因为它的训练价值更多在于避免误检。如果一个类别样本太少,模型容易把相似的类别混淆,这时候就要靠数据增强和重复采样来弥补。
1.3 数据划分与目录结构
数据划分遵循一个基本原则:同一场景或同一监控机位的图像,不能同时出现在训练集和验证集中。否则模型在验证集上的分数会虚高,一旦换到新的港口部署,性能立刻打折扣。
我的划分比例是训练集2400张、验证集400张、测试集200张。训练集用于拟合参数,验证集用于早停和选模型,测试集放在最后做一次性的绝缘评估,验证过程中绝对不看测试集结果。实际项目里很多团队把测试集当成“第二验证集”反复看,这会把验证集和测试集的信息不断反馈回训练过程,导致最终评价失真。
目录结构非常简单清晰,用YOLO标准格式组织:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── dataset.yamlimages和labels分开,文件名一一对应,后缀分别为.jpg和.txt。dataset.yaml是YOLO读取数据的关键配置文件,内容如下:
path: /data/maritime/dataset train: images/train val: images/val test: images/test nc: 6 names: 0: cargo_ship 1: container_ship 2: fishing_ship 3: passenger_ship 4: tugboat 5: sailboat这样组织的好处是零转换成本。任何基于YOLOv5/v8/v11的项目拿到这个目录就能直接开训,不用写一堆格式转换脚本。
2. 从原始图像到YOLO标签:标注与清洗
2.1 标注规范:遮挡、截断、小目标怎么处理
标注质量直接决定模型上限。我见过太多项目,模型训练效果差,最后查下来是标签问题:框偏移半个船身、类别标错、小目标漏标。这些问题用再好的模型也救不回来。
这套数据集的标注规范,核心原则是“框要紧贴船体,宁缺勿错”。对完全遮挡、只有极小一部分露出水面、无法判断类别的目标,一律不标,不能靠猜。对截断的船舶,边界框只框住可见部分,不凭经验脑补被遮挡区域。这看起来很简单,但标注员很容易在这个问题上放飞自我。
小目标另有规则。原始图像以1920x1080和1280x720为主,部分高空俯视画面里,一艘船只占几十个像素。我的做法是:只要目标宽度或高度大于等于8像素,就必须标注。低于8像素的目标既看不清类别,也给模型带来大量额外噪声。这个阈值参考了MS COCO对小目标的定义方式,同时又结合海事场景做了调整。实际操作中我们反复抽查标注结果,标准统一比标得快更重要。
2.2 YOLO标签格式速查
YOLO格式的标签是纯文本,每行对应一个目标,格式为:
class_id cx cy w h其中class_id是类别编号,从0开始。cx和cy是边界框中心点的归一化坐标,w和h是边界框宽高的归一化值。注意,YOLO格式的边界框不是两个角点,而是中心点加宽高,坐标归一化到0到1之间,用图像宽高分别做除法。
举个例子,一张1920x1080的图像中,有一艘船,框的左上角在(960, 540)。这里的括号坐标是像素值。边界框宽200像素、高80像素。那么中心点像素坐标是(960+100, 540+40)等于(1060, 580),归一化后的中心点坐标为(1060/1920, 580/1080)约等于(0.5521, 0.5370),归一化宽高为(200/1920, 80/1080)约等于(0.1042, 0.0741)。对应的标签文件内容为:
0 0.5521 0.5370 0.1042 0.0741很多标注工具导出的格式是VOC的XML或者COCO的JSON,转成YOLO格式时主要就是做两类转换:坐标格式转换和类别映射。坐标格式转换的核心就三步:左上角加右下角坐标转成中心点加宽高,再分别除以图像宽度和高度。最容易出错的是忘记归一化,或者分母用了宽高对调,导致标签全部错位。
2.3 一句话说清数据校验脚本
标签转完不等于能用,必须写脚本做全量校验。我每版数据都跑一个校验流程,查四类问题:标签文件和图像文件是否一一对应、标签里是否出现无效类别编号、边界框坐标是否超出图像范围、边界框宽高是否出现负值或零。
校验脚本的关键片段如下:
import os from PIL import Image def check_labels(img_dir, lbl_dir): errors = [] for img_name in os.listdir(img_dir): stem = os.path.splitext(img_name)[0] lbl_path = os.path.join(lbl_dir, stem + ".txt") if not os.path.exists(lbl_path): errors.append(f"missing label for {img_name}") continue with Image.open(os.path.join(img_dir, img_name)) as im: img_w, img_h = im.size with open(lbl_path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split() if len(parts) != 5: errors.append(f"bad line in {lbl_path}: {line}") continue cls = int(parts[0]) cx, cy, bw, bh = map(float, parts[1:]) if cls < 0 or cls > 5: errors.append(f"invalid class in {lbl_path}") if bw <= 0 or bh <= 0: errors.append(f"non-positive box size in {lbl_path}") x1, y1 = (cx - bw / 2) * img_w, (cy - bh / 2) * img_h x2, y2 = (cx + bw / 2) * img_w, (cy + bh / 2) * img_h if x1 < 0 or y1 < 0 or x2 > img_w or y2 > img_h: errors.append(f"box out of bounds in {lbl_path}: {line}") return errors这个脚本不复杂,但能挡住绝大多数低级错误。我强烈建议每个数据集在训练前跑一遍,每次修改标注后也跑一遍。不要依赖标注工具自带的导出校验,实测下来工具只保证格式合法,并不保证标注逻辑正确。
3. 训练前的预处理与数据增强
3.1 图像级清洗:哪些图必须删
有些图像会让模型学到错误的东西,这类图要果断删除。第一类是最低亮度区间内的夜间图,船体轮廓被完全淹没在黑暗中,如果数量不足以单独构成一个夜间场景子集,留着只会让模型把黑暗和“没有船”关联起来。第二类是镜头被水滴、油污、雾气大面积遮挡的图,整个画面糊成一片,正常训练时无法学习到有效特征。第三类是裁剪错误或拼接错误的图像,比如某些监控平台因为编码问题出现画面撕裂。
清洗时我通常用两个辅助工具:OpenCV计算图像的拉普拉斯方差来评估清晰度,方差低于阈值就标记为模糊图;再用直方图统计亮度分布,把整体像素值低于某区间的图筛选出来人工复核。很多人觉得这些图“也许能增加泛化能力”,但我的经验恰恰相反,低质量噪声图在训练时只会拉低均值,还会增加模型在真实场景下的误检率。
3.2 海事场景的增强组合
数据增强不能乱套。通用检测任务里的随机旋转、随机裁剪,在海事场景里需要精细控制。比如随机旋转,陆地上目标方向相对固定,但俯拍的船舶在画面中可以朝任意方向,所以我对每张图随机旋转0到180度,让模型学到的是“任意朝向的船”而不是“朝同一方向的船”。
水面光照变化是另一个难点。同一片水域,清晨低照度、正午强反光、傍晚金黄色调,色温差异极大。我的增强管线里,亮度扰动范围和对比度扰动范围都比通用配置调大了一档。同时保留了马赛克增强,因为船与船靠得很近时,单图训练容易让模型把密集目标当成一个整体,马赛克增强能强迫模型学会区分不同个体。
随机遮挡也有奇效。码头场景里船体经常被岸吊、浮标、栏杆挡住,真实样本量有限。用随机遮挡增强模拟这种局部遮挡,模型对截断目标的鲁棒性提升非常明显。注意遮挡区域不应太大,覆盖目标框的10%到30%比较合适,遮挡太大反而会让模型学到“船被挡住就识别不了”。
3.3 一张高清图拆成多张训练图
针对小目标多的情况,还有一个非常实用的技巧:滑窗切片。把1920x1080的高清图按区域切成四份,每份约960x540,再等比缩放到640x640或1280x1280作为训练图像,同时把落在对应区域的标签坐标换算到新图上。
这样做的好处是,原本一个只有16像素宽的小目标,切片后可能变成64像素以上,模型学起来轻松很多。代价是丢掉了全局上下文信息,比如“远处还有一艘大船的影子”这种线索没了。我的做法是保留原始整图的同时,额外加入一定比例的切片图,让模型既能认清小目标,又不丢宏观场景。
来看切片后的标签坐标换算,假设原始图像宽为W、高为H,切出的左上角坐标为(ox, oy),切片宽高为w_s、h_s,原标签归一化中心点为(cx, cy),宽高为(bw, bh)。那么新中心点坐标的像素值为((cxW - ox)/w_s, (cyH - oy)/h_s),宽度高度则分别用原宽乘原图像宽度再除以切片宽度得到。这套换算逻辑建议直接写进数据增强代码,配合albumentations或ultralytics自带的预处理一起执行。
4. 用YOLOv8把这个数据集跑起来
4.1 从PyCharm开始搭环境
很多刚接触YOLO的同学卡在第一步的环境安装,这里给出一套我常用的路径。用PyCharm新建项目,然后创建虚拟环境。Windows下可以用Anaconda来管理环境,Linux下直接用venv或conda均可。
创建好环境后安装核心依赖,主要就是ultralytics库和对应的深度学习框架。CPU环境装CPU版PyTorch就能跑通推理,但训练3000张图的数据集会非常慢,建议有条件还是配NVIDIA显卡并安装CUDA版的PyTorch。安装完成后先跑一个最简单的验证:
yolo detect predict model=yolov8n.pt source=https://ultralytics.com/images/bus.jpg我在本地的实际操作是,安装完ultralytics后直接运行yolo命令查看版本信息,确认没有报依赖错误,再用一张测试图跑通预测,环境就算OK了。这个环节最容易出问题的其实是PyTorch和CUDA版本不匹配,我的建议是先用英伟达官网的矩阵表查好版本再装,别装完再折腾。
4.2 训练命令与超参数选择
环境就绪后,直接用命令行训练。以YOLOv8s为例,推荐训练命令如下:
yolo detect train \ data=/data/maritime/dataset.yaml \ model=yolov8s.pt \ epochs=200 \ imgsz=1280 \ batch=16 \ lr0=0.005 \ augment=True \ patience=30 \ project=/data/maritime/runs \ name=ship_detect这里的每个超参数都不是随手填的。imgsz=1280是关键中的关键,海事监控里小目标占比高,用640训练的模型对远处小船的召回率很差。把输入分辨率从640提升到1280,小目标检测的mAP提升非常明显。代价是训练速度下降,显存占用上升。我的实测数据是,同一份数据集在单卡RTX 4090上,640分辨率训练一轮大约2分钟,1280分辨率则要到5分钟以上。
batch=16是根据显存定的,如果显存报OOM就降到8或4。我个人不太建议为了强行凑大batch而减小imgsz,因为小目标问题优先级更高。lr0=0.005是从yolov8默认的学习率出发,在自有数据集上做的一个小幅下调。迁移学习场景下,如果数据集不大,过大的学习率容易破坏预训练权重里已经学到的底层特征。epochs给到200配合patience=30做早停,正常情况下模型在100到150轮之间就会收敛,早停能避免后期过拟合。
4.3 训练日志里这几条曲线怎么读
训练过程中要重点看三条loss曲线:box_loss、cls_loss、dfl_loss。box_loss是边界框回归损失,反映预测框和真实框的位置误差;cls_loss是分类损失,反映类别判断是否正确;dfl_loss是Distribution Focal Loss,它让模型对边界框的分布建模更精确,解决的是框的“边缘不清晰”问题,尤其对船舶这种外形不规则的目标比较重要。
判断训练是否正常的核心标准是这几个loss都在持续下降,并且最后趋于平缓。如果训练集loss下降但验证集loss上升,说明模型开始过拟合,这时早停机制会自动触发。如果loss震荡得非常厉害,优先降低学习率或减小batch,不要急着换模型。
训练结束后,runs目录下会生成weights文件夹,里面有两个权重文件:best.pt和last.pt。best.pt是验证集上指标最好的一版,last.pt是训练结束时最后一轮的结果。实际部署用best.pt就够了。
5. 评估指标与调优实践
5.1 别只盯mAP50:看类别AP和PR曲线
训练完成后的第一反应通常是去看mAP。这里要分清楚mAP50和mAP50-95的区别。mAP50衡量的是预测框与真实框的IoU超过0.5时的平均精度,标准相对宽松,适合快速判断模型基本能力。mAP50-95则是把IoU阈值从0.5逐步提升到0.95,取多个阈值下的平均结果,标准严格得多,对边界框精度非常敏感。
海事场景中,我两个指标都会看。mAP50用于确认“船有没有检出来”,mAP50-95用于确认“检测框贴不贴船体”。如果业务只需要判断某区域是否出现船舶,mAP50优先;如果后续还要做船宽估算、航向判定,就必须把mAP50-95提上去。
训练一版以YOLOv8s、imgsz=1280、200轮为配置后,在测试集上得到的参考结果大致如下,你训练时会有浮动,但这个量级是有参考价值的:
| 指标 | 数值 |
|---|---|
| mAP50 | 0.963 |
| mAP50-95 | 0.812 |
| precision | 0.941 |
| recall | 0.932 |
| container_ship AP50 | 0.971 |
| fishing_ship AP50 | 0.947 |
| sailboat AP50 | 0.889 |
可以看到帆船这类样本较少的类别,AP50明显低于集装箱船。这是典型的类别不平衡表现。处理方式包括对少数类做重复采样、在增强时提高少数类的变换概率、或者收集更多该类别图像。单看总mAP容易掩盖这个问题,所以我习惯每次训练后都打印每个类别的AP表格,哪个类低就重点补哪个类的数据。
PR曲线也值得花时间看。如果曲线在召回率较高位置突然掉落,说明模型在“多检”和“漏检”之间的平衡有问题。此时调整置信度阈值可以改善业务指标。比如告警系统更怕漏报,那就把置信度阈值调低一些,让模型把更多低分目标也输出出来,代价是误检会增多;如果更看重准确率,则把阈值调高。
5.2 优先调整的四个方向
模型效果不好,第一反应先别看网络结构。我根据这套数据集的多次实验,总结了四个调整优先级。
第一,数据问题永远优先于模型问题。先检查标签有没有错标和漏标,再检查训练集和验证集的场景分布是否一致。第二,输入分辨率对小目标的收益远大于更换更大的骨干网络。1500多张图的小目标,用yolov8s加1280分辨率,效果比yolov8m加640分辨率好很多,而训练成本反而更低。第三,锚框可以在自定义数据上重新聚类,YOLOv8虽然不用手动设锚框,但自动聚类的结果会随数据分布变化,如果目标普遍偏小,可以检查anchors是否匹配。第四,单一模型不够时再做测试时增强或模型集成,这不是首选方案,会带来额外部署成本。
调优最忌讳一步到位。我习惯一次只改一个变量,用验证集结果说话。把每次实验的参数和结果记到一张表里,对比起来非常直观。很多时候你以为是在调模型,最后发现问题出在数据划分或者预处理,比如图像缩放方式不一致。
5.3 ONNX导出与边缘设备部署
训练只是开始,落地部署才是重头戏。导出ONNX格式的命令非常简单:
yolo export model=/data/maritime/runs/ship_detect/weights/best.pt format=onnx opset=12 simplify=True导出后建议用onnxruntime或者TensorRT做一次前向推理,确认输出shape和预期一致。如果你要在Jetson这类边缘设备上部署,可以进一步把ONNX转成TensorRT的engine文件。Agx Orin这类设备上,FP16推理对1280分辨率输入有很明显的加速,海事的实时监控场景通常需要多路视频并行处理,模型推理延迟控制在几十毫秒级别比较稳。
部署时的输入尺寸要特别注意。训练用的是1280,部署时如果为了省显存换成640,模型精度会明显下滑。我建议部署端的输入尺寸和训练保持一致,除非导出的模型已经做过分辨率适配。另一个容易被忽视的坑是图像归一化方式,训练时的预处理和数据增强管线,在推理端必须原样复现,否则效果对不上。
6. 常见问题速查与避坑笔记
6.1 训练不收敛、loss震荡怎么办
这个问题我在训练过程中遇到过几次,原因通常集中在三处。标签文件有问题是最容易被忽视的,比如类别编号越界、坐标超出图像尺寸、标签和图像文件名对不上。建议先跑一遍前面提到的校验脚本,而不是反复调学习率。其次是学习率设置,迁移学习场景下学习率过大会导致loss剧烈震荡,对YOLO系列模型,从0.001到0.01之间试错比较稳妥。最后是batch大小,batch过小会让BN层统计不稳定,显存允许的前提下,batch至少给到8以上。
如果项目里用了自定义的数据加载逻辑,比如自己写collate_fn或者实时变换,还要检查增强是否把标签的坐标同步变换了。很多新手在这里翻车,图像旋转了但框没跟着旋转,模型看到的标签位置全是错的,loss自然下不去。
6.2 夜间场景漏检严重
夜间船舶检测的难度主要来自三个原因:目标对比度低、监控相机噪点多、灯光反射造成虚假目标。我的处理经验是,在数据集中单独划出一部分夜间样本,增强管线里加入“降低亮度+增加高斯噪声”的组合,让模型见过足够多种类的夜间画面。如果现有夜间样本不够,可以考虑用图像处理手段模拟夜间效果,比如把亮度降到低区间再叠加噪点,这比完全放弃夜间场景强。
另一个经验是,夜间模型的置信度阈值不要定太高。夜间船体的轮廓特征本来就不完整,输出分数普遍低于白天,阈值设到0.25以下能换回不少召回率,代价是误检率升高。可以根据夜间和白天的业务需求分别设置阈值,避免用一个全局阈值两边将就。
6.3 标签错乱与类别不平衡怎么救
标签错乱最典型的表现是:训练集精度很高,测试集精度暴跌。排查方法是随机抽一批训练图像,把边界框和类别名称画出来,人眼过一遍。这一步我每次都会做,看起来费时间,实际上比调参高效得多。如果某一类框普遍画偏,那就是标注规范没执行到位,需要重新校准,不要想着靠模型“学”回来。
类别不平衡的应对手段,可以组合使用。对于帆船这类样本少的类别,我采用了一种就是过采样的方式,在每个epoch里把包含帆船的图像重复读取两到三次,同时限制总训练步数。配合数据增强时对帆船类目标额外做缩放扰动,AP50的提升是可观的。还有个思路是类别加权损失,给少数类更高的分类损失权重。这几招我都试过,最终采用的是过采样加增强的组合,效果最直观且对超参数不敏感。
数据集的3000张图里,最容易出问题的往往不是数量少的类别,而是外观相似的两个类别,比如货船和集装箱船。这时除了补数据,还可以在标注时给它们制定更细的区分规范,比如甲板上是否有集装箱堆叠、干舷高度差异等,减少标注员的主观判断空间。
回到这套3000张YOLO海事监测数据集,我实际用下来最大的感受是:整理数据的过程比训练模型更磨人,但收获也最大。很多人以为YOLO是主菜,数据集只是配菜,实际上在这个任务里完全反了过来。标注规范统一、场景覆盖充分、标签校验干净的数据集,配一个中等规模的YOLO模型就能达到可用状态;反过来,哪怕换更大的模型,标签一塌糊涂也白搭。
我再分享一个小技巧,凡是准备长期迭代的自建数据集,建议给每一个类别单独建一个“疑难样本”文件夹。每次模型在验证集上犯错,就把对应的图归入这类文件夹,下一轮训练前单独分析。这种事情做得多了,你对数据质量和模型能力的判断会越来越准。海事监控场景很复杂,但先把数据这关过了,后面无论是接多模态分析还是做自动告警,都会顺手得多。