简介:YOLOV5目录格式目标检测数据集,聚焦黑夜航拍场景下的海面船只智能识别,面向深度学习与计算机视觉方向的开发者、研究者及港口船只检测相关项目人员。图像为256×256的RGB图片,按训练集与验证集划分清晰,其中训练集含17204张图片及对应txt标签,验证集含4300张图片及对应txt标签,统一标注为boat类别,可直接投入模型训练,无需额外格式转换。资源包共2000个文件,以txt标签文件为主(1999个),另附1个可视化py脚本,压缩包大小约217.48MB。可视化脚本可随机读取图片并绘制边界框,无需更改即可直接运行,便于快速预览标注效果。当前已有234人学习下载,适合用于搭建黑夜环境下船只目标检测基线任务,亦可借此熟悉数据集目录组织方式;借助附带的py脚本,还能直观核对标签质量,减少数据清洗与调试环节的额外成本。
1. 这个黑夜航拍船只数据集,解决的是白昼模型集体失明的问题
夜晚的海面不是黑的,是到处都是亮点。很多工程师拿 YOLOV5 跑惯用目标检测数据集很顺,一到黑夜航拍船只就翻车——白天场景训练好的模型,拿到夜间航拍数据集上直接失灵。原因不是模型笨,而是训练数据里根本没有「黑夜航拍」这个分布。这里要讲的数据集,就是专门为这个场景准备的:YOLOV5 目录格式、大型黑夜航拍船只、单类别。它解决的是「海上夜间有多少船、船在哪」这类巡检与监管问题,适合做海事无人机巡检、智慧港口、安防监控的视觉工程师,也适合正被「yolov5 训练自己的数据集」绕晕的入门者。拿到它之后,你要做的不是马上训练,而是先把它当规范来检查、对齐,再喂给模型。
2. 拆解 YOLOV5 目录格式:这套数据集的结构与标注怎么对齐
2.1 认目录布局:images、labels、data.yaml 三段式
YOLOV5 的数据集目录约定很简单:图片放在 images 下,按 train、val、test 分开;标注放在 labels 下,且标签必须和图片同名。一个按 YOLOV5 目录格式交付的船只数据集,布局基本长这样:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml注意 labels 下通常没有 test:测试集一般不给标签,推理阶段的预测结果要用另外的脚本去评。data.yaml 是数据集的身份证,里面写 path、train、val、test 的相对路径,以及 nc=1 和 names。单类别数据集最省心的地方就是 names 只有一个值,比如写成names: ['ship'],类别编号从 0 开始。如果你之前用的是 VOC 那种 xml 标注或者 COCO 的 json,转换后最容易出错的地方就在这里:类别 id 没有从 0 开始,或者 txt 和 jpg 文件名对不上。常见做法是统一用 labelImg、CVAT 这类目标检测常用标注工具导出,或者用转换脚本做格式互转,但转完必须自己验一遍。
标注文件本身是纯文本,每行五个数:class x_center y_center width height。x_center、width 是相对图片宽度的比例,y_center、height 相对图片高度,取值 0~1。因为只有 1 个类别,所以每行开头的 class 永远是 0。这个格式本身不难,难的是坐标一旦算错,训练不报错,框却全部偏掉——这是 YOLO 系目标检测最典型的隐形事故。而且这套目录约定不只适用于 YOLOV5,YOLOv8 处理数据集时也是同一套逻辑,所以后面讲到的自检脚本可以直接跨版本复用。
2.2 先跑一个自检脚本:找缺失、越界、空标签
我第一次拿到这类数据集时吃过亏:txt 里坐标是像素值而不是归一化值,YOLOV5 不报错,训练 loss 也正常,但预测框全在画面角落。现在我的习惯是任何数据集进训练前,先跑一段自检脚本,把几类最常见的问题一次扫出来:
import numpy as np from pathlib import Path def check_dataset(data_root): img_dir = Path(data_root) / 'images' / 'train' lab_dir = Path(data_root) / 'labels' / 'train' missing_lab, empty_lab, bad_coord = [], [], [] for img_p in img_dir.glob('*.jpg'): lab_p = lab_dir / (img_p.stem + '.txt') if not lab_p.exists(): missing_lab.append(str(img_p)) continue lines = lab_p.read_text().strip().splitlines() if not lines: empty_lab.append(str(img_p)) continue for ln in lines: parts = ln.split() if len(parts) != 5: bad_coord.append(f'{lab_p.name}: 字段数不对 {ln}') continue x, y, w, h = map(float, parts[1:]) if not (0 < x < 1 and 0 < y < 1) or not (0 < w <= 1 and 0 < h <= 1): bad_coord.append(f'{lab_p.name}: 越界 x={x} y={y} w={w} h={h}') print(f'缺失标签: {len(missing_lab)} 张') print(f'空标签: {len(empty_lab)} 张') print(f'异常坐标: {len(bad_coord)} 条') for item in missing_lab[:5]: print(' 缺:', item) for item in bad_coord[:5]: print(' 坏:', item) check_dataset('./dataset')这个脚本把三类问题分开统计:标签缺失、txt 内容为空、坐标越界。字段数不等的场景会在 YOLOV5 加载时直接抛异常,反而是好事;越界和缺失是静默的,最坑。check_dataset入参只要数据集根目录,脚本内部自己拼接 images/train 和 labels/train 的路径,建议对 val 再跑一次。还有一种情况值得加进脚本:jpg 文件本身损坏,夜间航拍素材常来自压缩传输或掉帧,混进去一张坏图会让整个训练中断,用 PIL 打开一遍就能发现。
2.3 数据体检:小目标占比决定后面的参数怎么设
目录自检通过后,别急着训练,先做一次尺度统计。目标检测里有个不成文的说法:目标在 640x640 输入图上小于 32x32 像素就属于小目标,而黑夜航拍船只里,小目标往往是绝对主力。统计方式很简单,直接读标签里的 width、height,换算到原始分辨率下看分布:
import numpy as np from pathlib import Path def analyze_scale(lab_dir): areas = [] for lab_p in Path(lab_dir).glob('*.txt'): for ln in lab_p.read_text().strip().splitlines(): p = ln.split() w, h = float(p[3]), float(p[4]) areas.append(w * h) # 归一化面积占比 areas = np.array(areas) pixel_box = np.sqrt(areas) * 640 # 近似换算到 640 输入下的像素边长 small = (pixel_box < 32).mean() * 100 print(f'标注框总数: {len(areas)}') print(f'归一化面积均值: {areas.mean():.5f}') print(f'小目标占比(边长<32px @640): {small:.1f}%') print(f'面积分位: 25%={np.percentile(areas, 25):.5f} ' f'50%={np.percentile(areas, 50):.5f} ' f'75%={np.percentile(areas, 75):.5f}') analyze_scale('./dataset/labels/train')这里的areas保存归一化面积,乘上 640 是为了预先判断小目标占比。如果结果里大多数框的换算边长都小于 32,后面训练时 imgsz 取 1280 或者采用切图推理,会比硬拉 640 更合适;如果分布集中在中等尺寸,用默认参数反而省事。输出里的四分位数要重点看:当 25% 分位还不到 0.0001,意味着有一大批框小到可能在特征图下采样后只剩几个像素。这个体检结果直接决定后续预处理怎么做、anchors 要不要重算,也是判断数据集质量最硬核的一项。
3. 黑夜航拍数据训练前的预处理:增强要克制,策略要对路
3.1 先判断数据通道形态:可见光还是红外,做法完全不同
拿到这套黑夜航拍数据集后,第一件事是查看图像通道分布。不同航拍载荷的成像差别很大:可见光相机在夜里画面暗、噪点多;红外热像仪分辨率低但目标亮、背景净。这两种形态的标注难度和模型关注的特征完全不同。标题没有写明传感器类型,只能靠统计去判断:
import numpy as np from PIL import Image from pathlib import Path def channel_stat(img_paths, sample=50): dark_frac, sat_frac = [], [] for img_p in list(img_paths)[:sample]: arr = np.array(Image.open(img_p).convert('RGB')).astype(np.float32) gray = arr.mean(axis=-1) dark_frac.append((gray < 30).mean()) sat_frac.append((gray > 250).mean()) print(f'暗部像素占比均值: {np.mean(dark_frac)*100:.1f}%') print(f'饱和像素占比均值: {np.mean(sat_frac)*100:.1f}%') channel_stat(list(Path('./dataset/images/train').glob('*.jpg')))这个脚本统计暗部(<30)和饱和(>250)像素占比,对小样本跑 50 张就有直观认识。如果暗部占比高且饱和低,说明是可见光低照度场景,后续要做增强;如果亮部集中在一个个小热点上且暗部极黑,更像红外或带红外补光的形态,此时对比度增强的意义不大,反而该做高光抑制。这一判断会影响后面所有参数选择。
可见光和红外的处理差异其实很大。可见光低照度下,海面和天空噪声大,船只轮廓受灯光污染重,模型要把「灯光点」聚成「船体框」,所以更依赖上下文和整体形状,增强时保边缘纹理比单纯提亮重要。红外形态下背景平坦、目标高亮,难点不是看不见,而是「亮斑」太多——海面反光、码头灯光也有类似强度,模型容易把任何高亮区域都当成船。这两类问题的解决思路正好相反:前者重增强和去噪,后者重数据多样性和负样本。
3.2 低照度增强:CLAHE 和 gamma 的参数怎么定才不伤模型
对可见光形态,最常用的离线增强是 CLAHE(限制对比度自适应直方图均衡)和 gamma 校正。我一般会先离线增强一份副本,肉眼对比效果再做决定。注意不要对训练集所有图做同一个强映射,否则网络学到的是「增强后的假纹理」,推理时遇到原图反而失配。
import cv2 import numpy as np def clahe_enhance(img, clip=2.0, grid=(8, 8), gamma=0.8): lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=clip, tileGridSize=grid) l = clahe.apply(l) lab = cv2.merge((l, a, b)) out = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) out = np.power(out.astype(np.float32) / 255.0, gamma) * 255.0 return out.astype(np.uint8)clipLimit我一般取 2.0~4.0,tileGridSize8x8。clip 太小增强效果不明显,clip 太大容易把海面噪声一起放大,夜晚画面里噪点会锐化成假的纹理边缘。gamma 小于 1 提亮暗部,但 gamma 低于 0.6 时暗部细节会整体发灰,模型反而分不清船和背景。黑夜航拍图我通常取 0.7~0.9,宁可保守不要激进。这段代码另一个用途是做推理前的同等预处理:如果训练数据做过 CLAHE,推理时也要对测试图做同一套增强,否则 mAP 和实际表现会有明显落差,这是很多人没意识到的坑。
3.3 YOLOv5 自带增强项:黑夜场景该开什么,该关什么
YOLOv5 的 hyp.yaml 里有一批在线增强参数,训练时边增强边喂图。原则是「增强要增加场景多样性,不要改变目标外观的语义」。黑夜航拍船只最需要的是让小船在提亮后有更多形态变化,最怕的是把船灯和海面反光人为搅在一起。
| 参数 | 默认值 | 夜间航拍建议 | 理由 |
|---|---|---|---|
| hsv_v | 0.4 | 0.2~0.3 | 亮度抖动过大会让极暗的船在 batch 间忽隐忽现 |
| hsv_s | 0.7 | 0.3~0.5 | 黑夜图像饱和度本身低,大幅抖动意义不大 |
| mosaic | 1.0 | 0.5~0.8 | 保留拼图带来的上下文,但小船可能在切割时被切碎 |
| mixup | 0.0 | 0.0~0.2 | 夜间样本本来就稀疏,mixup 会模糊目标边界,新手建议先关 |
| fliplr | 0.5 | 0.5 | 单类别检测不需要区分船头方向,可以开 |
| degrees | 0.0 | 5~10 | 航拍姿态变化,小角度旋转等于免费扩充样本方向分布 |
这里要重点说 mosaic。YOLOv5 默认 mosaic 是 1.0,四张图拼成一张,对通用目标检测很有效。但黑夜航拍里船目标小,拼图把每张图的船缩到原图的四分之一面积以下,近处的船勉强还能看出来,远处的小船直接被切到只剩几个像素,这种样本对训练是纯噪声。我一般建议先降到 0.5 跑一轮,val 的小目标 AP 不升反降就继续关到 0。此外degrees很容易被忽略,俯视航拍里船体方向任意,给个 5~10 度旋转等于白送一批方向样本,代价几乎为零。
4. 用 YOLOV5 跑通这套船只数据集:命令、超参与 anchors 的完整链路
4.1 最小可复现训练命令
数据集校验和预处理做完后,直接进入训练。YOLOV5 的工程结构多年没变,环境配置上 Python 3.9+、PyTorch 2.x、一张 11G 显存的卡足够起步。yolov5 环境配置最常翻车的是 requirements.txt 里的 torch 版本和 CUDA 不对应,建议先建一个干净的 conda 环境再装依赖:
conda create -n yolo python=3.9 -y conda activate yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt python train.py --data /path/to/data.yaml --weights yolov5s.pt \ --img 640 --batch 16 --epochs 100 --project runs/ship--data指向数据集的 data.yaml,--img 640是输入边长,--batch 16看显存而定。我一般按 11G 显存跑 16,不够就降到 8。第一轮目标不是刷精度,是确认数据链路没断:训练到第一个 epoch 时看日志里 class 数、box、obj 的 loss 是数值而不是 nan。如果 nan 出现,先检查标签里有没有 width/height 为 0 的行,这是 YOLO 风格标注下最典型的隐性错误。
注意:训练第一轮日志出现 nan 时,优先检查标签里 width/height 是否为 0,其次是检查图片有没有损坏,最后才怀疑学习率。
yolov5s.pt是从官方 release 下载的预训练权重,黑夜小目标场景建议用预训练热启动,收敛快很多;如果网络受限,可以换随机初始化权重,但训练轮次要相应增加。
4.2 imgsz 与 anchors:小目标专用配置怎么选
黑夜航拍船只一个反直觉的事实是:输入分辨率比模型层数更影响小目标召回。640 输入下,一个 30x30 像素的船经过 5 次下采样后只剩约 1 个像素,特征图上的响应几乎不可分辨。所以这类任务的第一建议是:先试跑一轮--img 1280,但显存翻四倍,batch 必须跟着降。另一种更省钱的做法是维持 640 训练,推理时用切图(tile)保证实际输入里的船足够大,这一点在第 6 章展开。
anchors 是第二个关键点。YOLOV5 训练开始时会对当前数据集的标签做 autoanchor 计算,如果聚类出来的 anchors 和 COCO 默认值差异很大,小目标 AP 会有肉眼可见的提升。多数情况让它自动跑就行。只有一个例外:当数据集目标尺度分布极窄、或者单类别目标形状高度一致时,autoanchor 建议关闭,用--noautoanchor。原因很简单,聚类出来的 anchors 可能过拟合训练集,让验证集上的泛化变差。黑夜航拍船只属于「分布偏窄」这一类,但具体要不要关,用两轮短训练实验对比着看,训练日志里会打印出 autoanchor 前后的 anchors 数值。
| 模型 | 计算量相对值 | 黑夜小目标表现 | 适用场景 |
|---|---|---|---|
| yolov5s | 1x | 够用,收敛快 | 数据量小于 1 万张时首选 |
| yolov5m | 2.5x | 略好于 s,显存敏感 | 数据量大且需要精度 |
| yolov5l/x | 5~10x | 提升有限,显存压力大 | 不做 tiling 时慎选 |
这个表格是我自己的经验:黑夜小目标问题的瓶颈通常在输入分辨率和增强策略,而不是模型宽度。直接用大模型而不调 imgsz,很容易出现「训练慢、AP 没涨」的尴尬结果。
4.3 单类别数据集必调超参数:先动数据增强,后动学习率
单类别比多类别简单,但也更容易偷懒。很多人直接把 hyp.scratch-low.yaml 拿来就跑,结果小目标 AP 上不去,就归咎于数据集不行。我的 YOLOv5 超参数调整顺序是:先动增强,再动学习率,最后才动结构。黑夜场景下增强参数里有三个要重点动:hsv_v从 0.4 降到 0.2,避免模型只学会「看亮度不看形状」;mosaic降到 0.5~0.8,防止小船被切碎;degrees给 5~10 度,对应航拍姿态变化。
学习率方面,预训练热启动时lr0=0.01不需要动;从零开始随机训练时降为 0.005 更稳。这两个参数还有一个连带关系:小目标多的任务,batch 小了之后梯度噪声大,学习率也不要开大,否则 loss 会反复横跳。看到 val mAP 在 60~70 个 epoch 后停滞,先别急着加 epoch,回头检查是不是 mosaic 还开在 1.0。这种「loss 正常下降、小目标 AP 不动」的情况,八成出在增强配置而不是模型结构上。coco 预训练权重在这些场景里会帮大忙,但一定要保证你的 data.yaml 里 nc=1,否则类别头对不上,早期 loss 会异常偏高。
5. 夜间航拍船只数据集的避坑清单:现象、原因、解法
5.1 训练 loss 正常下降,验证 mAP 一直贴近 0
现象:训练日志里 box_loss 和 obj_loss 一路降,loss 曲线看着没问题,但每轮验证的 mAP50 永远是 0.0 几。 原因:最常见的是标签坐标没有归一化。txt 里存的是像素坐标或相对中心点的变体,YOLOV5 不校验坐标范围,模型把「大于 1 的坐标」当作背景忽略,于是学习过程顺利但完全学不到目标。这正是「目标检测模型微调崩了」最常见的一种形态。 解决:跑第 2 章的越界检查脚本,把所有 x/y/w/h 跑到 0~1 之外的标签行全部筛出来。如果确认是像素坐标,需要做一次转换:除以原图的宽和高,并确保 w、h 也同步归一化。
5.2 训练一开始就报 label class 越界或标签数量不匹配
现象:日志里出现ignoring corrupt label的警告刷屏,或者报 class 数量超出 nc。 原因:数据集虽然声称单类别,但 labels 里混入了旧标注。VOC 里 ship 的 id 可能是 6,转到 YOLO 后直接照搬;还有可能是同一目录下残留了没有对应图片的 txt。 解决:不依赖 YOLOV5 的自动过滤,自己写脚本把所有 txt 的每行第一个数字做唯一值统计,只保留 0。对残留 txt 和孤儿图片做一次全量配对清理,再重新跑自检脚本确认 clean。单类别数据集这一点尤其值得注意,因为标注量一大,人工很容易漏掉几行混杂数据。
5.3 val 指标不错,但推理时把港口灯、月亮倒影全识别成船
现象:验证集 mAP50 达到 0.9 以上,下到真实场景视频里,海面灯光被大量误检,船反而漏掉。 原因:数据集负样本不足。单类别数据集如果只有正样本,模型没有见过「夜里亮但不是船」的反例,会把任何高亮小斑块当作目标。 解决:收集一批不含船只的夜间海面、港口、天空镜头作为负样本,只放入 images、不放 labels,然后加入训练集。这是目标检测常用数据集整理里最容易被忽略的一步,但对夜间误检的改善往往立竿见影。加入负样本后记得把 data.yaml 里的 train 路径重新确认,并保证负样本目录不会被当 val 去统计 mAP。
5.4 把 4000x3000 的航拍原图 resize 到 640 后,船只剩几个像素
现象:训练用 640 输入,验证集 mAP 很高;实际对单张高空原图推理时,小船全部漏检。 原因:航拍原图分辨率高,船在原始图像里可能有 50x50 像素,但整体 resize 到 640 之后只剩不到 10 像素,特征图上已不可判别。 解决:两条路。一是训练和推理都用 1280 输入并降低 batch;二是维持 640 训练,推理时对原图切成 640x640 的 tile,再把框映射回原图。第二种对显存友好,实际工程里更常用,具体实现在第 6 章展开。
5.5 关闭 mosaic 后反而变好,这和主流教程说的不一样
现象:主流教程都说 mosaic 能提升精度,你把 mosaic 从 1.0 调到 0.5,模型的 val 指标明显变好,甚至调到 0 更好,于是产生怀疑。 原因:mosaic 的本意是增加上下文多样性,但黑夜小船图在拼接后,小目标被缩到几乎不可见,模型从这些退化样本里学到的是「忽略极小的亮斑」,推理时真实小船也被一并忽略。 解决:不用盲目跟随默认值。对夜间小目标,建议直接做mosaic=0.5与mosaic=0两档对比实验,选 val 小目标 AP 更高的档位。这看起来有点玄学,但实测里是最常翻车也最好修的配置项。
5.6 验证集 mAP 虚高:训练过的图片混进了验证集
现象:验证 mAP 很高,模型上线后表现却完全不同,像换了一个模型。 原因:数据划分时用了随机抽样但没有按文件名去重,或者同一航次、同一条视频里相邻帧同时进了 train 和 val,造成信息泄漏。 解决:划分 train/val 前按场景维度去重。比如文件名以场景 ID 开头,就按前缀分组后再划分。
from pathlib import Path from sklearn.model_selection import train_test_split img_pths = list(Path('./dataset/images').glob('*.jpg')) scene_ids = [p.stem.split('_')[0] for p in img_pths] train_files, val_files = train_test_split( img_pths, test_size=0.15, stratify=scene_ids, random_state=42 )这个片段只做一件事:把同一场景的帧归到同一组,再按组切分 train/val。stratify按场景分布去重后的类别比例分层,random_state固定随机种子保证可复现。对航拍视频抽帧类数据集,这一步是必需的,否则模型可能只是「背」下了训练帧,而不是真正学会检测船。
6. 用切图推理验证真实效果:比 mAP 更靠谱的最后一关
6.1 tile 切图推理如何把坐标映射回原图
训练和验证都正常后,最后一步是验证「原图上的真实表现」。4000x3000 的航拍原图直接扔进模型,小船看不出来;切图推理在遥感图像目标检测里是常规操作,航拍船只也一样。核心逻辑是切成 640x640 的 tile,逐个推理,再把框映射回原图坐标,最后做一次全局 NMS 合并:
def iou(a, b): ix1, iy1 = max(a[0], b[0]), max(a[1], b[1]) ix2, iy2 = min(a[2], b[2]), min(a[3], b[3]) iw, ih = max(0, ix2 - ix1), max(0, iy2 - iy1) inter = iw * ih union = (a[2]-a[0])*(a[3]-a[1]) + (b[2]-b[0])*(b[3]-b[1]) - inter return inter / union if union > 0 else 0 def infer_tile(model, full_img, tile=640, overlap=64, conf=0.25): h, w = full_img.shape[:2] all_boxes = [] for y in range(0, h, tile - overlap): for x in range(0, w, tile - overlap): tile_img = full_img[y:y+tile, x:x+tile] result = model(tile_img, conf=conf) for box in result[0].boxes.xyxy.cpu().numpy(): all_boxes.append([box[0]+x, box[1]+y, box[2]+x, box[3]+y, result[0].boxes.conf.item(), result[0].boxes.cls.int().item()]) return nms_merge(all_boxes) def nms_merge(boxes, iou_thr=0.5): boxes = sorted(boxes, key=lambda b: b[4], reverse=True) keep = [] while boxes: cur = boxes.pop(0) keep.append(cur) boxes = [b for b in boxes if iou(cur[:4], b[:4]) < iou_thr] return keeptile要与训练 imgsz 一致,overlap必须有 64~128 像素,否则船在 tile 边缘会被切掉一半,这是切图推理最常见的细节坑。映射回原图坐标时要加 x、y 偏移,相邻 tile 的重叠框用全局 NMS 合并。overlap 太小会漏检,太大计算量翻倍,航拍场景 64 是不错的起点。conf建议根据训练后画出的 PR 曲线来调,夜间误检多就升到 0.3~0.4。
6.2 用小目标召回率校准验收基线
mAP50 是宏观指标,对夜间航拍不够敏感。我现在的习惯是画一张「目标大小 x 召回率」的断点表:把验证集里的框按像素面积分成 <16、16~32、32~64 三档,分别看召回率。小于 16 像素的那一档如果没有 0.5 以上,说明模型的实用价值有限——真实场景里多数船只恰好落在这个区间。这个分析和第 2 章的尺度体检对应起来,就能看出哪些提升来自增强、哪些来自 imgsz,而不是只盯着一两个 epoch 的 loss 曲线。每换一个夜间场景,我都会先跑一遍这个分层召回,再决定要不要动 anchors 和输入的 tile 大小。希望帮到你。
本文还有配套的精品资源,点击获取