简介:这份资源面向无人机视觉与水上目标识别方向的学习者和开发者,提供基于 ultralytics YOLO11 的船只检测完整方案,可用于水上目标识别、海上搜救等场景,适合具备一定深度学习基础、希望快速复现或二次开发的人员。压缩包共约 2000 个文件,整体 676.5MB,包含 1059 个 txt 标签、284 张 jpg 图像、169 个 py 脚本、88 个 yaml 配置及少量 cpp、js、html 等推理与展示文件,覆盖数据、训练、部署多个环节。数据集已标注为 YOLO 与 VOC 两种格式,并划分 train、val、test,附 data.yaml,可直接用于 yolov5 至 v12 等算法训练,类别为 boat(船只)。资源同时提供使用教程与模型评价指标曲线图,便于读者掌握训练流程、评估模型效果并迁移到自有数据。已有 89 人学习,适合作为水上目标检测项目的起步模板与参考实现。
1. 无人机视角船只检测:为什么通用 YOLO 模型在水面上会集体翻车
把 YOLO11 直接套到无人机航拍的海面画面里,十有八九第一轮推理结果会让你怀疑人生:近处船只框得还行,中远距离的船要么漏检,要么被框成一片模糊的白色噪点。这不是模型不行,而是无人机视角 + 水面背景这个组合本身就带着几个反直觉的坑。水面高光反射、船只尾迹、远处海天交界线,都会让在 COCO 上表现优异的通用权重直接失灵。ultralytics 这套框架把训练、验证、导出、推理串成了一条流水线,但流水线跑得通不代表结果能用。
这篇笔记面向的是手里有无人机航拍素材、想做水上目标识别或海上搜救任务的工程师。我会把从数据集组织、YOLO11 训练配置、到无人机视角特有的参数调整和排错路径讲清楚,让你拿到一份「船只检测数据集 + 训练好的模型」之后,知道每一步在干什么、参数为什么这么设、哪里最容易翻车。海上搜救场景对漏检的容忍度极低,一个漏掉的小目标可能意味着一条人命,所以召回率优先于精度,这个基调会贯穿全文。
2. 数据集怎么组织:从原始航拍到 YOLO11 能吃的格式
2.1 无人机船只数据集的三个典型来源与标注策略
无人机视角的船只数据,常见来源无非三类:一是自采的航拍视频抽帧,二是公开的无人机海事数据集,三是用仿真环境合成的画面。自采数据的优势是场景贴合,劣势是标注成本高;公开数据集省事,但类别定义和你的任务未必一致;合成数据可以批量生成小目标,但域间隙(domain gap)会让模型在真实画面上打折扣。我一般会以自采为主、公开数据做补充、合成数据只用来预训练。
标注策略上,船只检测和通用目标检测最大的区别在于目标尺度跨度极大。同一帧画面里,近处的渔船可能占 800×600 像素,远处的快艇只有 12×8 像素。如果按常规的标注规范只框可见船体,远处目标会小到模型根本学不到特征。我的做法是:对小于 16×16 像素的目标,标注时适当外扩 2 到 4 个像素,保证标注框至少覆盖船体的主要轮廓;对超过画面 1/3 的近处大船,正常标注即可。类别上,如果只是搜救任务,建议先做单类「boat」,不要一上来就分渔船、货轮、快艇,类别越多小目标越难学。
标注工具用 LabelImg 或 CVAT 都行,导出 YOLO 格式的 txt。每张图的标注文件里,每行是class_id x_center y_center width height,全部归一化到 0 到 1。这里有个血泪经验:无人机画面里经常出现船只被浪花或尾迹部分遮挡的情况,标注时只框可见部分,不要脑补被遮挡的船体,否则模型会学到错误的边界。
2.2 目录结构与 data.yaml 的最小可用配置
YOLO11 对数据集的目录结构有固定要求,ultralytics 会在训练时按data.yaml里的路径去读。一个能直接跑通的最小结构如下:
boat_dataset/ ├── images/ │ ├── train/ # 训练集图片,jpg/png │ ├── val/ # 验证集图片 │ └── test/ # 测试集图片(可选) ├── labels/ │ ├── train/ # 与 train 图片同名的 txt │ ├── val/ │ └── test/ └── data.yamldata.yaml的内容:
# 数据集根路径,建议写绝对路径,避免相对路径踩坑 path: /home/user/boat_dataset train: images/train val: images/val test: images/test # 类别数,单类船只就写 1 nc: 1 # 类别名称,顺序必须和标注文件里的 class_id 对应 names: 0: boat逻辑说明:path是数据集根目录,train/val/test是相对于path的子路径。ultralytics 会拼接path + train去找图片,然后自动把images替换成labels去找同名标注。参数上,nc必须和标注文件里出现的最大 class_id 加一一致,写错了训练时不会报错,但类别会错乱。names的键必须是整数,从 0 开始连续。
提示:如果验证集和训练集用了同一批图片,mAP 会虚高,搜救任务里这种自欺欺人最要命。建议按 8:1:1 划分,且划分时保证同一段视频的帧不要同时出现在训练和验证集里,否则相邻帧高度相似会导致验证指标失真。
2.3 用脚本做一次数据集体检:尺寸、类别、空标注
在开训之前,我习惯跑一个体检脚本,把数据集的坑提前暴露出来。下面这段代码检查图片能否正常读取、标注是否越界、有没有空标注文件:
import os from pathlib import Path from PIL import Image root = Path("/home/user/boat_dataset") splits = ["train", "val", "test"] bad_images, empty_labels, out_of_range = [], [], [] for split in splits: img_dir = root / "images" / split lbl_dir = root / "labels" / split for img_path in img_dir.glob("*"): # 1. 检查图片是否可读 try: w, h = Image.open(img_path).size except Exception as e: bad_images.append((str(img_path), str(e))) continue # 2. 检查对应标注 lbl_path = lbl_dir / (img_path.stem + ".txt") if not lbl_path.exists(): empty_labels.append(str(img_path)) continue lines = lbl_path.read_text().strip().splitlines() if len(lines) == 0: empty_labels.append(str(img_path)) continue for line in lines: parts = line.split() if len(parts) != 5: out_of_range.append((str(lbl_path), "字段数不对")) continue cls, x, y, bw, bh = parts # 3. 归一化坐标必须在 0~1 之间 vals = [float(x), float(y), float(bw), float(bh)] if any(v < 0 or v > 1 for v in vals): out_of_range.append((str(lbl_path), line)) print("坏图:", len(bad_images)) print("空标注:", len(empty_labels)) print("越界标注:", len(out_of_range))逻辑说明:这段脚本做了三件事——用 PIL 打开图片确认文件没损坏;检查每张图是否有对应的标注文件且非空;校验归一化坐标是否在 0 到 1 之间。参数上不需要额外配置,改root路径即可。跑完如果「空标注」数量超过总数的 5%,说明标注工作有遗漏,需要回去补;「越界标注」通常是因为标注工具导出时坐标没归一化,或者手动改标注时写错了。
3. YOLO11 训练配置:从预训练权重到无人机视角的参数微调
3.1 ultralytics 安装与版本确认
ultralytics 的安装本身不复杂,但热搜里那个could not find a version that satisfies the requirement ultralytics的报错,我见过太多次。绝大多数情况是 Python 版本太低或者 pip 源的问题。YOLO11 要求 Python 3.8 以上,推荐 3.10 或 3.11。安装命令:
# 建议在虚拟环境里装,避免污染系统环境 python -m venv yolo_env source yolo_env/bin/activate # 升级 pip,老版本 pip 解析依赖容易出问题 pip install --upgrade pip # 安装 ultralytics,会自动带上 torch 等依赖 pip install ultralytics # 验证安装和版本 yolo version逻辑说明:python -m venv创建独立环境,避免和系统里其他包冲突。pip install --upgrade pip这步很多人跳过,但老版本 pip 在解析 ultralytics 的依赖树时确实会报找不到版本。yolo version会打印当前 ultralytics 版本,确认装上了再往下走。如果还是报could not find a version,检查一下 Python 版本是不是低于 3.8,以及网络是否能正常访问 PyPI。
注意:不要用
pip install ultralytics --pre去装预览版,预览版的 API 可能和稳定版不一致,训练脚本会莫名其妙报错。生产任务用稳定版。
3.2 用 yolo11n.pt 做迁移学习:训练命令与关键参数
YOLO11 有 n/s/m/l/x 五个尺寸,无人机视角船只检测我一般从yolo11n.pt或yolo11s.pt起步。n 版推理快,适合机载实时;s 版精度略高,适合地面站后处理。训练命令:
yolo detect train \ model=yolo11n.pt \ data=/home/user/boat_dataset/data.yaml \ epochs=150 \ imgsz=1280 \ batch=8 \ device=0 \ workers=4 \ patience=30 \ project=boat_runs \ name=exp_n_1280逻辑说明:model=yolo11n.pt加载 COCO 预训练权重做迁移学习,比从零训练收敛快得多。data指向刚才的data.yaml。epochs=150是上限,配合patience=30做早停——30 轮验证指标不提升就停,省时间。imgsz=1280是无人机视角的关键参数,下面单独讲。batch=8在 8GB 显存上比较稳,显存大可加到 16。device=0指定第一块 GPU,CPU 训练把 device 设成cpu但会很慢。workers=4是数据加载线程数,根据 CPU 核数调整。
参数上最需要解释的是imgsz。YOLO 默认 640,但无人机画面里小目标多,640 下远处船只可能只剩几个像素,特征提取网络根本抓不住。把imgsz提到 1280,小目标的像素面积变成原来的 4 倍,召回率会有肉眼可见的提升。代价是显存占用和训练时间增加,推理速度也会下降。如果机载算力有限,可以训练用 1280、推理用 960 或 640,但精度会掉一些,需要自己权衡。
3.3 无人机视角必调的超参:mosaic、scale、copy_paste
ultralytics 的默认数据增强对通用数据集够用,但无人机视角有它的特殊性。几个我每次都会动的参数:
yolo detect train \ model=yolo11s.pt \ data=/home/user/boat_dataset/data.yaml \ epochs=200 \ imgsz=1280 \ batch=8 \ mosaic=0.8 \ scale=0.6 \ copy_paste=0.3 \ degrees=5.0 \ fliplr=0.5 \ hsv_h=0.015 \ hsv_s=0.5 \ hsv_v=0.4逻辑说明:mosaic=0.8表示 80% 的概率做马赛克增强,把四张图拼成一张。这个增强对小目标特别有用,因为拼图后小目标会出现在不同位置和尺度上。但注意,mosaic 在训练最后 10 轮建议关掉(ultralytics 有close_mosaic参数,默认 10),让模型在真实分布上收尾。scale=0.6是随机缩放幅度,无人机高度变化导致目标尺度变化大,这个值可以适当放大到 0.6 到 0.8。copy_paste=0.3是复制粘贴增强,把标注的船只抠出来贴到其他位置,对增加小目标样本量很有效,但贴的时候要注意别贴到天空区域,否则模型会学到「船在天上」这种荒谬特征。
degrees=5.0是随机旋转角度,无人机航拍会有轻微倾斜,但不会像卫星图那样任意角度,所以给 5 度就够了,给太大反而引入不真实的样本。hsv_s=0.5和hsv_v=0.4是饱和度和明度扰动,水面反光会导致亮度剧烈变化,这两个值调大一点能提升模型对光照的鲁棒性。
提示:
copy_paste在 ultralytics 里需要分割标注才能发挥完整效果,检测任务下它做的是简单的矩形区域复制。如果你的数据集只有检测框,这个增强的效果有限,可以设成 0.1 到 0.2 保守使用。
4. 训练过程排查:loss 不降、mAP 虚高、显存爆炸怎么处理
4.1 看训练日志的三个关键指标
ultralytics 训练时会在终端和runs/目录下输出日志。我盯三个东西:box_loss、cls_loss、mAP50。box_loss是边界框回归损失,正常应该在前 20 轮快速下降然后趋于平缓;如果它一直震荡不降,多半是学习率太大或者标注框有问题。cls_loss是分类损失,单类任务下这个值应该很快降到接近 0,如果居高不下,检查data.yaml里的nc和names是否和标注一致。mAP50是 IoU 阈值 0.5 下的平均精度,搜救任务我更关注mAP50-95和召回率,因为高 IoU 下的精度才反映框得准不准。
如果训练到 50 轮mAP50还在 0.1 以下,基本可以停了,回去查数据集。常见原因是标注坐标没归一化、图片和标注文件名不匹配、或者data.yaml路径写错导致读到了空数据集。
4.2 验证集 mAP 高但实际推理漏检:过拟合与分布偏移
这是无人机船只检测里最迷惑人的坑:验证集mAP50跑到 0.9,拿一段新拍的视频去推理,漏检一片。原因通常是验证集和训练集来自同一段视频,相邻帧几乎一样,模型只是记住了这些帧,没有真正学到船只的通用特征。解决办法前面提过,按视频段划分数据集,而不是按帧随机划分。
另一个原因是分布偏移。训练集是晴天正午拍的,推理时是黄昏或者阴天,水面颜色和反光完全不同。缓解办法是在训练集里尽量覆盖不同光照、不同海况、不同飞行高度。如果实在覆盖不了,推理前做一次简单的直方图均衡化或者用测试时增强(TTA),yolo detect predict加augment=True可以开启 TTA,但速度会慢 2 到 3 倍。
4.3 显存不够时的降级路径
imgsz=1280加batch=8在 8GB 显存上可能直接 OOM。降级顺序我一般这么走:先把batch降到 4,再把imgsz降到 1024,最后才考虑换更小的模型。不建议一上来就降imgsz到 640,那样小目标检测基本废了。如果显存实在紧张,可以用梯度累积模拟大 batch:
yolo detect train \ model=yolo11n.pt \ data=/home/user/boat_dataset/data.yaml \ imgsz=1280 \ batch=4 \ accumulate=4 \ device=0accumulate=4表示每 4 个 batch 才更新一次权重,等效于 batch=16,但显存占用还是 batch=4 的水平。代价是训练速度会慢一些,因为多了等待时间。
5. 避坑与常见问题:五条血泪经验
现象一:训练 loss 正常下降,但推理时所有船都被框成同一个位置。原因通常是标注文件里的坐标没有归一化,比如写成了像素坐标而不是 0 到 1 的比例。解决方法是回到 2.3 的体检脚本,检查out_of_range的输出,把所有越界标注重新导出。
现象二:could not find a version that satisfies the requirement ultralytics反复出现。原因可能是 Python 版本低于 3.8,或者 pip 源里没有对应版本的包。解决方法是python --version确认版本,然后pip install ultralytics -i https://pypi.org/simple指定官方源重试。
现象三:训练到一半突然报 CUDA out of memory。原因可能是imgsz或batch设太大,也可能是workers太多导致数据加载占用显存。解决方法是降batch到 4 或 2,workers降到 2,如果还不行就降imgsz到 1024。
现象四:验证集 mAP 很高,但模型导出 ONNX 后推理结果和 PyTorch 不一致。原因通常是导出时的输入尺寸和推理时的预处理不匹配。解决方法是导出时明确指定imgsz,推理时用同样的尺寸做 letterbox 预处理,不要用 OpenCV 默认的 resize。
现象五:小目标漏检严重,大目标框得还行。原因可能是imgsz太小,或者mosaic增强关闭太早。解决方法是把imgsz提到 1280,mosaic保持 0.8 到 0.9,close_mosaic设成 15 而不是默认的 10,让模型多学一会儿小目标。
6. 进阶技巧:用切片推理把远处小船捞回来
即使imgsz=1280,无人机画面里最远处那几个像素级的船只还是可能漏掉。这时候可以用切片推理(SAHI,Slicing Aided Hyper Inference)的思路:把大图切成有重叠的小块,每块单独推理,再把结果合并。ultralytics 本身不直接提供 SAHI,但可以自己写一个简单的切片推理脚本:
import cv2 import numpy as np from ultralytics import YOLO model = YOLO("boat_runs/exp_n_1280/weights/best.pt") img = cv2.imread("test_frame.jpg") h, w = img.shape[:2] # 切片参数:每块 640x640,重叠 128 像素 tile, overlap = 640, 128 stride = tile - overlap all_boxes = [] for y in range(0, h, stride): for x in range(0, w, stride): # 保证切片不越界 x2, y2 = min(x + tile, w), min(y + tile, h) x1, y1 = max(0, x2 - tile), max(0, y2 - tile) crop = img[y1:y2, x1:x2] results = model.predict(crop, imgsz=640, conf=0.25, verbose=False) for box in results[0].boxes: # 把切片坐标映射回原图 bx1, by1, bx2, by2 = box.xyxy[0].tolist() all_boxes.append([bx1 + x1, by1 + y1, bx2 + x1, by2 + y1, box.conf[0].item()]) # 用 NMS 合并重叠框 def nms(boxes, iou_thr=0.5): boxes = sorted(boxes, key=lambda b: b[4], reverse=True) keep = [] while boxes: best = boxes.pop(0) keep.append(best) boxes = [b for b in boxes if iou(best, b) < iou_thr] return keep def iou(a, b): xx1, yy1 = max(a[0], b[0]), max(a[1], b[1]) xx2, yy2 = min(a[2], b[2]), min(a[3], b[3]) inter = max(0, xx2 - xx1) * max(0, yy2 - yy1) area_a = (a[2] - a[0]) * (a[3] - a[1]) area_b = (b[2] - b[0]) * (b[3] - b[1]) return inter / (area_a + area_b - inter + 1e-6) final = nms(all_boxes) print(f"切片推理得到 {len(final)} 个目标")逻辑说明:这段脚本把原图按 640×640 切片,重叠 128 像素,每块单独推理,然后把框映射回原图坐标,最后用 NMS 去重。参数上,tile和overlap需要根据目标大小调:目标越小,tile可以越小、overlap越大,但推理次数会成倍增加。conf=0.25是置信度阈值,搜救任务可以降到 0.15 提高召回,代价是误检增多,需要人工复核。
这个方法的代价是推理时间变成原来的十几倍,不适合机载实时,但适合地面站对关键帧做精细搜索。我一般只在搜救任务的最后确认阶段用,平时还是用整图推理。
最后说个习惯:每次训练完,我都会拿一段完全没参与训练的视频跑一遍,人工数漏检和误检,而不是只看验证集的 mAP。验证集指标是给自己看的,真实场景的漏检才是要命的。希望帮到你。
本文还有配套的精品资源,点击获取