ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于YOLOv8的船舶检测系统实战:从数据集到部署的一条龙经验

基于YOLOv8的船舶检测系统实战:从数据集到部署的一条龙经验

谁做船舶检测谁知道,这活看起来就是"对着图片框几个框",真上手才发现海面场景能把人折磨到怀疑人生。我最初在公开的船舶识别数据集上训了一版YOLOv8,验证集mAP到了90%左右,当时还挺得意,结果把模型放到某港口监控风格的实拍视频里,几百米外的船只有几十个像素,船身和波浪反光搅在一起,漏检、误检、一个框框住两条船,什么奇怪问题都出来了。后来我把整条链路重做了一遍——从数据集整理、标注规范、训练参数调优,到改进点选型、Web前端展示、部署加速——最后沉淀成一套可以"一条龙"跑起来的船舶检测系统。这篇文章就不整虚的了,把我在这个项目里真正踩过的坑、试过有效的方案、以及源码使用顺序全部摊开讲,给正在做船舶检测毕设、或者想入门YOLOv8完整项目流程的同学当个参考。

1. 船舶检测不是随便套个YOLO就行:先说清楚场景里的"坑"

1.1 海事场景和COCO场景的四大差异

第一个差异是目标尺度跨度极大。一张1920x1080的港口监控画面里,近处的船舶能占到画面四分之一,远处的船可能长边只有20到30像素。YOLOv8默认的P3、P4、P5特征层主要照顾中大型目标,小目标全靠P3层硬扛,直接开训就会出现"大的很准、小的全漏"这种偏科现象。

第二个差异是背景纹理极其复杂。海面不是干净的蓝色,有波浪反光、有雾霾白噪、有岸边建筑窗格、有浮标和浪花,这些纹理在特征层面跟船的外形很容易混淆。我最早跑出来的误检框里,一大半框的是翻滚的白色浪花和近岸窗户阵列——规律排列的窗户在浅层特征上很像船舷结构。

第三个差异是遮挡密集。港口泊位里船挨着船,船舷和装卸机械交错,航行中两船交会时没有清晰边界。遮挡带来的问题不只是漏检一个目标,而是可能把两条船框成一个框,或者把前景船的特征跟背景船的特征混在一起。

第四个差异是光照和天气不稳定。白天强逆光、夜间灯光反射在水面上、雾天能见度低,同一艘船在不同天气下的外观差异,可能大于不同船型之间的差异。这也是不少船舶检测论文都要做天气增强(雾化模拟、亮度扰动、逆光对比拉伸)的原因。

1.2 这套系统的定位:看得见、数得清、跑得动

标题里写的"一条龙",本质上就是要一个零基础也能跑通的闭环:数据准备好了、能一键训练、有可视化前端展示。所以这套系统的定位不是冲SOTA榜单,而是可交付、可复现、可扩展。

按这个目标,我把整个项目拆成三个模块:

  • 数据集模块:已经整理成COCO格式,目录结构固定,换数据集只需要改一个yaml配置。
  • 训练模块:封装训练入口,把环境检查、参数设置、日志归档、权重导出做成一条命令搞定;同时预留了骨干网络、检测头、损失函数的替换接口,方便做改进实验。
  • 推理与展示模块:接入多种输入源(单张图片、视频文件、RTSP监控流、本地摄像头),后端跑模型,前端画框统计。

这套系统在某个模拟港口监控数据集上完整跑通过,训练的规模大概是两万多张图片、五个船型类别。整体效果能保证近中距离船只稳定检出、远端小目标召回可用,前端页面能实时展示框选和计数。对毕设汇报、作品集展示、或者刚接触目标检测完整流程的同学来说,这个闭环已经够用了。

2. 数据集才是"一键训练"的地基:整理、标注与增强

2.1 数据从哪来:公开数据打底,自采数据补短

说实话,要自己从零攒一套"标注好的船舶检测数据集",最耗人的不是训练,是数据准备。我采用的是三层组合策略:

  • 第一层,用公开的船舶目标检测数据做底子。这类数据内容一般是中远海船只,背景干净、目标完整,用来保证模型见过"标准船"。
  • 第二层,从监控视频、岸边实拍中抽帧补充近岸、遮挡、反光、雾天之类的难例。这些难例才是模型在真实场景里翻车的高发区。
  • 第三层,针对类别失衡做定向补充。海上货运、客运船只样本往往很多,渔船和驳船经常样本严重不足,模型很容易把它们并进其他类别。

采集来的原始图片必须做清洗。模糊帧、画面严重过曝、几乎没有船只的纯海面大图,该删就删。无船图片可以保留一小部分当负样本,但比例不要超过5%,否则训练时正负样本失衡会让模型变"懒",什么都倾向于不检。另外提醒一句,涉及监控实拍的抽帧只适合个人学习验证,不要直接对外发布原始影像。

2.2 标注规范:类别怎么定、边界怎么画、遮挡怎么算

类别划分上,我最后定的是五个:货船、客船、渔船、集装箱船、驳船。类别不是越多越好,两个船型如果形态高度接近,标注分歧会直接变成训练噪声。比如"工程船"和"驳船"在远距离小目标状态下很难区分,硬要拆成两个类,模型学到的只是碰运气。

标注工具用的是X-AnyLabeling和LabelImg这一类的开源标注工具,做水平矩形框标注。虽然水平框表达不了船体角度,但对检测任务足够了。矩形框的边界规则只有一条:紧贴船体可见外轮廓。遮挡问题按可见范围标注,被遮挡超过70%又极小的目标,我一般直接跳过,宁可少一个样本也不给模型一个"半条船"的模糊监督信号。

标注做完必须做一致性抽检。我是先把不同人标注的同批次图片叠在一起看框的位置差异,差异大的类别重新统一标准。这一步非常影响后续训练,很多野路子代码包"怎么改都涨不了点",最后查出来是标注框本身乱飘。

2.3 训练集与验证集的划分逻辑

标注完成之后转成COCO格式:images/目录放图片,annotations/*.json放标注文件。这里有一个很多新手不知道的细节:划分train/val时一定要按"来源场景"切,而不是按"图片序号随机切"。

同一段监控视频相邻帧看起来不同,但背景和光照几乎一样。如果这些帧同时出现在训练集和验证集里,验证指标会虚高,模型真正面对新场景时就露馅。我按"视频来源"做分组,同一个来源的全部帧进同一侧,相当于逼模型学"泛化"而不是学"背题"。

train/val/test的比例我用的8:1:1。这个比例对两万张图规模的数据集够用了。如果后面要做消融实验,val必须固定成同一个集合,不能每次重新划分,否则不同实验的对比基线都不一致,结论完全不可信。

2.4 数据增强策略:哪些涨点、哪些白费

YOLOv8默认开启Mosaic增强,把四张图拼成一张训练。这个增强在大多数目标检测场景下都有帮助,但在船舶这种小目标密集的场景里有个副作用:拼接之后,本来就只有几十像素的小船会变得更小,容易超出网络能表达的范围。我的做法是Mosaic和Copy-Paste组合使用:Mosaic照常开,同时把小目标船只单独抠出来粘贴到其他干净海面上,相当于人工给模型制造更多"小目标样本"。

天气增强很值得做。用亮度扰动模拟白天不同时段的强光,用高斯噪声和模糊模拟雾天,用对比度拉伸模拟逆光。这些增强不会让模型在公开指标上暴涨,但能明显减少实拍场景里的误检。

随机翻转要注意一点:水平翻转对船舶目标来说基本安全,船的朝向在语义上没有严格约束;但做过OCR类的都知道,翻转会破坏文字可读性,如果以后想在船上做船号识别,这部分数据要单独保留不翻转。

3. YOLOv8训练一条龙:环境、脚本与训练状态判读

3.1 环境配置的版本搭配对照表

环境配置是最容易卡住新手的地方。我在项目里测过几套比较稳的组合,直接列出来给参考:

显卡资源推荐模型规模训练batch参考备注
RTX 3090/4090 24GYOLOv8m / 带P2层16显存充足,可以放心加改进模块
RTX 3060 12GYOLOv8s / YOLOv8m8~16注意Mosaic时显存波动大
消费级8G显卡YOLOv8s8开Mosaic建议关了Cache,省显存
无GPU / 云CPU不建议训练-可以纯推理,或租云GPU跑

版本搭配上,我用的是Python 3.10、PyTorch 2.x、Ultralytics 8.x的组合。PyTorch的CUDA版本要跟显卡驱动匹配,装了cuda 12.x的驱动,就可以直接选cu121或cu124版本对应,装错了最常见的报错就是找不到torch.cuda.is_available()。

OpenCV建议用官方预编译版,不要用某些conda渠道的旧版本,否则视频读取和图像编解码会有一些莫名其妙的兼容问题。

3.2 一键训练脚本的构成拆解

所谓"一键训练",不是简单跑一条yolo train命令,而是把训练前后的一堆杂活封装进去。项目里核心的配置文件就一个:

# dataset/ship.yaml path: D:/ship_detection/dataset train: images/train val: images/val test: images/test names: 0: cargo 1: passenger 2: fishing 3: container 4: barge

启动训练的命令也很简单:

python train.py --model yolov8s.pt --data dataset/ship.yaml --epochs 200 --batch 16 --imgsz 640 --device 0

但train.py内部多做几件事:自动检查数据集目录是否存在;自动检测GPU环境;训练结束后自动跑一遍val,并且把最好的权重best.pt导出成ONNX格式。所有日志和权重归档到一个带时间戳的文件夹里,方便不同实验之间对比。

这里有个参数值得单独说:imgsz。默认640对普通目标检测够用,但船舶小目标占比高,我实测把训练分辨率提到1280会有明显收益,代价是显存占用翻倍、训练速度变慢。如果显卡吃紧,可以"训练用640,推理时用1280",也能吃到一部分分辨率红利。

3.3 训练中必须盯住的四个信号

训练不是把命令扔出去就等结果。我每次训练都会盯四个信号:

  • loss曲线的锯齿幅度。如果loss下降不平滑、锯齿特别大,通常是batch太小或学习率偏高,建议先降学习率。
  • 训练loss和val loss的背离趋势。训练loss持续下降、val loss掉头上升,就是过拟合信号。Ultralytics提供了patience早停参数,一般设20到30个epoch就够。
  • P/R曲线的倾向。P高R低说明模型"宁可漏检也不误检",这在船舶场景下通常是目标太小、置信度普遍偏低造成的,光调conf阈值只是治标。
  • 混淆矩阵里类别串扰。比如渔船大量被识别成货船,说明这两个类别在标注层面边界就没有划干净,回数据集改标注的收益比改模型大得多。

3.4 验证集表现好但实拍拉胯,问题出在哪

这种现象其实很常见,原因一般是三个:验证集和训练集来自同一段视频的相邻帧;验证集本身小目标占比偏低;或者实拍场景跟训练数据的分布差异太大。

我后来专门在项目里加了一个"难例集评估"环节。把实拍素材按小目标、近岸遮挡、雾天、夜间四个维度分组,每次训练完都单独看这四个维度下的mAP、召回率,而不是只看一个总均值。这样一来,模型到底在哪类场景上变强、在哪类场景上变弱,一目了然。

4. 70+改进创新点(发刊向)到底怎么选:盘点了值得做的方向和一批伪需求

4.1 改进点的大致分类:骨干/颈/头/损失/训练策略

现在网上流传的各种"全套改进创新点",归类下来无非五类:骨干网络替换、颈部特征融合、检测头改造、损失函数替换、训练策略与数据处理。给个项目里我的分类表:

改进方向代表做法适用场景
骨干轻量化RepViT、MobileOne替代CSPDarknet端侧/实时部署
骨干提精度引入注意力(EMA、CBAM、CA)一般精度提升
颈部特征融合BiFPN、AFPN替代Concat多尺度目标
检测头改造增加P2小目标层、DyHead小目标密集场景
损失函数Wise-IoU、Inner-IoU、NWD遮挡/噪声样本多
训练策略SAHI切片、多尺度训练、Copy-Paste小目标、全景大图

4.2 五个实测有效且改动量小的改进方案

在"70+改进点"的包装下,真正我试下来稳定有效、改动又小的是下面这几个。

第一个是C2f模块融合EMA注意力。把EMA(Efficient Multi-Scale Attention)这种即插即用模块加到C2f的Bottleneck之后,相当于让网络在通道层面自适应增强船体特征、抑制海面纹理特征。结构上基本不用大改,只需要在构建C2f时多传一个分支。代码示意大概是这样的:

class C2f_EMA(nn.Module): def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5): super().__init__() self.c = int(c2 * e) self.cv1 = Conv(c1, 2 * self.c, 1, 1) self.cv2 = Conv((2 + n) * self.c, c2, 1) self.m = nn.ModuleList( Bottleneck(self.c, self.c, shortcut, g, k=3) for _ in range(n) ) self.att = EMA(self.c) # 在拼接之前做注意力增强 def forward(self, x): y = list(self.cv1(x).chunk(2, 1)) y.extend(m(y[-1]) for m in self.m) y[-1] = self.att(y[-1]) return self.cv2(torch.cat(y, 1))

第二个是增加P2小目标检测层。YOLOv8默认从P3开始做检测,P2层是原始分辨率的四分之一,对小目标更友好。加一层之后小目标召回率提升非常明显,代价是显存和推理耗时上涨。这个改动在船舶这种小目标密集的数据上属于性价比较高的选择。

第三个是用Wise-IoU v3替换默认CIoU损失。海面数据里有大量遮挡、模糊样本,它们属于"低质量样本"。CIoU对这种样本一视同仁,反而会把训练带偏;Wise-IoU v3会动态降低离群样本的权重,对海面这种噪声多的场景非常契合。

第四个是SAHI切片推理。把大图切成小块分别推理再合并,等效于给模型戴了个"放大镜"。它本质上不是网络结构的改进,而是推理策略的改进,但对于全景监控大图场景,效果立竿见影。SAHI适合在验证和部署阶段使用,训练阶段不用改任何东西。

第五个是预训练权重的使用策略。船舶跟COCO物体差异比较大,但从COCO预训练权重开始仍然是收敛最快的路径。有一种做法是前10层冻结训练,优先让检测头适配船舶特征,几十个epoch后再解冻全模型微调,这个方法我在小数据量场景下试过,能缓解早期震荡。

4.3 消融实验的正确打开方式

做消融实验最容易犯的错,就是每个改进单独跑一个模型,然后对比"加了A比不加高0.5个点"。放在论文里要让人信服,至少要按这个格式来做:

  • 固定同一个数据集划分,val集合不允许换了;
  • 固定同一种数据增强策略,Mosaic开或者关,所有实验保持一致;
  • 固定训练轮数和输入尺寸,不能一个实验训300轮、另一个训150轮;
  • 至少要报mAP50和mAP50-95两组指标,船舶小目标场景下后者往往低得吓人(因为框很小,轻微偏移就会掉IoU),但只有前者没参考价值;
  • 最好按"小目标/中目标/大目标"分开统计AP,很多改进点只对其中一类有效。

实际项目里"加一个涨一个"经常是假象。有的改进是靠随机种子波动涨的点,有的改进是多个模块组合之后相互耦合。所以最稳妥的做法是先跑三遍基线取平均值,再看改进模块相对平均值是不是真的涨。

4.4 那些看着高级、实际不动点的"创新"

我也见过不少为了创新而创新的做法,提醒一句,这些方向投入产出比很低:

  • 把一堆注意力模块在骨干上串成一长串,网络变慢,精度不涨反降;
  • 不管什么模块都强行换成Transformer结构,忽略小数据集训练不稳定的大前提;
  • 只在公开验证集上刷AP,对实际海面场景没有任何针对性的设计。

"70+改进点"本质上是个排列组合的集合,真正常用的、值得写进论文的,可能就那么几个真正理解了为什么涨点的方案。我的建议是别贪多,把三四个改到位,配上一套完整的数据集和前端展示,这个项目已经能甩开大部分"仓库复刻"作品了。

5. Web前端展示:把模型从命令行搬到浏览器

5.1 确定展示形态:单图上传、视频流、统计看板

一套检测系统如果只在命令行里跑,演示效果大打折扣。我最后做的页面支持三种输入形态:单张图片上传、视频文件/摄像头实时画面、以及简单的船只数量统计看板。对毕设和作品集来说,这三种形态基本覆盖了所有需要展示的场景。

5.2 后端接口设计与视频流推送

后端我用的是FastAPI(Flask也可以,但FastAPI原生支持异步,处理视频流更顺手)。模型在服务启动时加载一次到内存,不能每次请求都重新加载。最简单的单图检测接口长这样:

from fastapi import FastAPI, File, UploadFile import numpy as np import cv2 app = FastAPI() model = load_model("best.onnx") @app.post("/api/detect") async def detect_image(file: UploadFile = File(...)): img = np.frombuffer(await file.read(), np.uint8) img = cv2.imdecode(img, cv2.IMREAD_COLOR) results = model(img, conf=0.25, iou=0.45, imgsz=1280) boxes = [] for r in results: for box in r.boxes: boxes.append({ "bbox": box.xyxy[0].tolist(), "cls": int(box.cls), "conf": float(box.conf) }) return {"count": len(boxes), "boxes": boxes}

视频流推荐MJPEG方式,后端逐帧推理、把标注过的帧编码成JPEG推给浏览器,前端<img>标签直接显示。这个方案对新手最友好,不需要WebSocket也能跑通;缺点是带宽占用高,不适合公网部署,本地演示没问题。摄像头实时画面也可以走同样的逻辑,OpenCV读取本地摄像头返回给前端。

5.3 前端页面的核心逻辑

前端我用的是原生HTML + JavaScript + Canvas,没有上很重的前端框架。页面三段式布局:左侧上传区和设置区,中间检测预览画布,右侧统计面板。

画框用Canvas的strokeRect,类别名用fillText。有一个容易踩的坑:中文字体画在Canvas上可能变成方框,我一开始直接把"货船""客船"这种类别名写在框上,结果全部乱码。解决办法是后端接口返回类别ID(比如0-4),前端维护一张中文映射表,画框的时候查表显示中文,规避了字体问题。

统计面板我用开源图表库画柱状图,显示每类船型在当前画面/当前视频序列里的检出数量。逻辑上很简单,但演示效果很加分。

5.4 导出加速:从pt到ONNX再到TensorRT(可选)

训练完的best.pt先导出ONNX,推理速度会有明显提升,也方便脱离PyTorch环境部署:

yolo export model=best.pt format=onnx imgsz=1280 opset=12 dynamic=True

注意导出ONNX后推理结果跟PyTorch会有微小差异,主要原因是输入预处理(letterbox的缩放填充)必须保持一致。前端或后端推理时,要把图片先等比缩放再填充到1280x1280,不做这一步,框位全是偏的。

如果之后要上服务器做更高吞吐的推理,可以在NVIDIA显卡上再从ONNX转TensorRT。但这部分调优工作不少,对入门项目来说ONNX已经足够。

6. 从训练到上线,踩坑实录与迁移到你自己项目的建议

6.1 三个最容易被新手忽略的部署坑

第一个坑是服务器没有GPU也不做预案。CPU跑YOLOv8s在1280分辨率下单帧推理要几百毫秒到一秒不等,视频流如果逐帧处理会卡成PPT。我的做法是抽帧到5到10FPS,前端显示"检测到第几帧"而不是逐帧追帧率,用户观感上完全可接受。

第二个坑是并发请求直接把显存打爆。模型推理是吃显存的,多个用户同时上传图片,每张图都进模型,显存可能直接OOM。用一个简单的信号量限制同时推理的任务数,剩下的请求排队等。不要图省事不做限流。

第三个坑是导出ONNX后没有统一预处理。前面提到的letterbox缩放填充是最容易被忽略的,很多人在PyTorch里跑得好好的,一换ONNX推理,框全偏了,第一反应是"导出坏了"。实际上只要让ONNX推理和PyTorch推理用同一套letterbox参数,结果基本一致。

6.2 小目标场景下mAP高但实际体验差的原因

很多人做完项目发现验证集mAP还可以,但实拍视频里远处的小船就是检不出来。原因在于mAP是按类别计算的平均精度,不是按目标数量计算的。验证集里如果大船占多数,AP就会被大船拉高,小目标实际表现被平均掉了。

我项目里专门做了一份"难例评估集":只放长边小于32像素的船、近岸遮挡严重的船、雾天低对比度的船、夜间灯光下的船,四类各几十张图。每次训完,看这个集合的召回率比看总mAP有用得多。这也是我想建议所有做目标检测项目的同学养成的习惯——用难例集评估模型,而不是只信一个平均数。

6.3 如何把这条流水线迁移到其他检测任务

这套系统真正值钱的地方不在某一个模型权重,而在流水线本身。我设计时所有路径都是相对路径加统一yaml配置,因此换任务时只需要做几件事:

  • 准备新的COCO/YOLO格式数据集;
  • 修改ship.yaml里的类别名称和数量;
  • 同步修改前端的中文类别名映射表;
  • 按场景隔离重新划分train/val,别直接套用旧划分。

我曾经把这套流水线迁移到一个安全帽检测小任务上,只换了数据集的yaml和类别映射,训练、导出、前端展示全部复用,半天就能跑出新任务的完整演示。这也侧面说明,把数据和训练的边界切干净、把配置外置,是项目能复用的关键。

6.4 源码拿到手之后,按什么顺序跑起来

最后给个实际操作顺序,照着走基本能顺下来:

  1. 先看README.md和目录结构,确认数据集目录和ship.yaml路径一致;
  2. 按环境清单装好Python、PyTorch、Ultralytics、OpenCV,跑一下python train.py --help确认入口能起来;
  3. 直接跑一键训练命令,用默认参数先训10个epoch验证流程通不通;
  4. 拿到runs/detect/下的best.pt,跑推理脚本对测试图片出一版检测结果;
  5. 启动后端服务,浏览器打开前端页面,上传图片/视频验证展示链路。

我自己在实际做完这个项目后最大的体会是:模型训练本身只占整个项目工作量的一小半,真正决定项目质量高低的,是数据集有没有处理干净、评估有没有设置难例集、前端展示有没有把结果讲清楚。至于那些"70+改进点",挑三四个真正理解了原理的去用,比全堆上去有用得多。希望这篇梳理能帮你少走点弯路,把精力花在真正涨点的地方。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进