简介面向目标检测模型训练需求这份数据集以YOLOV5标准目录格式提供黑夜航拍海面船只的检测数据单类别为boat可支撑港口巡逻、海事安防等场景下的模型训练与算法验证。压缩包共2000个文件以txt标注文件为主并附带名为show.py的可视化脚本整体压缩后约217.48MB数据已按训练集与验证集目录划分无需额外整理即可接入训练流程。目前已有234人学习下载图像统一为256×256的RGB图片其中训练集含17204张图片及对应txt标签验证集含4300张图片及对应txt标签另附类别文本文件说明。使用时可直接加载数据集完成YOLOV5训练也可运行可视化脚本随机读取一张图片绘制边界框并保存便于快速检查标注质量、理解数据分布适合目标检测学习者与港口船只检测项目开发者直接上手。1. 拿到黑夜航拍船只的YOLOV5数据集后先想清楚这三件事黑夜航拍船只是目标检测里典型的“数据决定上限”场景可见光相机在低照度下信噪比骤降船体和海面灰度差可能只有十几个像素再加上航拍高度带来的小目标问题船只往往在画面里只占几十个像素。目标检测数据集YOLOV5目录格式解决的就是这个入口问题——它把原始影像整理成了YOLOv5能直接读取的images/labels目录树类别固定为1船只。拿到之后你真正要做的是三件事看懂这个数据集的成像分布、校验标注是否可靠、再用合理的参数把它训练起来。适合做航拍监控、港口巡检、海事监管方向验证的人也适合第一次想跑通“YOLOV5训练自己的数据集”链路的新手。2. 黑夜航拍船只数据集的成像特点与目录结构先看懂再动手2.1 黑夜可见光 vs 红外成像数据分布决定模型上限夜间航拍的检测难点不在网络结构而在输入图像的分布。可见光相机在晚上拍到的海面噪声颗粒非常明显ISO拉高之后暗部出现大量随机亮斑。船只在画面里通常有两种形态靠近港口时体积大、轮廓完整远洋或开阔水域时体积小、对比度低甚至只有舷灯或者甲板灯的几个亮点。一个成熟的黑夜航拍船只数据集为了覆盖这两种形态通常会在图像里混入大目标、小目标、强灯光干扰、海面反光、云雾遮挡等样本。训练时如果只拿大而清晰的船做训练部署到开阔水域就一定会漏检。红外或热成像在夜间能看到更完整的船体轮廓但它也有代价单通道灰度图缺少颜色和纹理信息船体和海水在热成像里的对比度同样不稳定。同一艘船白天可见光和夜间红外的特征分布差异极大。所以如果数据集里的图像是红外或伪彩图验证阶段就按灰度图或伪彩原始形态输入不要做自动白平衡也不要强行三通道复制后当普通RGB用——那等于给模型喂了不相干的颜色噪声。反过来如果是纯可见光夜间图那训练集里的“暗样本”充足与否就决定了模型的鲁棒性底子。2.2 YOLOV5目录格式的硬约定images/labels、文件同名、txt五列YOLOV5对数据集目录结构的约束非常死板好在也足够简单。根目录按train和val分两套各自再放images和labels两个子目录。图片全部放images标注全部放labels标注文件名必须和图片名完全一致只是扩展名换成.txt。YOLOv5的Dataset类在读取时会根据当前图片路径做前缀替换——把images替换成labels、把.jpg替换成.txt。文件名对不上这条样本会被安静地跳过训练日志里只会多一条“WARNING: 0 labels for class”之类的提示。dataset/ ├── train/ │ ├── images/ │ │ ├── 000001.jpg │ │ ├── 000002.jpg │ │ └── ... │ └── labels/ │ ├── 000001.txt │ ├── 000002.txt │ └── ... └── val/ ├── images/ └── labels/labels里的txt文件每一行代表一个目标框。格式是五列类别id、归一化中心x、归一化中心y、归一化宽w、归一化高h。拿刚才的000001.txt举例0 0.512340 0.734567 0.123456 0.056789这行的含义是第一个字段0表示类别id本数据集只有船一个类别所以id固定为0后面四个浮点数分别是目标框中心点x坐标占图片宽度的比例、中心点y坐标占图片高度的比例、框宽占图片宽度的比例、框高占图片高度的比例。所有值都在0到1之间。这个归一化动作是刚接触YOLOv5的人最容易翻车的地方——有人手滑把像素坐标直接写进txt训练时loss直接爆炸而且日志里还不容易看出来是哪一行的问题。目录结构还有一个容易被忽视的细节images和labels的目录层级必须一模一样。如果train/images下面有2024子目录那train/labels下面也必须有一个2024子目录标注文件放里面。YOLOv5定位标注文件时是按相对路径替换前缀的一个层级对不上就会找不到文件。很多从网盘下载的数据集压缩包解压之后目录树是好的但如果你自己往images里加了归档子目录却忘了在labels里建同名子目录训练集的有效样本数会瞬间少一大截。这个坑我踩过不止一次后面在避坑章节里还会展开。2.3 标注质量与目标尺度分布两个容易被低估的变量训练数据集的标注质量比模型结构更致命。黑夜航拍图里人眼都很难判断船体边缘到底在哪里标注员如果经验不足会把框画得忽大忽小。框画大了模型学到的目标范围是“船海面”框画小了模型学到的特征只有船体局部。相比白天数据集夜间数据的bbox贴合度对mAP影响更大因为船体和背景的边界模糊框的漂移会直接放大回归误差。另一个变量是尺度分布。航拍数据集的天然问题是小目标占比高如果训练集里小目标像素面积小于32×32的框占比超过一半那YOLOv5默认的640输入分辨率会显著吃亏。因为下采样到特征图后小目标只剩几个cell特征基本被池化抹平了。解决方向有两个训练时提高输入分辨率到1280甚至1536或者按尺度对样本做重采样确保训练时模型能看到足够多的大中小混合样本。这两个操作对黑夜航拍场景的收益往往比换一个更大的骨干网络还要明显。3. 把原始航拍素材整理成YOLOV5格式目录初始化、xml转txt与批校验3.1 从视频抽帧到目录划分划分原则按航次不按帧拿到数据集之后第一件事是检查目录结构是否完整然后用脚本初始化一套干净的训练/验证目录。如果你是从原始视频自己构建那还要先做抽帧——固定间隔抽帧比逐帧抽省时间而且能避免相邻帧高度相似导致的信息冗余。常见做法是每个航次视频按每2秒或每5帧抽一张抽出来的图按来源航次命名比如night_2024_0621_0001.jpg。# 初始化YOLOV5目录结构train/val各一套images和labels DATASET_ROOT/data/harbor_ship_det mkdir -p $DATASET_ROOT/{train,val}/{images,labels} # 按航次分桶拷贝前8个航次做train后2个航次做val for f in /raw_data/night_2024_{0601..0620}_*.jpg; do cp $f $DATASET_ROOT/train/images/ done for f in /raw_data/night_2024_{0621..0630}_*.jpg; do cp $f $DATASET_ROOT/val/images/ done这里最关键的一点是按航次划分而不是按帧划分。航拍视频相邻帧之间内容几乎是连续的同一个场景在不同帧里反复出现。如果按顺序抽帧再随机切成train和val验证集里和训练集几乎重复的帧会带来虚高的指标部署时换个新航次立刻原形毕露。正确做法是把不同日期、不同航线、不同天气条件的航次分成两组保证验证集里的场景是训练时没见过的。划分比例的常规做法是8:2或9:1。航拍数据如果总量够大我倾向9:1把更多样本留给训练因为验证集只需要覆盖场景多样性就行不需要堆量。如果数据总量只有几千张那8:2更稳防止验证集太小而指标抖动。3.2 从VOC xml转YOLO txt转换脚本与参数说明YOLO格式和VOC格式的差异本质上是坐标表达方式的差异。VOC的xml用左上角和右下角的绝对像素坐标YOLO用归一化的中心点和宽高。如果数据集里的标注是LabelImg导出的xml就得写个转换脚本。import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, img_w, img_h, cls_map): 将单张VOC xml标注转换为YOLOv5 txt标注。 cls_map: 类别名到id的映射本数据集只有ship - 0。 tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.iter(object): name obj.find(name).text if name not in cls_map: continue # 不在类别表里的目标直接跳过 cls_id cls_map[name] box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 越界修正把超出图片范围的坐标裁回边界内 xmin max(0, min(xmin, img_w - 1)) xmax max(0, min(xmax, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) ymax max(0, min(ymax, img_h - 1)) if xmax xmin or ymax ymin: continue # 修正后宽高非正丢弃该框 # 转归一化中心点与宽高 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 lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if not lines: return # 空标注返回空串 return \n.join(lines) \n # 使用示例把单张xml转成同名txt cls_map {ship: 0} txt_content voc_to_yolo(000001.xml, 1920, 1080, cls_map) with open(000001.txt, w) as f: f.write(txt_content)逻辑说明函数遍历xml里每一个object节点先查类别映射表映射表里没有的类别直接跳过然后读取bndbox里的四个坐标统一裁剪到图片尺寸范围内防止标注工具手滑把框拖出画面边缘最后把绝对坐标转成归一化的中心点和宽高。输出保留6位小数精度完全够用。参数说明cls_map是类别名到id的映射字典本数据集只有ship所以映射只有一项id固定为0。img_w和img_h是当前图片的真实宽高如果数据里有1080p也有4K图每一张都要传各自的宽高不能全写死成1920和1080。xml里字段不完整时脚本会直接抛KeyError这是好事——它逼着你检查漏标的xml而不是带着脏数据继续跑。3.3 训练前必跑的校验脚本越界、空标注、非法类别一次揪出标注格式转换完不等于数据就干净了。我在每次训练前都会跑一遍校验脚本把这几类问题一次性暴露出来坐标越界、空 txt、类别 id 非法。这些问题如果拖到训练中途才暴露排查成本极高不如在喂给 YOLOv5 之前就处理干净。import os def validate_labels(label_root, img_root, allowed_cls{0}): 校验YOLOv5格式标注的三大类问题 1) 有图片但没有对应txt或txt为空 2) 坐标值不在0~1区间 3) 类别id不在allowed_cls许可范围内 empty_label, bad_shape, bad_cls [], [], [] for img_name in os.listdir(img_root): base os.path.splitext(img_name)[0] label_path os.path.join(label_root, base .txt) if not os.path.exists(label_path): empty_label.append(img_name) continue with open(label_path, r) as f: lines [line.strip() for line in f if line.strip()] if not lines: empty_label.append(img_name) continue for line in lines: parts line.split() if len(parts) ! 5: bad_shape.append((img_name, line)) continue cls_id int(parts[0]) vals list(map(float, parts[1:])) if cls_id not in allowed_cls: bad_cls.append((img_name, cls_id)) if not all(0.0 v 1.0 for v in vals): bad_shape.append((img_name, line)) return empty_label, bad_shape, bad_cls empty_label, bad_shape, bad_cls validate_labels( /data/harbor_ship_det/train/labels, /data/harbor_ship_det/train/images, ) print(缺标注或空标注:, len(empty_label)) print(坐标或字段异常:, len(bad_shape)) print(类别非法:, len(bad_cls))逻辑说明脚本遍历 images 目录里每个文件名按同名前缀找 labels 下的 txttxt 不存在或内容为空都归入空标注每一行按空格切分字段数必须等于 5类别 id 必须在许可集合里坐标的 4 个值都必须在 0 到 1 闭区间内。三类问题分别统计最后打印数量。参数说明allowed_cls 这里写死为 {0}因为本数据集是 1 类别的船只检测。如果你扩展成多类别改成 {0, 1, 2} 即可。坐标检查用闭区间 0~1 是严格模式有些标注工具会导出 1.000001 这种浮点溢出值训练时虽然能读但会触发 clamp 警告建议在校验阶段就处理掉。提示空标注的图片建议直接剔除而不是保留成一张没有目标的负样本。负样本应该通过纯背景图加入数据集混入“有图无框”的样本会让 YOLOv5 把它当背景参与正负样本平衡反而干扰训练稳定性。4. 用YOLOV5训练黑夜船只模型data.yaml、超参数与结果评估4.1 data.yaml的写法绝对路径与names顺序YOLOv5训练时第一个读的文件就是data.yaml。它告诉训练脚本数据在哪、类别有几个、类别叫什么。写法很简单但里面有两个坑常被忽略。# data.yaml train: /data/harbor_ship_det/train/images val: /data/harbor_ship_det/val/images nc: 1 names: [ship]第一个坑是路径。train和val指向的是images目录不是图片文件也不是labels目录。如果写成相对路径YOLOv5会基于当前工作目录解析而train.py又习惯在yolov5项目目录下执行稍有不慎路径就失效。我的习惯是一律写绝对路径不在这上面赌运气。第二个坑是names的顺序。names列表的索引就是类别id本数据集只有[ship]一个元素所以ship对应的id正好是0。如果你之后把多个类别拼进来顺序和类别id必须和标注txt里的首列严格一致否则训练出来的模型语义全错推理结果张冠李戴。4.2 训练命令与核心超参数imgsz、batch、epochs怎么定在conda环境里配置好YOLOv5依赖之后训练命令的结构并不复杂真正要花心思的是几个参数怎么定。cd /workspace/yolov5 conda activate ship_env python train.py \ --data /data/harbor_ship_det/data.yaml \ --weights yolov5s.pt \ --img 1280 \ --batch 8 \ --epochs 100 \ --device 0 \ --workers 4 \ --project /data/harbor_ship_det/runs \ --name ship_night_v1参数说明--weights yolov5s.pt 是 COCO 预训练权重船只类别在 COCO 里存在语义域接近迁移学习能明显加速收敛。--img 1280 是输入分辨率这是黑夜航拍小目标最值得动的参数。默认 640 对小目标很不友好1280 之后 mAP50 通常会有明显提升代价是显存和训练时间翻倍。--batch 8 在 1280 输入下大概需要 16G 以上显存如果显卡不够降到 batch 4 并把 --img 降到 960 是更稳妥的组合。--epochs 100 对中小规模数据集够用如果 loss 在 80 epoch 还在稳定下降可以续训到 150。--workers 4 是数据加载线程数Windows 下有时要降到 0 否则会卡死Linux 下 4 到 8 都正常。训练过程中会实时打印 box_loss、cls_loss、dfl_loss 和 P、R、mAP50、mAP50-95。判断训练是否正常的初级标准是前 10 个 epoch loss 必须明显下降mAP50 必须从接近 0 开始爬升如果 20 个 epoch 后 loss 还在原地抖动先别急着换模型回去看数据和学习率。4.3 训练产物与指标解读best.pt/last.pt、mAP50与mAP50-95训练结束后runs/ship_night_v1 目录下会留下几个关键文件。很多新手分不清 best.pt 和 last.pt直接拿 last.pt 去部署结果部署的版本不是最高 mAP 的那一个。文件作用什么时候用best.pt验证集上 mAP 最高的权重推理、评估、导出部署模型首选last.pt最后一个 epoch 的权重断点续训或想多看几个 epoch 做后期判断results.pngloss 和 mAP 曲线图判断是否过拟合、是否还能继续训confusion_matrix.png混淆矩阵分析漏检与误检的分布评估指标里有几个数值需要分清。mAP50 要求预测框和标注框的 IoU 大于 0.5 就算命中它对黑夜里的模糊大框很宽容——框飘了一些但 IoU 依然大于 0.5分数照样高。mAP50-95 是从 0.5 到 0.95 每隔 0.05 算一次 IoU 阈值下的 AP 再取平均对框的回归精度更敏感。航拍船只部署场景关注的是几十像素小目标能不能精确定位所以 mAP50-95 比 mAP50 更有参考价值。如果你看到 mAP50 已经 0.9 了mAP50-95 却只有 0.5说明框的位置飘重点要调回归分支而不是继续堆分类能力。注意如果你训练完发现 mAP50 低于 0.7先不要急着换更大的模型。1 类别的航拍数据训练到这个分数以下十有八九是标注质量问题或 train/val 划分不当换模型只是在错误的数据上花更多时间。5. 避坑黑夜航拍船只数据集训练中最常踩的5个坑5.1 loss不降、mAP完全不动暗图增强强度不够现象训练了二三十个 epochbox_loss 还在原地横跳mAP50 始终在 0.1 以下徘徊。原因黑夜图像整体亮度低YOLOv5 默认的 HSV 增强对亮度扰动幅度不够。更隐蔽的是mosaic 增强会把四张暗图拼接在一起拼缝处产生大量虚假的“船形”噪点模型被噪点带偏压根没在学船体特征。解决修改 hyp.scratch-low.yaml 里的增强参数把 hsv_v 从默认 0.4 提高到 0.6让亮度扰动幅度更大同时把 mosaic 的概率从默认 1.0 降低到 0.7给模型更多原比例背景样本。如果用的是官方训练脚本可以另存一份 hyp_night.yaml在 train.py 里通过 --hyp 指定。5.2 mAP50高但远处小船全漏检输入分辨率与anchors没适配现象验证集 mAP50 有 0.85一跑到新的夜间航拍视频里远处那些几十像素的小船全部漏掉只剩近处大船能检出。原因验证集和训练集来自同一批航次尺度分布高度一致模型在验证集上是“开卷考试”部署视频里的船更小、对比度更低落在训练分布边缘之外。同时 YOLOv5 默认 anchors 是 COCO 80 类统计出来的先验对航拍视角下细长的船形适配度差。解决把 --img 从 640 提到 1280 甚至 1536让小目标在特征图上拥有更多像素训练时不要加 --noautoanchor让 YOLOv5 自动统计数据集的真实 anchor 分布并更新默认锚点。训练日志里出现“autoanchor: 从默认锚点更新”的提示说明新 anchor 已生效。这个组合拳对航拍小目标通常能从 0.6 拉到 0.8 以上。5.3 日志报“0 labels for ship”文件名、扩展名与CRLF换行现象训练日志里频繁出现 WARNING提示某张图片找不到 label统计下来有效样本数少了几百张。原因三种常见诱因。一是图片扩展名是大写的 .JPG标注文件名是 000001.txtYOLOv5 扫描文件名时按小写匹配找不到对应 txt 就跳过二是从 Windows 拷贝数据集到 Linuxtxt 文件带着 \r 行尾读取时解析错乱三是 images 目录新增了子目录但 labels 目录没同步建同名子目录路径替换找不到文件。解决先把扩展名统一成小写再用 dos2unix 清洗 txt 换行符find /data/harbor_ship_det -name *.JPG -exec rename s/\.JPG$/.jpg/ {} \; find /data/harbor_ship_det -name *.txt -exec dos2unix {} \;这两个命令跑完之后用第 3 章的校验脚本再扫一遍缺标注数量应该归零。如果还有漏网之鱼逐条看 images 和 labels 的目录树是否对称。5.4 推理结果类别编号错乱类别id写成了1现象训练时没报错看起来一切正常但推理时模型把船识别成某个不存在的类别输出结果的 class id 乱跳。原因标注 txt 里的类别 id 被写成了 1而不是 0。1 类别数据集的潜规则是类别 id 从 0 开始因为 names 列表只有 1 个元素names[0] 才是 ship。但不少人从多类别项目拷贝脚本习惯性地把第一个类别写成 1导致模型读到 class 1 并尝试对应 names[1]而 names[1] 并不存在。解决训练前用第 3 章的校验脚本跑一遍 allowed_cls{0}把首列非 0 的 txt 全部找出来。如果已经训练完才发现不用重新标写个小脚本把所有 txt 首列减 1 再重新训练一轮即可。这个错误最坑的地方是不报错只会在结果里悄悄错乱。5.5 val指标虚高、部署失灵train/val按帧划分泄漏现象val 的 mAP50 高达 0.95模型上机部署后漏检一片表现和验证指标完全对不上。原因这是航拍数据里最隐蔽的黑匣子——划分泄漏。如果 train 和 val 是从连续视频帧里随机拆的相邻帧高度相似模型相当于在验证时遇到了训练时的“近亲”。尤其是白天黑夜交替、云层变化不大的平稳航次同一场景在连续几十帧里几乎没变化随机划分会把几乎一样的图同时送进 train 和 val。解决按航次或按日期划分数据不要按帧划分划分完之后随机抽 20 对 train/val 样本做人工比对检查有没有同一场景的相邻帧混入。这条经验是我做航拍项目时的血泪教训val 指标虚高比低指标更危险它会给你一个错误的自信让你跳过必要的现场验证直接部署。6. 从权重到部署树莓派5上的端侧验证路径6.1 模型轻量化导出TorchScript与INT8量化的实测选择训练完 best.pt接下来的问题是这个方向在边缘设备上到底能不能用。我会在树莓派5上做一轮端侧验证先导出轻量格式python export.py --weights best.pt --img 1280 --include torchscript --simplify python quantize.py --weights best.pt --img 1280 --quant-int8导出之后模型体积比 fp32 小一半以上。树莓派5上跑 1280 输入大概能到 10~15 FPS降到 960 输入能到 20 FPS 以上。航拍无人机巡航场景15 FPS 基本够用。但要注意INT8 量化对夜间噪声图更敏感小目标在量化后可能额外退化所以部署前必须用独立视频序列实测。6.2 独立视频序列验证误报记录与量化衰减判断我在树莓派上会准备一段从未参与训练的夜间航拍视频跑推理时把每帧检测结果写入 json 日志再和人工标注做逐帧比对。评估的心理预期是边缘设备 INT8 量化 1280 输入下mAP50 比 fp32 掉 3~5 个点算正常掉 10 个点以上说明量化敏感这时优先保留 fp16 而不是 int8。“这个方向值不值得继续投入”我最后只看三个数字端侧 FPS、量化后的 mAP 衰减幅度、误报类型。漏检集中在极远小目标就继续提高输入分辨率误报集中在波形噪声就补充负样本两者都还好再考虑优化后处理里的置信度阈值和 NMS。这些年我养成的习惯是每次项目结束时留一份数据体检报告记录训练时的 anchor、mAP 曲线、量化衰减值。下次换相机、换夜间场景先翻这份记录再决定要不要重新训练而不是把一切推倒重来。这份记录帮我避开过很多重复劳动希望这个习惯也能帮到你。本文还有配套的精品资源点击获取