YOLO舰船目标检测实战:数据转换、训练调参与部署避坑指南
简介这份资源面向深度学习与计算机视觉方向的学习者和研究者提供一套基于YOLO算法的舰船目标检测完整实现方案可用于海上救援、军事侦察、交通控制等场景下的船只自动识别研究。资源包共60个文件包含55张jpg舰船图像、2个mat数据文件、2个m脚本文件及1个说明文档压缩包约2.33MB其中图像与标注数据用于构建训练集m脚本与mat模型文件对应YOLOv2网络的训练与测试流程。目前已有127人学习下载。读者可借助SSDD舰船目标数据集的多环境样本结合Matlab的矩阵运算、图像处理与深度学习工具箱完成从数据加载、模型训练到检测结果可视化的完整链路并参考脚本中的损失函数选择、优化算法与超参数调整思路快速复现并优化舰船检测模型理解YOLO将检测转化为回归问题的核心机制。1. 舰船目标检测为什么总在靠港和雾天翻车做过海面监控的同行大概率都遇到过这种场景模型在晴天开阔水域跑得挺好一进港口、一起雾、一有逆光检测框就开始乱跳要么把远处的渔船和货轮混为一类要么整片海面漏检。基于 YOLO 的舰船目标检测本质上就是拿 YOLO 系列这套单阶段检测框架去解决海面场景下舰船这一类目标的定位与分类问题。它适合的人群很明确手里有航拍、岸基摄像头或卫星遥感图像想快速搭一套能跑起来、能迭代的检测流水线而不是从零手写两阶段网络的工程师。这个方向真正难的地方不在 YOLO 本身而在数据。舰船目标尺度跨度极大近处一艘船能占半张图远处只剩十几个像素海面背景又高度重复波浪、云影、尾迹都容易干扰。所以整篇文章我会围绕一条主线讲怎么把舰船数据整理成 YOLO 能吃的格式怎么选模型和参数怎么训练以及部署时那些让人后悔没早知道的坑。读完你应该能自己跑通一版基线并知道下一步往哪调。2. 舰船数据从原始标注到 YOLO 格式的完整转换2.1 为什么舰船数据集必须先做尺度分层YOLO 的检测头是在固定网格上做预测的输入分辨率一旦定死小目标的可用特征就非常有限。舰船场景里同一张图可能同时存在靠岸的巨型邮轮和远处的小艇如果直接按默认 640 输入训练小目标基本等于放弃。常见做法是先统计标注框的宽高分布把目标按像素面积分成小、中、大三档再决定输入尺寸和是否启用多尺度训练。我一般会先跑一段统计脚本把每个标注框的宽高和面积打出来看分布再动手。下面这段代码读取 YOLO 格式的标签文件统计归一化宽高并换算成像素面积方便你判断数据集里小目标占比。import os import numpy as np # label_dir 下是 YOLO 格式的 .txt每行: class cx cy w h均已归一化 label_dir labels/train img_w, img_h 1920, 1080 # 换成你数据集的真实分辨率 areas [] for name in os.listdir(label_dir): if not name.endswith(.txt): continue with open(os.path.join(label_dir, name)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue _, cx, cy, w, h map(float, parts) # 归一化宽高还原成像素再算面积 areas.append(w * img_w * h * img_h) areas np.array(areas) print(目标总数:, len(areas)) print(小目标(32*32)占比:, (areas 1024).mean()) print(中目标占比:, ((areas 1024) (areas 9216)).mean()) print(大目标(96*96)占比:, (areas 9216).mean())逻辑说明脚本把归一化的宽高乘回图像分辨率得到真实像素面积再按 COCO 的小中大阈值切分。参数说明img_w、img_h必须和你标注时的原图一致如果数据集里图片分辨率不统一建议先统一缩放再统计否则面积会失真。跑完如果小目标占比超过三成输入尺寸就别低于 960或者考虑切片推理。2.2 用 LabelImg 打标后转 YOLO 格式的边界坑很多人用 LabelImg 打标默认存出来是 Pascal VOC 的 XML而 YOLO 要的是每行class cx cy w h的 txt。转换本身不难坑在边界框越界和类别映射。LabelImg 允许框拖到图像外转成归一化坐标后会出现负数或大于 1 的值YOLO 训练时这类框要么被忽略要么直接报错。转换脚本里必须做裁剪。import xml.etree.ElementTree as ET import os classes [warship, merchant, fishing, other] # 类别顺序必须固定 cls_map {c: i for i, c in enumerate(classes)} def convert(xml_path, out_path, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text if name not in cls_map: continue # 未登记类别直接跳过避免类别错位 bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) # 关键裁剪到图像范围内防止越界框污染训练 x1, x2 max(0, min(x1, img_w)), max(0, min(x2, img_w)) y1, y2 max(0, min(y1, img_h)), max(0, min(y2, img_h)) if x2 - x1 2 or y2 - y1 2: continue # 裁剪后过小的框丢弃 cx (x1 x2) / 2 / img_w cy (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_map[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_path, w) as f: f.write(\n.join(lines))逻辑说明先按固定类别表映射再对坐标做上下界裁剪最后归一化。参数说明classes的顺序一旦确定就不能改训练配置里的names必须和它完全一致否则类别会整体错位。x2-x12这个阈值是经验值太小的框留着只会增加噪声。2.3 数据集划分与配置文件写法转换完还要划分训练验证集并写 YOLO 的 data yaml。舰船数据如果来自连续视频帧千万别随机划分相邻帧高度相似随机分会导致验证集泄漏指标虚高。我一般按采集批次或时间段划分。# ship_data.yaml path: /data/ship_dataset train: images/train val: images/val nc: 4 names: [warship, merchant, fishing, other]逻辑说明path是数据集根目录train、val是相对路径。参数说明nc必须等于类别数names顺序和转换脚本里的classes一一对应。划分比例上舰船场景我通常用 8:2但如果某类样本极少验证集里也要保证每类都有否则 mAP 没法看。3. YOLO 舰船检测的训练配置与损失函数调参3.1 模型选型从 YOLOv5 到 v8 在舰船场景的差别舰船检测选哪个版本不是越新越好。YOLOv5 生态成熟、部署资料多适合快速出基线YOLOv8 的 anchor-free 头对小目标和密集目标更友好但导出和部署链路相对新。我的习惯是如果目标是尽快验证数据质量先用 v5s 跑一版如果小目标漏检严重再换 v8 并配合更大输入。版本检测头小目标表现部署成熟度适用阶段YOLOv5sanchor-based一般高快速基线YOLOv8sanchor-free较好中精度迭代YOLOv8manchor-free好中算力充足时选型时还要看你的硬件。边缘设备上跑参数量直接决定能不能实时别一上来就上 m 或 l。3.2 损失函数里舰船检测最该关注的三个部分YOLO 的损失一般由分类损失、框回归损失和置信度损失组成。舰船场景里框回归损失最值得调因为目标长宽比差异大货轮细长、渔船短粗。CIoU 这类损失会同时考虑重叠面积、中心距离和长宽比对细长目标比单纯 IoU 更稳。如果发现框位置总偏优先检查回归损失和 anchor 匹配而不是盲目加数据。分类损失方面舰船类别间外观相似度高比如军舰和商船在远距离下几乎一样这时可以适当提高分类损失权重或者引入标签平滑避免模型对某一类过度自信。置信度损失则和正负样本比例有关海面背景占绝大多数负样本过多会让模型倾向于压低所有预测表现为整体漏检。3.3 一份能直接跑的训练命令与参数解释下面以 YOLOv8 为例给一条训练命令参数都是舰船场景常用的。yolo detect train \ dataship_data.yaml \ modelyolov8s.pt \ imgsz960 \ epochs150 \ batch16 \ lr00.01 \ lrf0.01 \ mosaic1.0 \ scale0.5 \ patience30 \ device0逻辑说明imgsz960是为了保住小目标mosaic1.0开启马赛克增强提升小目标鲁棒性scale0.5允许随机缩放。参数说明lr0是初始学习率舰船数据量中等时 0.01 比较稳patience30表示 30 轮无提升就早停省时间。batch要根据显存调960 输入下 16 需要至少 12G 显存不够就降到 8。训练过程中重点看三个指标mAP50、mAP50-95和每类的precision/recall。如果整体 mAP 还行但某一类 recall 很低多半是那类样本太少或标注不一致回去补数据比调参有效。4. 舰船检测部署与推理阶段的排查清单4.1 置信度门限和 NMS 怎么设才不误检部署时误检率高十有八九是置信度门限和 NMS 没调好。默认 conf 0.25 在舰船场景往往偏低海面反光、浪花会被当成目标。我一般先把 conf 提到 0.4 到 0.5 看效果再调 IoU 阈值。密集靠港场景里船挨着船NMS 的 IoU 设太高会把相邻船合并设太低又会重复框0.45 到 0.5 是常见起点。from ultralytics import YOLO model YOLO(best.pt) results model.predict( sourcetest.jpg, conf0.45, # 置信度门限海面场景建议 0.4~0.5 iou0.5, # NMS IoU 阈值密集场景可降到 0.45 imgsz960, device0 )逻辑说明conf控制哪些预测保留iou控制重叠框合并。参数说明如果漏检多就降 conf误检多就升 conf如果同一艘船出多个框降 iou。这两个参数要在验证集上扫一遍别凭感觉定。4.2 边缘设备部署舰船模型的显存与速度权衡在边缘设备上部署速度和精度永远在打架。常见做法是先把模型导出成 ONNX 或 TensorRT再用半精度推理。舰船检测输入分辨率高显存占用大头在特征图不是权重。如果显存吃紧优先降 batch 到 1再考虑降输入尺寸但降尺寸会直接伤小目标要权衡。导出 ONNX 的命令很简单yolo export modelbest.pt formatonnx imgsz960 halfTrue逻辑说明halfTrue启用 FP16能省显存提速度。参数说明imgsz必须和训练时一致否则精度会掉。导出后务必用同一批测试图对比 ONNX 和原模型的输出确认没有精度损失再上线。5. 舰船检测避坑与常见问题排查5.1 训练 loss 正常但 mAP 极低现象训练日志里 box loss、cls loss 都在降但验证 mAP 一直上不去。原因多半是类别映射错位或验证集标签路径写错模型学的东西和评估对不上。解决拿一张验证图手动推理把预测类别和标签类别逐一对确认names顺序一致再检查 val 路径下图片和标签是否同名配对。5.2 小目标舰船整片漏检现象近处大船检测正常远处小船一个都检不出。原因输入分辨率不够小目标在深层特征图上已经消失。解决把imgsz提到 1280 或启用切片推理把大图切成带重叠的小块分别检测再合并。切片会成倍增加推理时间只在必要时用。5.3 雾天和逆光下误检暴增现象晴天正常一有雾或逆光就满屏假框。原因训练集缺少这类天气样本模型没见过这种分布。解决补采雾天、逆光、夜间数据做亮度对比度增强必要时在推理前加一个简单的图像预处理做对比度拉伸。别指望靠调 conf 解决分布问题。5.4 相邻船只被合并成一个框现象靠港时两艘并排的船只出一个框。原因NMS 的 IoU 阈值太高把相邻框当重复框删了。解决把iou降到 0.4 到 0.45或者改用 soft-NMS。如果船特别密集考虑换 anchor-free 的 v8 并调小 NMS 阈值。5.5 导出 ONNX 后精度明显下降现象PyTorch 模型正常导出 ONNX 后 mAP 掉好几个点。原因输入尺寸不一致、归一化方式不同或算子不支持导致图被改写。解决确认导出imgsz和训练一致对比预处理是否都是除以 255用 onnxruntime 跑同一张图逐层比对输出。6. 用切片推理把远海小目标召回率拉上来舰船检测里最头疼的就是远海小目标整图推理时它们只有几个像素任何模型都难。我后来固定用的一套办法是切片推理把原图按固定尺寸切块块间留重叠每块单独推理最后把框映射回原图再做一次全局 NMS。这套方法在卫星遥感舰船检测里几乎是标配代价是推理时间成倍增加所以只在对召回率要求高的离线场景用。实现上切片尺寸一般取模型输入尺寸的 1 到 1.5 倍重叠比例 0.2 左右。重叠太小跨块的船会被切断太大重复框多、速度慢。下面是一个最小实现思路。import numpy as np def slice_infer(model, img, tile960, overlap0.2): h, w img.shape[:2] step int(tile * (1 - overlap)) boxes [] for y in range(0, h, step): for x in range(0, w, step): patch img[y:ytile, x:xtile] if patch.shape[0] 32 or patch.shape[1] 32: continue res model.predict(patch, conf0.4, verboseFalse)[0] for b in res.boxes: xyxy b.xyxy[0].cpu().numpy() # 把块内坐标映射回原图 xyxy[0] x xyxy[1] y xyxy[2] x xyxy[3] y boxes.append((*xyxy, float(b.conf), int(b.cls))) return boxes # 之后对 boxes 做一次全局 NMS逻辑说明按步长滑窗切块每块推理后把坐标加回偏移量。参数说明tile建议等于训练输入尺寸overlap0.2 是召回和速度的平衡点。切完一定要做全局 NMS否则跨块目标会重复。验证方法很直接拿一批已知小目标位置的图对比整图推理和切片推理的召回率如果提升不明显说明你的小目标问题不在分辨率而在标注质量。我自己的习惯是任何舰船项目上线前都先跑一遍切片推理的对比实验哪怕最后不用也能知道模型的能力边界在哪。这套流程踩过的坑不少但每次把召回率从六成拉到九成的时候都觉得值。希望帮到你。本文还有配套的精品资源点击获取