简介:这是一份面向目标检测初学者与算法工程师的YOLO船舶目标检测数据集,聚焦水面船只、皮划艇、独木舟、摩托艇等水上目标的识别任务,可直接用于模型训练、课程实验与算法对比。数据集已按train、val、test完成划分,并附带data.yaml配置文件,类别仅含boat一类,yolov5、yolov7、yolov8等主流框架开箱即可训练。压缩包共2000个文件,以1817个txt标注文件、178张jpg图像和5个yaml配置为主,整体约114.65MB,标注与图像一一对应,目录结构清晰便于快速接入训练流程。目前已有1521人学习下载,适合需要现成船舶检测数据、验证模型效果或开展迁移学习实践的读者参考使用。
1. 船舶检测数据集拿到手之后:先别急着喂给 YOLO
很多做内河航运、港口监控或者海上执法辅助的同行,拿到yolo-boat-detect-dataset-1.zip这类船舶目标检测数据集时,第一反应是解压、改data.yaml、直接yolo train。我见过太多人卡在 mAP 只有 0.3 上下,然后怀疑是模型不行、显卡不行,其实问题出在数据本身——船舶目标在遥感或岸基视角下长宽比极端、密集停泊时框叠框、水面反光导致标注边界模糊,这些都不是换个 YOLOv8 权重能解决的。这篇笔记就围绕这个数据集,把从解压验收到训练调参、再到踩坑排查的完整路径讲清楚,适合已经跑通过 YOLO 官方 COCO 示例、准备切到自己垂类数据的工程师,也适合刚接触yolo数据集制作、想少走弯路的同学。核心就一件事:让这份船舶数据真正训得动、评得准、部署得出去。
2. 拆开压缩包先做三件事:目录结构、类别分布、标注质量
2.1 目录结构决定你后面改多少行配置
船舶检测数据集常见的组织方式有两种:一种是 YOLO 原生格式,images/和labels/平行放,配一个data.yaml;另一种是从 VOC 或 COCO 转过来的,多一层Annotations或annotations.json。拿到yolo-boat-detect-dataset-1.zip后,先别解压到桌面,找个纯英文路径,比如/data/boat_detect/,因为后面labelimg 打标完yolo格式的标或者脚本转换时,中文路径会让 OpenCV 读图直接返回 None,这个坑我踩过不止一次。
解压后执行下面这段脚本,把结构打印出来,同时统计图片和标签是否一一对应:
import os from pathlib import Path root = Path("/data/boat_detect") for split in ["train", "val", "test"]: img_dir = root / "images" / split lbl_dir = root / "labels" / split if not img_dir.exists(): print(f"[跳过] {split} 图片目录不存在") continue imgs = {p.stem for p in img_dir.glob("*.jpg")} | {p.stem for p in img_dir.glob("*.png")} lbls = {p.stem for p in lbl_dir.glob("*.txt")} if lbl_dir.exists() else set() print(f"{split}: 图片 {len(imgs)} 张, 标签 {len(lbls)} 个, 缺失标签 {len(imgs - lbls)}, 多余标签 {len(lbls - imgs)}")逻辑说明:用stem做集合运算,能快速定位「有图无标」和「有标无图」的情况。参数上,如果你的图片是.jpeg或.bmp,把 glob 模式补全即可。这一步跑完,如果缺失标签超过 5%,说明压缩包本身可能不完整,或者标注时漏标了,需要回头找数据提供方确认,不要硬训。
2.2 类别分布和长宽比统计:船舶数据的两个命门
船舶检测和通用目标检测最大的区别在于:船体细长,靠泊时密集,远小目标多。所以第二步要统计每个类别的框数量,以及宽高比的分布。下面这段脚本直接读 YOLO 格式的cls x y w h:
import numpy as np from pathlib import Path from collections import Counter lbl_dir = Path("/data/boat_detect/labels/train") cls_counter = Counter() ratios = [] for txt in lbl_dir.glob("*.txt"): for line in txt.read_text().strip().splitlines(): parts = line.split() if len(parts) != 5: continue c, x, y, w, h = parts cls_counter[int(c)] += 1 if float(h) > 0: ratios.append(float(w) / float(h)) print("类别分布:", dict(cls_counter)) if ratios: arr = np.array(ratios) print(f"宽高比 中位数 {np.median(arr):.2f}, 90分位 {np.percentile(arr, 90):.2f}, 最大 {arr.max():.2f}")逻辑说明:cls_counter告诉你有没有类别极度不平衡,比如「货船」几千个框、「渔船」只有几十个,那训练时就要考虑加权或过采样。宽高比中位数如果超过 3,说明大部分是细长船体,默认的 anchor 或 YOLOv8 的 anchor-free 头虽然能适应,但数据增强里的scale和mosaic参数要调,否则拼接后船体被裁断,模型学到的全是局部。参数上,90 分位超过 5 的话,建议在data.yaml里把imgsz提到 960 甚至 1280,不然下采样后船体只剩几个像素。
2.3 用可视化抽查标注框有没有「漂移」
统计只能看数量,框画得准不准必须肉眼抽检。我一般随机抽 16 张,把 YOLO 格式还原成框画上去:
import cv2 import random from pathlib import Path img_dir = Path("/data/boat_detect/images/train") lbl_dir = Path("/data/boat_detect/labels/train") samples = random.sample(list(img_dir.glob("*.jpg")), 16) canvas = np.zeros((4*480, 4*640, 3), dtype=np.uint8) for i, img_path in enumerate(samples): img = cv2.imread(str(img_path)) h, w = img.shape[:2] lbl = lbl_dir / (img_path.stem + ".txt") if lbl.exists(): for line in lbl.read_text().strip().splitlines(): c, x, y, bw, bh = map(float, line.split()) x1 = int((x - bw/2) * w); y1 = int((y - bh/2) * h) x2 = int((x + bw/2) * w); y2 = int((y + bh/2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) img = cv2.resize(img, (640, 480)) r, c = divmod(i, 4) canvas[r*480:(r+1)*480, c*640:(c+1)*640] = img cv2.imwrite("/data/boat_detect/check_grid.jpg", canvas)逻辑说明:YOLO 的x y w h是归一化后的中心点和宽高,还原时先乘回原图尺寸。重点看三种情况:框是否把船头船尾切掉、密集停泊时框是否严重重叠、水面反光处有没有误标。如果发现某类框系统性偏移,比如所有框都往左上偏,那可能是标注工具导出时坐标系没对齐,需要重新导出。这一步花十分钟,能省后面几小时调参的玄学时间。
3. 从 data.yaml 到第一次训练:参数怎么设才不浪费显卡
3.1 data.yaml 里三个必须改的字段
船舶数据集的data.yaml通常长这样,但直接拿来用往往报错:
path: /data/boat_detect train: images/train val: images/val test: images/test nc: 4 names: ['cargo', 'fishing', 'passenger', 'other']必须确认的三点:第一,path用绝对路径,别用../,YOLO 在子进程里解析相对路径经常翻车;第二,nc和names长度必须一致,如果 names 里有中文,改成英文或拼音,否则训练日志乱码;第三,如果test目录不存在,直接删掉这一行,不然验证阶段会报FileNotFoundError。改完用yolo checks跑一下环境,确认 Ultralytics 版本和 CUDA 可用。
3.2 第一次训练用哪个模型和哪些超参
船舶检测我一般从yolov8s起步,不直接上yolov8x,因为船舶数据量通常不大,大模型容易过拟合,而且推理速度在边缘设备上扛不住。下面是一条能直接跑的命令:
yolo detect train \ data=/data/boat_detect/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=960 \ batch=16 \ patience=30 \ lr0=0.01 \ lrf=0.01 \ mosaic=0.8 \ scale=0.3 \ degrees=5.0 \ fliplr=0.5 \ cache=True \ device=0 \ project=/data/runs/boat \ name=exp1逻辑说明:imgsz=960是为了保住远小船的像素,如果显存不够降到 640,但 mAP 通常会掉 3 到 5 个点。mosaic=0.8比默认的 1.0 略低,因为船舶拼接后容易出现「半条船」的伪标签,尤其密集停泊场景。scale=0.3控制随机缩放幅度,太大让细长船体变形。degrees=5.0只做小角度旋转,船舶在遥感图里方向固定,大角度旋转反而引入噪声。cache=True把图片缓存到内存,船舶数据集通常几千张,16G 内存够用,能明显加快 epoch 速度。
3.3 训练过程中盯哪几个指标
启动后不要只看 loss,重点看metrics/mAP50(B)和metrics/mAP50-95(B)的差距。如果 mAP50 很高但 mAP50-95 很低,说明框的位置不够准,通常是标注框偏大或偏小,回到 2.3 节重新抽检。如果训练集 mAP 涨但验证集 mAP 平甚至降,那是过拟合,把patience调小、dropout打开,或者增加copy_paste增强。另外注意cls_loss如果一直不降,检查类别是否标错,比如把「货船」标成了「其他」。
4. 船舶检测专属的避坑排查:这五个问题几乎人人都会遇到
4.1 现象:mAP 卡在 0.4 上不去,loss 震荡
原因:船舶数据里小目标占比高,默认的imgsz=640下采样 32 倍后,一条 20 像素宽的船只剩不到 1 个像素,特征全丢。解决:把imgsz提到 960 或 1280,同时在data.yaml同级目录加一个hyp.yaml,把box损失权重从 7.5 提到 9.0,让模型更关注框回归。
4.2 现象:验证时提示「No labels found」
原因:YOLO 找标签的路径是images替换成labels,如果你的目录叫labels_train或者标签放在Annotations,它就找不到。解决:要么改目录名,要么在data.yaml里显式写train: images/train和val: images/val,并确保labels和images同级。别用软链接,Windows 下软链接经常失效。
4.3 现象:训练到一半显存爆了
原因:mosaic增强会随机拼 4 张图,如果原图分辨率不一致,拼完的尺寸可能远超imgsz,加上cache=True把原图也缓存了。解决:训练前用脚本把所有图片统一 resize 到长边 1280 并保存,或者把cache改成disk,牺牲一点速度换稳定。
4.4 现象:推理时框重叠严重,一条船出好几个框
原因:船舶密集停泊时,NMS 的iou阈值默认 0.7 太高,相邻船的框被保留。解决:推理时加iou=0.5,或者训练时把overlap_mask关掉。如果还是不行,说明标注时相邻船框本身就重叠,需要在标注阶段用「不重叠框」原则重新标。
4.5 现象:换到自己的岸基摄像头图片上,模型完全失效
原因:训练数据是遥感视角,岸基是斜视,船体外观和背景分布差异大。解决:这不是调参能解决的,要么补充岸基视角的标注数据,要么用yolo export导出 ONNX 后做域适应。我一般会先抽 50 张目标场景图,用训练好的模型跑一遍,看置信度分布,如果普遍低于 0.2,就别硬调了,直接补数据。
5. 把船舶检测推到可用:置信度门限、切片推理与导出部署
5.1 置信度门限不是拍脑袋定的
很多人推理时直接用conf=0.25,这是 COCO 的默认值,放到船舶场景往往漏检小船。正确做法是拿验证集跑一遍,画出不同conf下的 precision 和 recall 曲线,找 F1 最高点。下面这段脚本直接调 Ultralytics 的 val 接口:
from ultralytics import YOLO import numpy as np model = YOLO("/data/runs/boat/exp1/weights/best.pt") results = model.val(data="/data/boat_detect/data.yaml", conf=0.001, iou=0.6, plots=True) # 从 results 里取曲线数据,实际使用时可直接看生成的 PR_curve.png print("mAP50:", results.box.map50) print("mAP50-95:", results.box.map)逻辑说明:conf=0.001是为了让 val 输出完整的 PR 曲线,然后看PR_curve.png里 F1 最大值对应的 conf。船舶检测我一般会把最终部署的conf设在 F1 最大点再减 0.05,留一点召回余量,因为漏检一条船的代价比误检高。
5.2 大图切片推理:让远小船舶不再漏
遥感船舶图往往 4000×4000 以上,直接 resize 到 960 会丢细节。常见做法是切片推理:把大图切成 1024×1024 带重叠的小块,逐块推理再合并。Ultralytics 本身不带这个功能,但可以用sahi库,或者自己写一个滑动窗口:
import cv2 import numpy as np from ultralytics import YOLO model = YOLO("/data/runs/boat/exp1/weights/best.pt") img = cv2.imread("/data/test_big.jpg") H, W = img.shape[:2] tile, overlap = 1024, 128 step = tile - overlap all_boxes = [] for y in range(0, H, step): for x in range(0, W, step): patch = img[y:y+tile, x:x+tile] if patch.shape[0] < tile or patch.shape[1] < tile: patch = cv2.copyMakeBorder(patch, 0, tile-patch.shape[0], 0, tile-patch.shape[1], cv2.BORDER_CONSTANT) res = model.predict(patch, conf=0.3, verbose=False)[0] for box in res.boxes: xyxy = box.xyxy[0].cpu().numpy() xyxy[[0, 2]] += x xyxy[[1, 3]] += y all_boxes.append(xyxy) # 合并时用 NMS 去重 def nms(boxes, iou_thr=0.5): boxes = np.array(boxes) if len(boxes) == 0: return [] x1, y1, x2, y2 = boxes[:,0], boxes[:,1], boxes[:,2], boxes[:,3] areas = (x2-x1) * (y2-y1) order = areas.argsort()[::-1] keep = [] while order.size > 0: i = order[0] keep.append(i) xx1 = np.maximum(x1[i], x1[order[1:]]) yy1 = np.maximum(y1[i], y1[order[1:]]) xx2 = np.minimum(x2[i], x2[order[1:]]) yy2 = np.minimum(y2[i], y2[order[1:]]) w = np.maximum(0, xx2-xx1); h = np.maximum(0, yy2-yy1) inter = w * h iou = inter / (areas[i] + areas[order[1:]] - inter) order = order[1:][iou <= iou_thr] return boxes[keep] final = nms(all_boxes, 0.5) print(f"切片推理得到 {len(final)} 个框")逻辑说明:tile=1024和overlap=128是经验值,重叠区保证边缘的船不被切一半。合并时用 NMS 去掉重叠框,iou_thr=0.5比默认严一点,因为切片边缘容易产生重复检测。参数上,如果显存够,tile可以提到 1536,但推理时间线性增长。
5.3 导出 ONNX 和 TensorRT 的注意点
训练完要部署,yolo export一条命令能导 ONNX,但船舶模型导出时有两个细节:第一,imgsz必须和训练时一致,否则 anchor 对不上;第二,如果用了mosaic增强,导出时不用管,但推理前处理要自己写 letterbox。TensorRT 导出在 Jetson 上很常见,但注意half=True只在支持 FP16 的设备上开,否则精度掉得厉害。我一般先在 PC 上用 ONNX Runtime 验证一遍,确认输出和 PyTorch 一致,再上边缘设备。
5.4 一个我坚持了很久的习惯
每次训完船舶模型,不管 mAP 多高,我都会拿 20 张「最差场景」——夜雾、强反光、密集停泊——跑一遍,把漏检和误检截图存到一个叫bad_cases的文件夹。下次补数据、调增强,直接对着这个文件夹改。这个习惯让我从「调参玄学」里解脱出来,因为问题永远在数据里,不在lr0的小数点后第三位。希望帮到你。
本文还有配套的精品资源,点击获取