☰
YOLOv7船舶检测实战:从数据集标注到ONNX部署全流程
2026/9/26 2:12:10 网站建设 项目流程

简介:这份资源面向目标检测学习者与船舶检测方向的开发者,提供一套基于YOLOv7的完整船舶检测方案,帮助快速搭建并训练自己的舰船检测系统。压缩包共2377个文件,约247.63MB,以772张jpg图像、742个xml标注、741个txt标签为主,另含31个py脚本、36个yaml配置、14个ipynb笔记本及2个pt权重文件,覆盖数据、代码与训练配置。数据集同时提供VOC与YOLO两种标签格式,可按需选用;训练曲线与TensorBoard事件文件便于观察loss和mAP变化,评估收敛情况并调参优化。资源还包含训练脚本与预训练权重,可直接微调以适应船舶检测任务,并附有TensorRT、ONNXRuntime等推理对比笔记,方便理解部署与性能差异。目前已有1057人学习下载,适合希望深入YOLOv7模型或快速落地船舶检测实践的研究人员与开发者参考。

1. 船舶检测为什么值得用 YOLOv7 重做一遍

港口的监控摄像头每天产生海量视频,靠人盯着屏幕找船、数船、判断船型,既不现实也不可靠。船舶检测要解决的就是这个问题:给定一张航拍或岸基摄像头拍到的画面,自动框出每一艘船的位置,并给出类别——货船、渔船、客船、快艇,甚至更细的型号。这件事在智慧港口、航道监管、渔业执法、海事搜救里都是刚需。

YOLOv7 在 2022 年发布时,在 5 FPS 到 160 FPS 的范围内都做到了当时的最优精度与速度平衡,尤其是 YOLOv7-E6E 在 56.8% AP 的同时还能跑到 56 FPS(V100),这个性价比对船舶检测这种「目标尺度差异极大、背景干扰强」的场景非常合适。大船在画面里可能占几百像素,远处的小渔船只有十几个像素,YOLOv7 的多尺度特征融合和 E-ELAN 结构对这类尺度跨度大的任务比早期版本更稳。

这篇文章面向的是想自己跑通一套船舶检测系统的工程师:手里有或者能搞到船舶图片,想用 YOLOv7 训练、评估、推理,最后部署到实际业务里。我会把数据集怎么准备、代码怎么改、参数怎么调、坑在哪里,按我自己做过的顺序讲清楚。你不需要先看完 YOLOv7 论文,跟着走就能跑出第一个能用的模型。

2. 数据集从哪来、怎么标注、怎么转成 YOLOv7 能吃的格式

2.1 船舶检测公开数据集与自采数据的取舍

做船舶检测,第一步永远是数据。公开数据集里比较常用的有几个方向:一是航拍视角的船舶数据,画面俯视、背景是水面,目标密集但尺度相对均匀;二是岸基监控视角,透视明显、近大远小,远处小船只有几个像素;三是卫星遥感船舶数据,分辨率高但标注成本也高。公开数据能让你快速起步,但类别定义往往和你的业务对不上——比如你关心渔船和货船,公开数据可能只标了「ship」一类。

我的建议是:先用公开数据跑通流程,验证代码和训练配置没问题,再逐步替换成自己的业务数据。自采数据要注意几点:同一艘船在不同帧里不要重复标注成多个样本,否则模型会学到「船总是成对出现」这种假规律;阴天、逆光、雨雾天气的样本要刻意补,否则模型一到坏天气就翻车;正负样本比例要控制,纯水面无船的负样本占 10% 到 20% 比较合适,太少会导致误检率高。

2.2 标注规范:类别定义和边界框的四个边界坑

标注工具用 LabelImg 或 CVAT 都行,导出格式选 YOLO 格式(每张图一个 txt,每行是class_id x_center y_center width height,全部归一化到 0 到 1)。这里有几个我踩过的坑:

第一,类别定义要一次定死。不要今天把「货船」标成 0,明天又觉得应该拆成「集装箱船」和「散货船」。类别体系一变,所有标注都要返工。第二,边界框要贴紧目标外轮廓,但不要把桅杆、天线这种细长结构单独框进去,否则框会变得又高又窄,训练时回归不稳定。第三,被遮挡的船要不要标?如果遮挡超过 50%,建议标成「困难样本」或者直接不标,否则模型会学到不完整的形状。第四,小目标不要漏标。远处的小船如果肉眼能分辨,就一定要标,漏标比错标危害更大,因为模型会把「没标的小船」当成背景来学。

2.3 用脚本把 VOC 格式转成 YOLO 格式并划分训练集

如果你拿到的数据是 VOC 格式(XML 标注),需要转成 YOLO 格式。下面这个脚本我用了很多次,处理船舶数据没问题:

import os import xml.etree.ElementTree as ET import random from pathlib import Path # 类别映射,根据你的数据集改 CLASS_MAP = {"ship": 0, "boat": 1, "warship": 2} def voc_to_yolo(xml_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.strip() if name not in CLASS_MAP: continue cls_id = CLASS_MAP[name] bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 归一化并转成中心点+宽高 x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 裁剪到 0-1,防止标注越界 x_center = min(max(x_center, 0), 1) y_center = min(max(y_center, 0), 1) w = min(max(w, 0), 1) h = min(max(h, 0), 1) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") return lines def convert_dataset(xml_dir, img_dir, out_img_dir, out_lbl_dir, val_ratio=0.2): xml_dir = Path(xml_dir) img_dir = Path(img_dir) out_img_dir = Path(out_img_dir) out_lbl_dir = Path(out_lbl_dir) out_img_dir.mkdir(parents=True, exist_ok=True) out_lbl_dir.mkdir(parents=True, exist_ok=True) all_files = list(xml_dir.glob("*.xml")) random.shuffle(all_files) val_count = int(len(all_files) * val_ratio) val_files = set(all_files[:val_count]) for xml_file in all_files: img_file = img_dir / (xml_file.stem + ".jpg") if not img_file.exists(): continue # 用 PIL 读尺寸,避免依赖 OpenCV from PIL import Image with Image.open(img_file) as im: img_w, img_h = im.size lines = voc_to_yolo(xml_file, img_w, img_h) if not lines: continue split = "val" if xml_file in val_files else "train" # 复制图片 import shutil shutil.copy(img_file, out_img_dir / f"{split}_{img_file.name}") # 写标签 with open(out_lbl_dir / f"{split}_{xml_file.stem}.txt", "w") as f: f.write("\n".join(lines)) if __name__ == "__main__": convert_dataset( xml_dir="data/annotations", img_dir="data/images", out_img_dir="dataset/images", out_lbl_dir="dataset/labels", val_ratio=0.2 )

这段代码的逻辑是:遍历 XML 文件,解析每个 object 的类别和边界框,把 VOC 的左上角+右下角坐标转成 YOLO 的中心点+宽高并归一化。CLASS_MAP必须和你的标注类别一致,多一个少一个都会导致类别错位。val_ratio控制验证集比例,船舶数据一般 0.15 到 0.2 就够了,数据量特别大可以降到 0.1。注意代码里做了坐标裁剪,因为有些标注会超出图片边界,不裁剪的话训练时会出现 loss 为 nan 的玄学问题。

转换完成后,目录结构应该是dataset/images/train_xxx.jpg和dataset/labels/train_xxx.txt一一对应。然后写一个ship.yaml:

path: ./dataset train: images val: images nc: 3 names: ['ship', 'boat', 'warship']

这里train和val都写images是因为我在文件名里用train_和val_前缀区分了,YOLOv7 的 dataloader 会自己按前缀找。如果你习惯用 ImageSets 那种划分方式,也可以改成train: images/train和val: images/val,但目录要对应建好。

3. YOLOv7 训练船舶检测模型的完整命令与参数调优

3.1 环境搭建与代码拉取后的第一件事

YOLOv7 官方代码依赖 PyTorch、torchvision、numpy、opencv-python 等。我一般用 conda 建一个干净环境:

conda create -n yolov7_ship python=3.9 -y conda activate yolov7_ship pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 --extra-index-url https://download.pytorch.org/whl/cu113 pip install -r requirements.txt

版本不用完全照抄,但 PyTorch 和 CUDA 要匹配你的显卡驱动。装完之后先跑一次python test.py --weights yolov7.pt --source inference/images验证环境没问题,再动船舶数据。这一步能帮你排除掉 80% 的「训练报错其实是环境问题」。

3.2 训练命令拆解:从 yolov7.pt 微调还是从头训

船舶检测我强烈建议从预训练权重微调,不要从头训。YOLOv7 在 COCO 上已经学到了通用的边缘、纹理、形状特征,船舶数据通常几千到几万张,从头训收敛慢且容易过拟合。命令如下:

python train.py \ --weights yolov7.pt \ --cfg cfg/training/yolov7.yaml \ --data data/ship.yaml \ --hyp data/hyp.scratch.p5.yaml \ --epochs 150 \ --batch-size 16 \ --img-size 640 640 \ --device 0 \ --workers 8 \ --name ship_yolov7 \ --project runs/train

--weights yolov7.pt加载预训练权重,--cfg指定模型结构,--data指向你的 ship.yaml,--hyp是超参数文件。--img-size船舶检测建议 640 起步,如果小目标多可以上 1280,但显存要够。--batch-size根据显存调,16 在 12G 显存上跑 640 没问题。--workers是 dataloader 线程数,设成 CPU 核数的 1/4 到 1/2。

训练开始后重点看几个指标:box_loss应该稳定下降,如果震荡剧烈说明学习率太大;obj_loss反映目标置信度,船舶数据背景干净的话这个值会降得比较快;mAP@0.5在验证集上应该逐步上升,如果训练集 mAP 涨但验证集不涨,就是过拟合了。

3.3 船舶场景下必须调的四个超参数

hyp.scratch.p5.yaml里有几十个参数,但船舶检测真正需要动的是这几个:

参数默认值船舶场景建议原因
lr00.010.005 到 0.01微调时学习率太大容易破坏预训练特征
lrf0.20.1 到 0.2最终学习率,太小收敛慢,太大不稳定
anchor_t4.03.5 到 4.0船舶长宽比差异大,放宽匹配阈值
mosaic1.00.5 到 1.0小目标多时保留 mosaic,大目标为主可降低

lr0是初始学习率,微调场景我一般设 0.005,比从头训小一半。anchor_t控制 anchor 匹配的正样本阈值,船舶的宽高比从 1:1 到 1:8 都有,放宽一点能让更多 anchor 参与回归。mosaic数据增强对船舶检测帮助很大,因为它能把四张图拼在一起,变相增加小目标的数量和背景多样性,但如果你的数据里大船占绝对多数,mosaic 会让大船被裁切,反而有害。

另外--img-size如果设成 1280,--batch-size要相应降到 4 或 8,否则显存溢出。YOLOv7 的--img-size支持矩形推理,但训练时最好用正方形,避免长宽比失真。

4. 推理、评估与部署:把模型变成能用的检测服务

4.1 用 test.py 做批量推理和结果可视化

训练完在runs/train/ship_yolov7/weights/下会有best.pt和last.pt。best.pt是验证集 mAP 最高的,last.pt是最后一轮的。推理命令:

python detect.py \ --weights runs/train/ship_yolov7/weights/best.pt \ --source data/test_images \ --img-size 640 \ --conf-thres 0.25 \ --iou-thres 0.45 \ --device 0 \ --save-txt \ --project runs/detect

--conf-thres是置信度阈值,船舶检测我一般设 0.25,因为水面背景相对干净,低一点能召回更多小船。--iou-thres是 NMS 的 IoU 阈值,0.45 是通用值,如果船密集停靠可以降到 0.4 减少漏检。--save-txt会把检测框保存成 txt,方便后续做计数或轨迹关联。

4.2 评估指标怎么看:mAP、Recall 和船舶场景的取舍

YOLOv7 训练日志里会输出mAP@0.5、mAP@0.5:0.95、precision、recall。船舶检测里mAP@0.5到 0.85 以上算不错,但更重要的是recall——漏掉一艘船的代价可能比误检一艘船高得多。如果 recall 偏低,优先检查小目标标注是否充分,其次考虑提高输入分辨率或降低conf-thres。

还有一个容易被忽略的指标是不同尺度下的 AP。YOLOv7 的验证脚本会按 small/medium/large 分别统计,如果 small 的 AP 明显低于 large,说明小目标检测是短板,需要补小目标数据或者用--img-size 1280重训。

4.3 导出 ONNX 并用 onnxruntime 做推理

部署时不一定有 PyTorch 环境,导出 ONNX 是常见做法:

python export.py --weights runs/train/ship_yolov7/weights/best.pt --grid --end2end --simplify --img-size 640 640

--grid把网格解码放进模型,--end2end把 NMS 也导出,--simplify用 onnx-simplifier 简化计算图。导出后用 onnxruntime 推理:

import onnxruntime as ort import cv2 import numpy as np sess = ort.InferenceSession("best.onnx", providers=["CUDAExecutionProvider"]) img = cv2.imread("test.jpg") img = cv2.resize(img, (640, 640)) img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW img = np.ascontiguousarray(img, dtype=np.float32) / 255.0 img = img[None, ...] # 加 batch 维度 outputs = sess.run(None, {sess.get_inputs()[0].name: img}) # outputs[0] 形状是 [1, 25200, 7],前四个是 xywh,第五个是 obj,后面是类别

ONNX 推理比 PyTorch 快 20% 到 50%,而且不依赖 PyTorch。注意预处理要和训练时一致:BGR 转 RGB、归一化到 0 到 1、HWC 转 CHW。如果导出时用了--end2end,输出已经包含 NMS 结果,不需要再手动做。

5. 船舶检测训练与部署的避坑清单

5.1 现象:训练 loss 正常但 mAP 一直是 0

原因通常是类别映射错了。YOLOv7 读ship.yaml里的names列表,索引从 0 开始,如果你的标注 txt 里类别 id 和names顺序对不上,模型学到的就是错位的类别。解决方法是写个脚本统计标注 txt 里出现的所有 class_id,和names长度对比,多一个少一个都会出问题。

5.2 现象:推理时框出一大片水面,置信度还不低

这是典型的负样本不足。模型没见过足够多的「无船水面」,把波浪纹理当成了船。解决办法是在数据集里加入 10% 到 20% 的纯水面负样本,标注文件为空即可。YOLOv7 对空标注图片的处理是只计算 obj_loss 的负样本部分,不会报错。

5.3 现象:小目标检测效果差,远处小船全漏

原因可能是输入分辨率太低,或者 anchor 尺寸不匹配。船舶数据里小目标可能只有 10 到 20 像素,640 输入下经过 32 倍下采样只剩不到 1 个像素。解决办法是把--img-size提到 1280,或者用--cfg cfg/training/yolov7x.yaml这种更大容量的模型。另外可以检查hyp里的anchor_t,适当放宽让更多小 anchor 参与匹配。

5.4 现象:训练到一半 loss 突然变成 nan

血泪经验:大概率是标注坐标越界或者宽高为 0。VOC 转 YOLO 时如果 xmax 等于 xmin,宽就是 0,归一化后还是 0,计算 loss 时 log(0) 就炸了。解决方法是转换脚本里加一行过滤,宽或高小于 1 像素的框直接丢弃。另外学习率太大也会导致 nan,把lr0降到 0.001 试试。

5.5 现象:ONNX 推理结果和 PyTorch 对不上

常见原因是预处理不一致。PyTorch 的detect.py用的是 letterbox 填充,保持长宽比,而你自己写 ONNX 预处理时如果直接 resize 到 640x640,长宽比就变了,框的位置会偏移。解决办法是照着detect.py里的letterbox函数实现一遍,或者导出时用--img-size和推理时保持一致。

6. 把船舶检测推到更高精度:几个我常用的进阶技巧

第一个技巧是切片推理(SAHI)。船舶图像里小目标多,整图推理时小目标被下采样太狠。SAHI 的思路是把大图切成有重叠的小块,每块单独推理,再把结果合并。对 4000x3000 的航拍图,切成 640x640 的块,重叠 20%,小目标召回率能提升 15% 以上。代价是推理时间线性增加,适合离线分析不适合实时视频。

第二个技巧是测试时增强(TTA)。YOLOv7 的test.py支持--augment,会对每张图做翻转、缩放等多尺度推理再融合。我实测在船舶数据上 mAP@0.5 能涨 1 到 2 个点,但速度慢 3 倍。如果业务对精度极度敏感、对延迟不敏感,可以开。

第三个技巧是类别平衡。船舶数据里货船可能占 70%,渔船只占 10%,模型会偏向多数类。除了在hyp里调cls_pw,更直接的办法是对少数类做过采样,或者用 copy-paste 增强把渔船贴到不同背景上。我一般先看混淆矩阵,哪个类别被误判最多,就针对性补那个类别的数据。

验证模型有没有真正学到东西,我习惯做一件事:把验证集里模型置信度在 0.3 到 0.5 之间的框全部导出来,人工看一遍。这些是模型的「犹豫样本」,如果里面大部分是正确检测但置信度偏低,说明模型欠拟合,可以多训几十轮;如果里面是明显的误检,说明需要补负样本。这个习惯帮我省了很多盲目调参的时间。

最后一个习惯:每次改完数据或超参数,先跑 10 个 epoch 看 loss 曲线和 mAP 趋势,不要一上来就训 150 轮。10 轮就能看出方向对不对,不对就赶紧停,省下来的时间够你多试好几组配置。船舶检测这个方向,数据质量比模型结构重要得多,把标注做干净、把负样本补足,比换什么模型都管用。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询