做船舶检测这个项目,其实起因挺朴素的——某天接到一个水运管理方向的需求,对方希望用视觉手段自动识别航道内过往船只的类别,给调度、监管和统计提供辅助。当时市面上能直接抠来的通用目标检测方案不少,但真正落到“船舶分类识别检测”这个具体场景,边界条件比想象中复杂得多。这篇文章我打算把基于YOLOv8完整搭建一套船舶分类识别检测系统的过程捋清楚,包括数据集怎么准备、模型怎么训练、UI界面怎么接、部署阶段踩了哪些坑。想复现同款项目、或者准备从零做图像识别应用的同学,这套流程可以当参考。
1. 船舶识别检测任务:从需求到技术选型
1.1 船舶检测到底解决什么问题
船舶识别不是一个新问题,但它和通用的行人、车辆检测有明显差异。水面的背景是动态的,受光照、波浪、雾霾影响极大;船只本身又包含很多种形态,从集装箱货轮到小型渔船,长度从几米到几百米都有。传统方案靠人工盯监控屏,费眼且容易漏,尤其在夜间多路视频的场景下,人力完全跟不上。
当时梳理下来,需求方要的核心能力有三块:一是区分画面里有没有船,二是给船打上类别标签,比如货船、渔船、客船、帆船、拖船,三是尽量给出置信度,方便后续系统根据可信程度做人工复核。这三块需求直接决定了项目的技术路线:必须用目标检测模型,而且检测和分类要一步到位,不能先检测再单独分类,那样两套模型串联会引入额外延迟和误差累积。
1.2 为什么最终选择YOLOv8
目标检测的技术路线其实有好几种,经典的两阶段有Faster R-CNN系列,单阶段有YOLO系、SSD、还有基于Transformer的DETR之类。两阶段精度高但慢,DETR收敛慢且对硬件要求偏高,在工业落地场景里,大家更看重的是“接近实时的推理速度”和“能直接在普通显卡甚至CPU上跑”。YOLO系在这块的优势一直很突出,而选择YOLOv8而不是继续用V5,主要看中三点:
第一,YOLOv8是无锚框检测,解耦了分类头和回归头,对形状差异很大的目标比如细长型船只更友好,省去了调锚框尺寸的环节。第二,它自带C2f结构,梯度流更丰富,小目标特征提取能力有提升,这对远处小船的检测很关键。第三是生态成熟,Ultralytics的仓库把训练、验证、导出封装得相当顺手,从拿到环境到跑通第一次训练,周期非常短。
除了模型本身,我也把数据集来源、标注工具、部署方式一起纳入选型判断。如果选纯Transformer方案,数据集规模需要非常大才能体现优势,而船舶类的公开数据不多,前期还得自采自标,YOLOv8对中小规模数据集更友好,迭代起来也快。
1.3 整体架构规划
系统的整体架构我一开始就按闭环来设计,包含数据层、训练层、推理服务层和展示层。数据层负责图片和视频整理、标注、划分数据集;训练层用YOLOv8执行模型训练和验证;推理服务层负责加载权重、执行检测、做后处理和格式转换;展示层就是用户看得见摸得着的UI界面,可以用桌面程序也可以用Web页面。
这套架构的优点是每一层都能独立替换。比如训练层以后想换更轻量的模型,或者推理层想加一个跟踪模块,都不会牵一发而动全身。UI界面我最终选择了桌面端方案,因为现场值班人员更习惯本地软件,不依赖浏览器和服务器资源,部署也简单。
2. 数据准备的完整链路:采集、清洗、标注与增强
2.1 数据集来源与构成
模型能不能用,60%以上取决于数据质量。船舶识别的公开数据集确实存在,但类别分布和真实场景有差距,往往偏重某一类船,或者分辨率太单一。所以我采取了“公开数据预热 + 自采数据校准”的组合策略。
公开部分我筛选了以水域目标检测为核心的几类数据集,主要用来让模型先见到足够多的大样貌变化。自采部分则更贴合实际部署场景:在河边、港口、跨河大桥等位置架设相机,采集不同时间段、不同天气、不同距离下的船舶画面,连续记录一周,形成素材库。这个过程看起来枯燥,但非常必要,因为自采数据能覆盖部署现场的光线角度、水面波纹、船只涂装这些“现场特征”。
数据总量大约整理了八千多张图片,按8:1:1划分训练集、验证集、测试集。反复提醒自己的一点是:划分时必须保证同一艘船在不同帧里的画面不能同时出现在训练集和验证集,否则验证指标会虚高。
2.2 类别定义与样本平衡
类别定义直接决定了系统的实用性。太多类别会让样本数量被稀释,太少又达不到“分类识别”的需求。我最终保留了六类:集装箱货船、散货船、客船、渔船、帆船、拖船。这六类基本覆盖了内河和近海常见船型,也各有辨识特征,比如集装箱船甲板整齐、渔船上层建筑偏尾部且常带吊臂。
样本平衡是绕不开的坑。拍回来的素材里,货船数量明显多于帆船,因为货船航速慢、在画面里停留时间长。如果直接用原始比例训练,模型对帆船和拖船的召回率会很难看。处理办法是数据加权:对于少样本类别,在训练时提高其loss权重,同时用上下翻转、旋转、尺度变化进行针对性扩充。实际操作中,我还在训练轮次中途做了类别分布统计,确认少数类参与训练的频率没有低到离谱。
2.3 标注规范的细节
标注质量直接决定模型上限。这个项目里,我明确了几条标注纪律:遮挡超过50%的目标不标;目标小于32x32像素且肉眼难以判断类别的宁可不标;紧挨着的两艘船,尤其是并靠的渔船和驳船,框边必须紧贴船体轮廓,不能为了省事拉一个大框把两艘船框进去。
标注格式用的是YOLO的txt格式,每行是“类别id 中心点x 中心点y 宽度 高度”,所有坐标都归一化到0到1。这个格式看起来简单,但有个隐蔽问题:如果图片尺寸变了,归一化坐标不用改,可一旦在标注后对图片做了裁剪,坐标体系就全乱了。所以我的流程是先统一把所有图片resize到同尺寸,再做标注,避免后续转换出错。
标注工具我选了一款开源的图形化标注软件,这类工具大同小异,关键在于支持快捷键连续标注,能大幅提升标图效率。八千多张图,单人标完大概用了两周。如果想压缩时间,可以用大型模型先出一版粗糙标注,人工再修正,但涉及类别定义比较细致的场景,我不建议完全依赖自动标注。
2.4 数据增强策略
数据增强不能乱加。船舶场景和通用物体不同,水面有方向性,波纹和倒影的存在导致过度旋转增强会产生不真实样本。我实测下来,水平翻转是最安全的,因为它不破坏船身结构、只是镜像,符合水面场景的实际情况。
再往下是轻度随机旋转,控制在正负15度以内。超过这个范围,船身倾斜得太厉害,会让模型学到“斜着的船”这种假规律。HSV色域增强也有用,主要是应对清晨和傍晚偏暖色、阴天偏灰的情况,实际设置成H偏移较小、S和V偏移中等即可。马赛克增强能提高小目标检测效果,因为四张图拼接出一张大图,相当于变相缩小了目标尺寸,模型被迫去学更精细的特征。但马赛克增强在训练后期要逐渐关掉,否则模型容易对拼接边界产生依赖,我在最后50个epoch把增强强度降到了原来的30%。
3. YOLOv8模型训练:配置文件、参数调优与训练监控
3.1 模型版本的选择
YOLOv8有n、s、m、l、x五个规格,分别对应从轻量到超大的参数量。选择哪个规格,不能只看精度排行,更要看部署环境的算力。项目最终运行在一台配备中等性能显卡的工控机上,同时要保证UI界面操作流畅,所以一开始就不考虑l和x。我在n和s之间做了对比:n版本推理速度极快但小目标召回偏弱,s版本精度提升明显、速度损失可接受。最终训练选的是YOLOv8s,后续推理时把batch重新置为1,实际单帧耗时大约在12到18毫秒,完全能满足实时需求。
规格选定之后,我用官方预训练权重作为初始权重。预训练模型在COCO数据集上见过大量通用物体,迁移到船舶场景能省很多训练时间,尤其是船和“箱子”“车辆”这类形状相似的物体,迁移特征可以直接复用。如果队伍里没有大算力显卡,用预训练权重微调是性价比最高的方案。
3.2 训练参数逐项解读
训练参数的设置,这里很多数值是靠测试得到的,不建议无脑照搬。我的核心配置如下:
- 输入分辨率640x640。这个尺寸兼顾了小目标检测和显存占用。再往上加到960,精度有提升但推理耗时上涨明显,对实时系统不划算。
- epoch设置为200。前100轮loss下降快,后100轮主要用来收敛细节。到第170轮左右验证集的mAP开始周期性震荡,说明学习率需要衰减。
- batch size设定为16。太大会导致单卡显存溢出,太小则梯度不稳定。如果显存不够,优先减batch并同步降低学习率。
- 初始学习率0.01,使用余弦退火调度,最小的学习率设为初始值的十分之一。余弦退火的好处是让loss在训练后期平缓逼近,不容易在极小值附近反复横跳。
- 权重衰减设为0.0005,用于抑制过拟合。样本有几万张,模型参数量几千万,不加约束训练后期很容易在训练集上刷分、验证集上翻车。
- 类别权重我在少样本类上上调到了基准的1.5倍。这个值不是越大越好,太大会导致模型为了强凑少数类而误判多数类。
训练启动的命令其实很简单:
yolo detect train data=ship.yaml model=yolov8s.pt epochs=200 imgsz=640 batch=16 lr0=0.01 cos_lr=True weight_decay=0.0005训练过程中可以随时中断,Ultralytics会保存最近一次的权重文件,恢复训练时直接指定最新的pt文件即可。这个机制在调整参数后重新续跑时救了不少次。
3.3 训练过程监控与中断恢复
训练监控最重要的一项是分辨“欠拟合”和“过拟合”的信号。我每20个epoch手动看一眼验证集上的混淆矩阵,而不是只盯着loss曲线。loss曲线下降到一个平台不代表模型真的学会了,有可能只是训练集的记忆游戏。有一次训练到第120个epoch时训练loss还在下降,验证集mAP反而掉了一个点,这就是典型过拟合信号,我果断停止了训练,回滚到第100轮的权重。
监控工具上,我用了Ultralytics自带的训练日志和TensorBoard两种方式并行。TensorBoard里主要看train/loss、val/loss、metrics/precision、metrics/recall四条曲线。如果precision和recall在训练后期出现此消彼长的剪刀差,通常是置信度阈值没校准或者正负样本不平衡,需要回到数据层面找原因。
另外一个值得记录的细节是:最好把验证集中预测错误的图片单独存到一个文件夹,每轮训练结束后看一眼错误样本的变化趋势。刚开始错误集中在“把波浪反光当船”,中后期基本消失;训练到末尾时错误样本主要是“近距离两船并靠互相遮挡”,这个问题靠调参无法根治,最终通过后处理NMS逻辑做了缓解。
3.4 指标评估与坏例分析
模型训练完,我只看四个指标:Precision、Recall、mAP50、mAP50-95。其中mAP50-95比mAP50更严格,它统计了不同IoU阈值下的平均精度,能反映出框位置的准确性。船舶检测场景里,框位置抖动会影响后续对船舶航向的估算,所以mAP50-95一定不能太低。最终我的验证结果是mAP50大约93%,mAP50-95大约77%,对于六分类的中等规模数据集算是正常偏上水平。
坏例分析阶段,我重点看了三类错误。一是把航标、浮筒、礁石误判成小船,这类错误往往因为标注数据里缺少“负样本”进行约束。解决方法是在背景图片里随机加入部分高相似干扰物并标为背景。二是把远距离货船和客船混淆,因为远处像素太少,两类船的纹理细节全部丢失,只能靠轮廓猜测。解决方向是增加“远距离高分辨率成对数据”,让模型看见同一条船在不同距离下对应的特征差异。三是夜间灯光干扰,有些船的甲板灯在低照度下显得异常明亮,和岸边霓虹灯混杂,导致误检,这要靠数据扩充和图像预处理联合去解决。
4. 从模型权重到可用的UI系统:推理链路与界面设计
4.1 技术栈选择:桌面端还是Web端
模型训练好只是第一步,交付物必须是一个操作界面友好的系统。技术路线我在Flask Web端和PySide6桌面端之间犹豫过。Web端的好处是多人访问方便,服务部署在一台机器上,值班室和办公室都能看;缺点是视频流回传延迟、浏览器兼容性调整、以及并发访问时的资源管理,都需要额外工作量。考虑到实际使用环境通常是值班室里的固定工控机,桌面端更直接,所以我选了PySide6。
PySide6是Qt的Python绑定,信号槽机制对多线程界面控制特别合适。用它写一个检测面板,左侧放视频画面,右侧放参数面板和结果列表,底栏显示FPS和检测计数,样式调整自由度高。和OpenCV的HighGUI相比,PySide6做出来的东西起码像个正经软件,而不是调试窗口。
4.2 核心功能模块设计
UI的功能模块,我按照使用流程拆成了四块:输入模块、检测模块、结果展示模块、数据管理模块。
输入模块支持三种来源:单张图片、视频文件、实时摄像头信号。视频文件用OpenCV逐帧读取,摄像头信号通过设备的索引或者RTSP流地址接入。检测模块封装了一个Detector类,负责加载模型、执行推理、后处理,并向外提供每帧的检测列表。结果展示模块负责在画面上绘制边界框、类别标签、置信度,并在右侧表格里同步更新记录。数据管理模块支持把检测结果导出为CSV或者带标注的视频文件,方便留档。
检测逻辑的代码结构需要提前规划,不能在界面回调里直接写模型推理。我的Detector类大致长这样:
from ultralytics import YOLO class ShipDetector: def __init__(self, weights_path, conf_thres=0.25, iou_thres=0.45): self.model = YOLO(weights_path) self.conf_thres = conf_thres self.iou_thres = iou_thres def detect_frame(self, frame): results = self.model.predict( source=frame, conf=self.conf_thres, iou=self.iou_thres, verbose=False, device="cuda:0" ) detections = [] for box in results[0].boxes: detections.append({ "cls": results[0].names[int(box.cls)], "conf": float(box.conf), "box": [float(v) for v in box.xyxy[0]] }) return detections这样界面层只需要把读到的帧交给detect_frame,拿回列表后绘制即可。模型推理和界面绘制完全解耦,后面想换成ONNX或者TensorRT的推理后端,只需要改Detector内部实现,界面代码不动。
4.3 实时视频流与线程模型
UI卡顿是这类应用最容易翻车的地方。如果直接在界面主线程里做视频读取和模型推理,画面必然一卡一卡,甚至触发Qt的“无响应”警告。解决办法是把视频读取和模型推理放到两个QThread里,界面主线程只负责接收“已经绘制好的帧”并显示。
我的线程结构是:CaptureThread负责从摄像头或视频文件取帧,通过信号发给DetectThread;DetectThread执行模型推理,并把推理结果连同原始帧一起发回主线程;主线程收到后在Qt的QLabel上绘制边界框,并刷新结果表格。这里要注意的是OpenCV的VideoCapture对象不能跨线程传递,它必须在CaptureThread内部创建并循环读取。
还有一个细节是队列容量。如果检测速度跟不上视频帧率,中间队列会无限堆积旧帧,导致延迟越来越大。我用了有界队列,容量设为5,满了就丢最旧的帧。实际体验中,牺牲一小部分帧率换来的实时性,比攒一堆延迟帧更有价值。
4.4 推理加速与模型导出
实时视频流跑起来之后,我进行了两轮加速优化。第一轮是把模型从PyTorch权重导出成ONNX,然后使用ONNX Runtime做推理。ONNX Runtime在保持精度的前提下,比原生PyTorch的推理快20%到40%,这是因为它做了算子融合和内存复用。导出命令非常简单:
yolo export model=runs/train/exp/weights/best.pt format=onnx opset=12导出后注意检查输出节点,YOLOv8的ONNX模型输出的形状是(1, 84, 8400),84的前四位是cx、cy、w、h加上80个COCO类别得分。因为我们是六分类,模型会输出(1, 10, 8400),所以写后处理时要根据实际类别数调整解析逻辑。第二轮优化是把ONNX进一步转成TensorRT的engine文件,在NVIDIA显卡上跑能快更多,但这个环节依赖CUDA和TensorRT版本,环境配置麻烦,只推荐在正式部署时做。
5. 实战中踩过的坑与补救方案
5.1 小目标漏检:一个长尾问题
项目刚跑通时,画面里的大船检测效果很棒,但稍远一点的船经常漏掉。这个问题不是“模型不够准”,而是数据里小目标样本太少了。我统计了训练集的目标框尺寸分布,发现小于64x64像素的目标占比不到15%。模型在训练时大概率没见过多少小目标,自然学不到对应特征。
补救措施从两端入手。数据端,我把远处的船只区域单独裁剪出来放大后并入训练集,相当于给小目标“多拍了特写”;同时把大图中的目标降采样作为小目标样本。模型端,训练时开启多尺度训练,输入尺寸在480到960之间随机波动,让模型适应不同尺度。经过一轮迭代,测试集上小目标召回率提高了近20个点。这个坑本身不可怕,怕的是不看数据分布盲目调参。
5.2 近距离多船互相遮挡
港口里船舶并靠是常态,检测结果经常出现一个框把两条船框住,或者两条船的置信度都偏低。YOLOv8的后处理NMS设计默认是抑制同一区域内的重叠框,但两船重叠时真实语义上是两个目标,nms反而会杀掉一个正确框。
我的处理方案是降低IoU阈值,从默认的0.45调低到0.3,让重叠区域的多个候选框更容易同时保留。另一个辅助做法是给结果列表增加“是否遮挡”的标记,当某个框的置信度在0.4以下且和另一个高置信度框大比例重叠时,系统自动提示人工复核。这个问题我认为没有完美解法,最终得靠部署策略容忍一定误检,再由人工兜底。
5.3 夜间与恶劣天气的表现
一开始训练数据都是白天晴朗天气,部署测试时夜里画面一上,检测率直接掉了一大截。原因很直白:模型没见过夜间的特征分布。夜间船只在探照灯下是高对比度轮廓,背景水面完全黑色,这和白天暗背景下的小目标检测完全不是一回事。
解决方式分两步。第一,晚间专门采集了一部分真实画面加入训练,包括靠灯光的、月光下的、完全无光靠红外补光的。第二,UI界面上增加了图像预处理选项,比如夜间模式自动做直方图均衡化和轻度降噪,让画面细节更明显。这两步都上了以后,夜间检测率才恢复到白天的八成左右。这个案例给我的教训是:数据采集阶段就要把“运行环境的极端情况”当成一等公民来设计,而不是事后补课。
5.4 类别不平衡导致的误判
前面提到过帆船样本少,所以训练时给了额外权重。但权重加过头,模型又变得过度敏感,把一些带白色帆形结构的小艇也误判成帆船。这类问题调试起来很烦,因为它在整体指标上几乎看不出变化,只有在查看错误图片时才会暴露。
我最后的处理技巧是:把误判成帆船的图片单独搜集起来,作为“难例”放进训练集,并在标注时让模型看到这些目标其实是其他类别。难例挖掘在训练这类长尾分布数据的时候非常管用。每训练完一轮,就把验证集上的错误样本回灌到训练集,进行下一轮微调,这种做法比单纯增加权重更有效地纠正模型行为。
6. 项目落地后的扩展方向与个人体会
6.1 从检测到跟踪:为船只轨迹分析做准备
单纯检测每一帧里的船,只能在逻辑上回答“这一帧里有没有船”。想进一步做航迹分析、船舶计数、区域闯入预警,就需要在检测模块之上叠加目标跟踪。现在比较成熟的做法是ByteTrack或DeepSORT。ByteTrack的优势是简单高效,对短期遮挡容忍度好,很适合部署在实时系统里。
我在推理链路里预留了Tracker接口,检测结果会先经过跟踪器,给每个目标分配一个稳定ID,再送到UI展示。这样同一艘船在跨帧时不会因为短暂的检测抖动而丢失身份。做这个扩展的初衷是客户真正关心的是“这艘船从哪里来、往哪里去”,而不仅仅是“这艘船是什么”。
6.2 模型服务化与多路视频接入
现场不只有一路摄像头,往往有四五路甚至十几路。桌面端单机模式处理一路视频没问题,多路接入就涉及算力分配和画面拼接。一种可行的方向是把模型封装成HTTP服务或者用消息队列管理推理任务,各个视频通道把帧推送到服务中心,服务中心统一调配GPU资源,返回结果后再分发给前端。
我建议不要一次性追求过于复杂的服务架构,先按“一路一进程”的方式部署,用GPU显存占用情况和单卡推理延迟决定最多能并行跑几路。等系统稳定了,再考虑引入批处理推理,把多路视频帧合并成batch一次推理,吞吐量提升非常可观。
6.3 写到最后的一些体会
这个项目从需求梳理到系统交付,前前后后反复迭代了差不多两个月。最大的体会是:模型训练在项目里占的时间其实没有想象中那么多,真正耗时的是数据采集、数据标注、界面联调和现场部署。YOLOv8把训练门槛压得很低,但把“能用”变成“好用”靠的还是围绕场景做的工程工作。
如果让我给准备做类似项目的人三句话,第一句是先花一周把数据分布摸清楚,比先跑十版模型有用得多;第二句是UI和推理一定要解耦,否则后面加功能会改到怀疑人生;第三句是预留难例挖掘的机制,因为模型上线之后,最让你头疼的问题一定来自那些你没想过的情况。这套流程沉淀下来以后,换一个检测对象再做一遍,会发现大部分环节都能复用,无非是数据变了、类别变了、界面文案变了,方法论是通用的。