
简介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 的相对路径以及 nc1 和 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 系目标检测最典型的隐形事故。而且这套目录约定不只适用于 YOLOV5YOLOv8 处理数据集时也是同一套逻辑所以后面讲到的自检脚本可以直接跨版本复用。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} f50%{np.percentile(areas, 50):.5f} f75%{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, sample50): 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, clip2.0, grid(8, 8), gamma0.8): lab cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l, a, b cv2.split(lab) clahe cv2.createCLAHE(clipLimitclip, tileGridSizegrid) 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.0tileGridSize8x8。clip 太小增强效果不明显clip 太大容易把海面噪声一起放大夜晚画面里噪点会锐化成假的纹理边缘。gamma 小于 1 提亮暗部但 gamma 低于 0.6 时暗部细节会整体发灰模型反而分不清船和背景。黑夜航拍图我通常取 0.7~0.9宁可保守不要激进。这段代码另一个用途是做推理前的同等预处理如果训练数据做过 CLAHE推理时也要对测试图做同一套增强否则 mAP 和实际表现会有明显落差这是很多人没意识到的坑。3.3 YOLOv5 自带增强项黑夜场景该开什么该关什么YOLOv5 的 hyp.yaml 里有一批在线增强参数训练时边增强边喂图。原则是「增强要增加场景多样性不要改变目标外观的语义」。黑夜航拍船只最需要的是让小船在提亮后有更多形态变化最怕的是把船灯和海面反光人为搅在一起。参数默认值夜间航拍建议理由hsv_v0.40.2~0.3亮度抖动过大会让极暗的船在 batch 间忽隐忽现hsv_s0.70.3~0.5黑夜图像饱和度本身低大幅抖动意义不大mosaic1.00.5~0.8保留拼图带来的上下文但小船可能在切割时被切碎mixup0.00.0~0.2夜间样本本来就稀疏mixup 会模糊目标边界新手建议先关fliplr0.50.5单类别检测不需要区分船头方向可以开degrees0.05~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 python3.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 数值。模型计算量相对值黑夜小目标表现适用场景yolov5s1x够用收敛快数据量小于 1 万张时首选yolov5m2.5x略好于 s显存敏感数据量大且需要精度yolov5l/x5~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 度对应航拍姿态变化。学习率方面预训练热启动时lr00.01不需要动从零开始随机训练时降为 0.005 更稳。这两个参数还有一个连带关系小目标多的任务batch 小了之后梯度噪声大学习率也不要开大否则 loss 会反复横跳。看到 val mAP 在 60~70 个 epoch 后停滞先别急着加 epoch回头检查是不是 mosaic 还开在 1.0。这种「loss 正常下降、小目标 AP 不动」的情况八成出在增强配置而不是模型结构上。coco 预训练权重在这些场景里会帮大忙但一定要保证你的 data.yaml 里 nc1否则类别头对不上早期 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 的本意是增加上下文多样性但黑夜小船图在拼接后小目标被缩到几乎不可见模型从这些退化样本里学到的是「忽略极小的亮斑」推理时真实小船也被一并忽略。 解决不用盲目跟随默认值。对夜间小目标建议直接做mosaic0.5与mosaic0两档对比实验选 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_size0.15, stratifyscene_ids, random_state42 )这个片段只做一件事把同一场景的帧归到同一组再按组切分 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, tile640, overlap64, conf0.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:ytile, x:xtile] result model(tile_img, confconf) 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_thr0.5): boxes sorted(boxes, keylambda b: b[4], reverseTrue) 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 大小。希望帮到你。本文还有配套的精品资源点击获取