夜间无人机车辆检测:VOC/COCO/YOLO三格式与YOLO11三平台训练
简介面向无人机夜间车辆检测需求的配套资源包聚焦夜间无人机视角下的城市道路行驶车辆、道边停车、停车场、小区车辆及遮挡严重等真实场景统一标注为“car”单一类别适合无人机场景车辆检测项目开发及通用车辆检测数据补充。包体为1个PDF文件共4.13MB内附数据集基本情况、标注格式说明及百度网盘获取方式实际数据集包含1000张高质量夜间无人机图像采用LabelImg标注提供VOC(xml)、COCO(json)、YOLO(txt)三种格式标签可直接用于YOLO等算法训练。此外附赠YOLO11一键训练脚本覆盖GPU(GPUs)、CPU、Mac(M芯片)多平台并有博主训练结果日志供参考便于快速复现与调优。目前已有372人学习下载适合具备一定目标检测基础、需开展无人机夜间车辆检测实践的研究者与开发者。1. 夜间无人机视角下的车辆检测1000 张图能解决什么不能解决什么晚上把无人机悬在路口上方镜头往下看车灯在地面上拉成一片光带车身和沥青路面在低照度下几乎糊成一体——这时候想让检测模型把一辆辆车的准确框出来白天那一套在个别场景里完全翻车。这个数据集瞄准的正是“夜间 无人机俯视 车辆”三个条件同时满足的训练场景1000 张图配齐 VOC、COCO、YOLO 三种格式的标签再附一个能在 NVIDIA GPU、纯 CPU 和 Apple Silicon Mac 上直接跑的 YOLO11 训练脚本。对做目标检测入门、毕设或者夜视监控前期验证的人来说它的价值不是“数据量大”而是把数据到模型中间最容易耗时间的几个环节——标注格式转换、环境配置、训练脚本调试——一次性打包掉了拿到手就可以开始训练而不是先花两周做数据工程。2. 读懂三份标注VOC 的 XML、COCO 的 JSON、YOLO 的 TXT 各自怎么用2.1 夜间无人机视角给数据标注带来的三个特殊干扰夜间车辆检测在标注阶段就和白天有很大的区别拿常规检测数据集的思路去标很容易标出前后不一致的结果。第一是目标形态受车灯影响很多小车在画面里只有灯是亮的车身轮廓融在暗背景里标注员看不清楚就倾向于按“灯的两端”去给宽但实际上车宽不等于灯宽这会导致标签里的 bbox 普遍偏小训练出来的模型也习惯性地把框收得很紧。第二是视角问题无人机的俯视角度意味着绝大多数车辆都以车顶形态出现在数据集里几乎很难出现车头牌照这类角度特征模型只能靠轮廓、纹理和热辐射特征去区分车与非车注意力不足的话会把路灯杆、空调机组室外机甚至发亮的屋顶天线当车。第三是尺度变化同一张 1920x1080 的画面里飞行高度 80 米和 120 米拍到的车宽窄可以相差 40% 以上这对后续做 anchor 配置和训练尺寸的设定影响很大不能按照固定地面监控的标准去处理。这三个问题对应的处理方式分别是标注时严格按完整车身可见边界画框车头车尾被遮挡或者只有半截车身的就归类为难例但要照实标不需要单独注意车牌类的小目标注意力放在车顶特征训练图片尺寸不要一味往大调而是根据大多数车辆在画面里占的像素比例来定 imgsz一般 640 起步如果目标过小再往 768 或 1024 去试。2.2 三种格式的数据骨架与读取方式拿到这份数据集时正常情况下一张图会对应三份标签分别存在于三个文件夹或者三种后缀的文件里。VOC 格式是一份 XML根节点是 annotation里面包含 source、size、object 等子节点每个 object 里是一个 bndbox 节点记录 xmin、ymin、xmax、ymax四个值都是绝对像素坐标坐标原点在左上角。COCO 格式是一个整体的 JSONimages 里注册每张图的 id 和宽高annotations 里按 segments/bbox/category_id 记录每个目标的信息bbox 也是绝对像素坐标但用的是 [x, y, width, height]注意不是两个角点。YOLO 格式是三份里最简单也最容易写错的每张图对应一个同名 txt 文件一行一个目标格式是 class x_center y_center width height四个数值都被归一化到 0~1 之间除以原图的宽和高得到的。三个格式的坐标换算关系大概是VOC 的 xmin/ymin 对应 COCO 的 x/yxmax-xmin 对应 width而 YOLO 的 x_center 就是 xmin 与 xmax 的和除以 2 再除以图宽。读取时确认一个原则任何时候都以图和标签的文件名作为主键去关联不要用目录里的顺序因为遍历目录得到的顺序在不同操作系统上根本不一致。2.3 1000 张训练集的划分与增强策略1000 张图对于检测任务来讲不大不小刚好是一个“能训练出像样模型但容错率低”的量级。这里最关键的是划分而不是数量。按 7:2:1 切成训练集、验证集、测试集是常见做法但要注意必须分层随机抽样不能直接按整个文件夹随机切否则天气、飞行高度、路段类型会分配不均夜间场景往往前 500 张是晴天后 500 张是阴天直接随机切模型验证时的指标会虚高。我做这类项目时习惯先按拍摄条件打标签字段比如光照强度、飞行高度、天气三项然后每个组合内部再随机分配确保验证集里的难例和训练集分布一致。数据增强方面1000 张图必须用增强但不要盲加。YOLO11 自带的训练增强参数里有 hsv_h、hsv_s、hsv_v 三项可以分别调整色相、饱和度和明度夜间任务把 hsv_h 调低到 0.01、hsv_s 调到 0.5 就好重点放在 hsv_v 上因为夜间亮度方差大给到 0.6 左右模型能对过暗和过曝的图片更鲁棒。另外建议加小幅度的 scale把尺度抖动控制在 0.5 左右模拟无人机高度变化带来的目标大小波动比单独把 imgsz 拉大更有效。3. 三格式同包的真正价值是流程兼容别只当它是转换工具3.1 选 VOC 还是 COCO取决于下游读它的是谁很多拿到三格式标签的人会陷入一个误区既然是同一个数据集我只需要一种格式另外两种是冗余。我在实际项目里一般直接劝做检测的用 YOLO 格式训练因为 ultralytics 的训练流程原生读取 txt 标签。但 VOC 和 COCO 格式的存在有它们的下游意义。举个例子如果你要做的是夜间车辆感知的对比实验需要跑检测之外的语义分割或者实例分割方案那么 COCO 格式可以直接接入 mmdetection 或者 Detectron2它们的注册机制默认吃 COCO JSON如果你要做的是传统机器学习的特征分析或者用第三方标注平台复核、重新标注、增补困难样本VOC 的 XML 是这些工具最常见的交换格式因为它的可读性最高单个目标单文件坏了哪一条很容易定位。更现实的情况是很多公开的夜间检测论文和预训练权重是用 COCO 的类别定义来做的你手里的车辆这个类别如果语义定义和 COCO 里 car/truck/bus 对得上直接用 COCO 格式做类别映射会省去大量精力。所以三格式存在的本质是流程兼容要认识到但别被它困住模型训练的原料其实只有 TY 这一种。在往下做之前先检查一下这份数据集给的是单类还是多类。标题只写了“车辆检测”并没有明确说明是否对轿车、卡车、公交车做了细分我处理时建议先检查 labels 目录里任意一个 txt 的第一列数字如果始终是 0 就是单类写在数据集配置文件里的 nc 就是 1如果出现 0、1、2 等不同数字就得按多类处理。千万不要想当然以为一定是单类或者一定是多类按错配 nc 训练会直接报维度错误。3.2 三种格式互相转换限定边界的三段式转换逻辑如果后续要另起炉灶去接其他框架而不仅仅是使用现成的三份标签那掌握转换逻辑还是有必要的至少出了问题能自己排查。我做这类转换一般只依赖 Python 标准库和简单的文本处理不额外引重型依赖代码按“按 VOC 为中间格式向 COCO 和 YOLO 分别转换”来写。import glob import os import xml.etree.ElementTree as ET import json voc_dir labels_voc # 存放所有 XML 的目录 coco_out labels_coco.json yolo_dir labels_yolo os.makedirs(yolo_dir, exist_okTrue) coco_json { images: [], annotations: [], categories: [{id: 1, name: vehicle}], } ann_id 0 for voc_path in glob.glob(os.path.join(voc_dir, *.xml)): tree ET.parse(voc_path) root tree.getroot() img_name root.find(filename).text img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) img_id len(coco_json[images]) coco_json[images].append({ id: img_id, file_name: img_name, width: img_w, height: img_h, }) txt_lines [] for obj in root.findall(object): xmin float(obj.find(bndbox/xmin).text) ymin float(obj.find(bndbox/ymin).text) xmax float(obj.find(bndbox/xmax).text) ymax float(obj.find(bndbox/ymax).text) # 转换到 COCO 格式 coco_bbox [xmin, ymin, xmax - xmin, ymax - ymin] coco_json[annotations].append({ id: ann_id, image_id: img_id, category_id: 1, bbox: coco_bbox, area: coco_bbox[2] * coco_bbox[3], }) ann_id 1 # 转换到 YOLO 格式归一化[0,1]注意 class id 从 0 开始 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h txt_lines.append(f0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) yolo_path os.path.join(yolo_dir, os.path.splitext(img_name)[0] .txt) with open(yolo_path, w) as f: f.write(\n.join(txt_lines)) with open(coco_out, w) as f: json.dump(coco_json, f, indent2)这段代码的逻辑很直白先遍历所有 VOC 的 XML 文件从根节点里提取文件名和图片尺寸然后再遍历里面的每个 object 节点读取 bndbox 的四个角点。转 COCO 时小括号里做了两个角点向中心点加宽高的换算转 YOLO 时把坐标全部除以宽和高归一化同时做了类别 ID 偏移COCO 里第一个类别的 id 是 1而 YOLO txt 里第一个类别必须从 0 开始。这是最容易踩的一个点直接把 category_id 填进第一列会导致模型训练时类别维度对不上。参数方面唯一需要按数据集实际情况改的是第 18 行的 categories 定义如果这份数据是三类比如 0car、1truck、2bus那这里就要列出三个字典同时在代码里把 category_id 写成一个变量而不是固定 1。另一个容易被忽略的点是 img_name 变量有的 XML 里 filename 只有不带路径的文件名有的是绝对路径建议在训练前统一确认一下否则 COCO 的 file_name 对不上实际的图片路径加载时报找不到文件。3.3 标签自检脚本找出空标签、越界框和类别错位拿到一份别人整理好的数据集先别急着开训练花十分钟做一次标签健康检查比训练到一半发现指标上不去再回头查省事得多。我一般做三项检查分别对应目标检测数据集最常出现的三种翻车情况。第一项查空标签。YOLO 格式里有些 txt 文件可能是 0 字节对应这张图里没有任何目标。空标签本身不是致命的ultralytics 会跳过没有标注的图但如果空标签的图太多训练时模型会学出“什么也不输出”的倾向导致漏检率升高。批量检查用 os.path.getsize 判断就可以了。第二项查越界 bbox。txt 里五个数值都声称在 0~1 之间但因为归一化时的分母写错了或者其他格式转换时的 bug可能出现 width 超过 1 或者 x_center 加上一半 width 大于 1 的情况。这类越界框在训练时可以被 ultralytics 的自动清洗逻辑处理掉一部分但被裁剪掉的框如果占比过高就说明标签整体有问题而不是单张错。一个好的检查原则是同一张图的 YOLO 标注换算回像素坐标后不得超出图片宽高的 ±2%超出就得回到原始标注看是谁错了。第三项查类别错位。多类数据集里最容易出现的问题是某个类别的图片分布太极端比如 bus 只出现在 20 张图里模型从头到尾没见过几次测出来 mAP 好看是因为基本没预测出 bus而不是预测对了。这时候用 pandas 给每个类别的框数量做个统计如果某个类别框数少于 50我一般建议直接把它当背景或者并到相近类别里去。from pathlib import Path import numpy as np label_dir Path(labels_yolo) counts {} bad_files [] for txt_path in label_dir.glob(*.txt): with open(txt_path) as f: lines [l.strip() for l in f if l.strip()] if len(lines) 0: bad_files.append((txt_path.name, empty)) for line in lines: parts line.split() cls int(parts[0]) nums np.array([float(v) for v in parts[1:]], dtypenp.float32) if (nums 0).any() or (nums 1).any(): bad_files.append((txt_path.name, out_of_range)) counts[cls] counts.get(cls, 0) 1 print(类别分布:, counts) print(问题文件数:, len(bad_files))这段脚本的核心是先把所有标签文件读进来判断是否为空文件、数值是否越界、类别分布如何。文件数 1000 张时跑起来不超过三秒一次就能排除掉大批低级问题。跑完这三项检查再进训练环节数据侧的风险就基本被提前清掉了。4. YOLO11 三平台一键训练把环境差异关在脚本外面4.1 一键脚本的骨架设备探测、数据配置、参数分层标题里写的“支持 GPU(GPUs)/CPU/Mac 三平台 YOLO11 一键训练”这句话让训练门槛降低的同时也引出了一个问题三套环境底下的训练行为其实差异巨大GPU 和 CPU 之间的速度差距可能达到 20 倍以上Mac 的 MPS 后端支持也不如 CUDA 成熟。真正稳妥的一键脚本必须做两件事一是自动识别设备二是根据设备类型自动调整参数而不是简单地把三个平台硬塞进同一套配置里。设备探测的逻辑很直接优先用 torch 检查 CUDA 是否可用也就是 NVIDIA 显卡然后检查 Apple Silicon 的 MPS最后兜底给 CPU这种做法在 ultralytics 里也对齐容易因为训练时传一个 device 字符串就行。Mac 上跑的机器设置要注意一点MPS 目前对部分算子的支持不完全训练时还需要依赖 PyTorch 的 MPS fallback 机制处理兼容性差的算子。三平台里 CPU 训练最容易忽略的是进程数默认 worker 数在超线程机器上反而恶化性能需要根据插槽手动压一压。参数分层的意思是训练参数分两套一套是平台无关的模型参数比如 imgsz、epochs、类别数另一套是平台相关的运行参数比如 batch 大小、worker 数、是否使能 cache。运行时先做设备探测再根据探测结果覆盖第二套参数这样在 Mac 上训练不会因为 batch 太大触发内存压力而崩溃在 CPU 上也不会因为 cacheTrue 把内存吃光。4.2 GPU / CPU / Mac 三平台的环境配置对照环境配置是这类一键脚本最容易在两三分钟内跑挂的环节。YOLO11 本身通过 ultralytics 这个 Python 库调用而 ultralytics 底层依赖 PyTorch、OpenCV 等组件这三个平台的差别主要集中在 PyTorch 安装源和加速后端的匹配上这里给出一份我实际验证过可行的配置清单。GPU 平台也就是 NVIDIA 显卡 Windows 或 Linux 的环境步骤是先确认显卡驱动支持的 CUDA 版本然后用对应的版本号安装 PyTorch最后安装 ultralytics。常见翻车点是 torch 默认从 PyPI 安装的是 CPU 版CUDA 不可用但不报错导致训练跑在 CPU 上还不知情。CPU 平台最省心直接 pip 安装标准版 PyTorch 就够了Python 包里自动集成 CPU 实现不需要任何额外配置。开 triton 之类的加速推理库对训练没有帮助还容易因为依赖冲突引发编译问题。Mac 平台要区分 Apple Silicon 和 Intel 处理器Apple Silicon 上 PyTorch 自带 MPS 支持安装后的验证方法是打印 torch.backends.mps.is_available()为 True 就说明能用。Intel 版 Mac 没有 MPS 后端只能走 CPU。这里给出环境的关键安装命令# NVIDIA GPU先装和显卡驱动匹配的 PyTorch再装 ultralytics pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install ultralytics # CPU 或者 Apple Silicon Mac直接走默认源安装的顺序是先 torch 再 ultralytics pip install torch torchvision pip install ultralytics安装完不急着训练先用一段极短的代码验证设备可用性和 torch 版本匹配关系这一步相当于看看火药桶有没有毛病。import torch print(PyTorch:, torch.__version__) print(CUDA 可用:, torch.cuda.is_available()) print(MPS 可用:, torch.backends.mps.is_available())这段验证逻辑是从左到右逐条排查的思路先看 torch 版本再逐项打印 CUDA 与 MPS 的可用状态。如果 CUDA 显示 False检查显卡驱动和 PyTorch 的版本号里是否带 cu121 之类的后缀如果 MPS 显示 False检查机器是不是 Apple Silicon 芯片。三行输出能帮助快速区分环境问题与代码问题节省大量排错时间。4.3 能直接跑起来的训练脚本与 10 个必调参数环境就绪后整个项目里最核心的就是训练脚本了。标题里提到的“一键”应该做成这样合上数据配置文件就能启动训练启动后自动根据当前机器的硬件选择设备同时按设备调整训练参数。我给出一个可以直接复制的版本这个脚本在 Windows、Linux 和 macOS 上都能跑只要你用的是 Python 3.9 以上版本。from pathlib import Path import torch from ultralytics import YOLO DATA_YAML night_drone.yaml # 数据配置文件包含 train/val 路径和类别名 def detect_device(): if torch.cuda.is_available(): return 0 # 单卡直接写 0多卡可以返回 0,1,2 if torch.backends.mps.is_available(): return mps return cpu def main(): device detect_device() print(f[INFO] 当前使用设备 - {device}) # 按设备类型给训练参数做默认值覆盖 if device 0: epochs, batch, workers 100, 16, 8 elif device mps: epochs, batch, workers 100, 8, 4 else: epochs, batch, workers 60, 4, 2 model YOLO(yolo11n.pt) # 预训练权重YOLO11n 是轻量版 results model.train( dataDATA_YAML, epochsepochs, imgsz640, batchbatch, devicedevice, workersworkers, cacheTrue if device ! cpu else False, # CPU 上读图缓存会爆内存 patience15, projectruns/night_vehicle, nameyolo11n_base, exist_okTrue, pretrainedTrue, optimizerauto, verboseTrue, ) print([INFO] 训练完成权重保存在, project) if __name__ __main__: main()代码的走向很清晰它先通过 detect_device 函数拿到当前机器的设备标识然后根据设备类型预置三组不同的训练参数接着加载 YOLO11 的轻量基座模型用一行 model.train 启动训练。设备标识有三种情况NVIDIA 显卡返回设备号字符串Apple Silicon 返回 mps 字符串其他情况返回 cpu。训练参数里预置不同 batch 和 worker 的道理在于GPU 显存大、读取快可以开大 batch 和 workerMac 的 MPS 后端虽然有 GPU 算力但显存与 CPU 共用batch 开大了容易触发内存膨胀CPU 则是全瓶颈在算力过大 worker 反而因为调度开销拖慢速度。脚本里 10 个参数值得单独说明一下。data 指向的 night_drone.yaml 是这个项目的数据配置文件里面必须写清楚 train 和 val 两段的绝对路径以及类别名列表注意 YOLO 格式里索引从 0 开始也就是第一个类别对应数字 0。imgsz 是训练时的输入尺寸640 是 YOLO11 的基线设置如果目标偏小可以换成 768 或 1024代价是显存和训练时间几乎翻倍。epochs 用常规 100 轮起步配合 patience15 早停策略意思是连续 15 轮验证集指标不涨就提前结束所以不需要担心一上来不知道怎么定轮数。optimizer 设置成 auto让 ultralytics 按模型规模自动选优化器省去调 AdamW 与 SGD 的时间。exist_ok 和 project 两个参数控制输出目录保证重复训练同一任务时自动写入同名目录而不是新增乱序文件夹。数据配置文件 night_drone.yaml 的写法也直接贴出来照着改路径就能用path: /absolute/path/to/your/dataset train: images/train val: images/val names: 0: vehicle这里有三个需要留意的点path 建议写绝对路径相对路径在终端工作目录变化时容易出现找不到训练图片的报错images 目录和 labels 目录必须是兄弟目录这一条在 ultralytics 中写死names 字段的字典顺序会影响模型输出测出数字标号一旦训练完想换顺序就得重新训练真踩上了只能认栽。先确认这三处再跑训练速度和心情都会好不少。4.4 训练完看什么weights、results.csv 与混淆矩阵训练完的产物默认会落在 project 和 name 拼出来的 runs/night_vehicle/yolo11n_base 目录下里面最重要的有 weights/best.pt 和 weights/last.pt、results.csv、混淆矩阵图三个东西。best.pt 是验证集上 mAP 最高那轮的权重项目落地时就用它last.pt 是指最后一轮的权重如果需要从头续训用它。results.csv 记录每一轮的 train_loss、val_loss、mAP50、mAP50-95 等指标变化我习惯直接拉到最后 10 行看 val 指标是否还在上升如果在上升就说明训练轮次可能不够需要关掉早停或者加轮次。混淆矩阵图是判断夜间车辆漏检与误检的关键材料如果右上角的车灯误检成车辆比较严重一般是负样本不够考虑补图或者回退到性能更强的 base 模型。给新手一个判断门槛夜间车辆检测这类小目标任务mAP50 能过 0.75这个数据集基本就算用出价值了。5. 夜间车辆检测的 5 个踩坑记录现象、原因、解法5.1 训练三分钟就爆显存报 CUDA out of memory现象是脚本启动后前两个 epoch 还很正常第三个 epoch 刚开始就报 RuntimeError: CUDA out of memory而且这台显卡跑别的检测任务没出过问题。原因基本有三个一是 imgsz 设太大夜间无人机视角下目标普遍小很多人喜欢直接拉到 1024 训练显存占用按平方关系上涨二是 batch 太大三平台脚本为了追求速度把 batch 顶到 16 甚至 32但显存小的显卡根本装不下三是开启 cacheTrue 后 ultralytics 会把所有图片一次性加载进显存1000 张图 1024 分辨率下大概吃 6GB 显存很多显卡直接破防。解法是临时把 batch 调成 4imgsz 调回 640cache 关掉先确认能正常跑完一个 epoch再逐步加 batch 和尺寸。显存够不够可以先用 torch.cuda.max_memory_allocated 打印峰值心里有数再往上调不建议一上来就往保守了去训练速度会白白浪费。5.2 三份标签对不上数训练报 label 缺失警告现象是训练日志里出现 WARNING empty label或者某个 txt 文件的内容明显是另一张图的标注。原因来自数据集打包环节三种格式不是从同一份原始标注生成的而是各自独立导出。比如 VOC 里的 filename 和文件系统里的实际图片名不一致或者 YOLO txt 是从 COCO JSON 转出来的过程中 image_id 没有按文件名对齐。解法是回头用前面 3.3 节的自检脚本做一次全量对比把三个格式里所有文件的基础名提取出来求交集只保留三者共有的那一批图片。我做过一次这种处理最后砍掉 20 张图才恢复正常但训练指标稳定了很多。认真说一句三格式标签给全是一种交付姿态但使用前校验永远是自己的责任。5.3 Apple Silicon 上训练卡死进度条不动也不报错现象是在 M1 芯片的 MacBook 上启动训练前几秒日志正常打印之后整个进程像冻住一样CPU 占用率一直很高但训练进度条纹丝不动。原因在于 torch 的 MPS 后端在部分算子图组合下会产生死锁尤其是 batch 内图片尺寸不一致导致 pad 行为变化时容易触发。这种死锁不报异常排查起来特别费时间。解法是我在前面脚本里把 MPS 的 batch 压低到 8同时设置环境变量 PYTORCH_ENABLE_MPS_FALLBACK1 让不支持的算子自动回退到 CPU 实现两条同时用上卡死概率能降八到九成训练也稳一些这套调整目前在我接触过的 M 系列机器上都适用。5.4 验证集 mAP50 看起来不错实拍视频里漏检严重现象是训练集和验证集指标双双过 0.8但把模型接到一段城市道路的夜间实拍视频里大量车辆被漏检尤其转弯处和坡道上。原因是验证集划分不合理没有按亮度、天气和飞行高度分层导致验证集里全是简单样本。模型本质上只是背下了训练数据里车灯和车身对比度较好的形态灯光再乱一点的场景直接废掉。解法是回到 2.3 节说的分层抽样思路把验证集里加入雨天、逆光、过曝这三种最野的样本宁可验证集指标掉到 0.7也要保证实拍效果的参考性。夜间数据这里是最值得为验证集多花时间的因为数据量小、分布散好验证集比好模型更重要。5.5 车灯过曝把车“吃”掉了框只能框到半个车身现象是训练出来的模型对停在暗处的车能干净检测但遇到大灯近距离照射、镜头逆光、路灯强光直射入镜时模型只框出灯光周围的一小块把大部分车身漏在外面。原因是夜间样本里车身和背景对比度本来就低增强时又过度调高了 hsv_v导致模型学到的特征是“亮的部分就是车”灯的亮度盖过了车身暗部纹理。解法是训练时把 hsv_v 从默认的 0.4 回降到 0.2 左右同时把 her0 和 flipud0 这两个参数关掉车辆正立方向的先验信息对夜间任务很重要上下翻转会让模型彻底分不清路面与天空这个坑在夜间数据上比白天更明显。6. 用切小块验证 CLAHE 增强把夜间小目标再榨一榨6.1 切块推理验证让小车在 640 视野里“变大”夜间无人机视角的车辆在整张图里往往只占二三十个像素YOLO11 即便把输入拉大到 1024小目标依旧很难被稳定召回。我一般会把训练好的模型用切图推理验证一次做一个低成本目标大小评估把原图按一定步长滑窗切成若干个重叠的子图分别推理再按坐标还原。这样做的好处是等于是强行让模型在更小的视野里看一个“放大”的车把相对尺度拉上来。用这份数据集做完之后我对模型的小目标能力有个基本判断如果切块后比整图推理多召回 20% 以上说明小目标主要死于尺度而非纹理特征后续可以考虑切片训练或者加 P2 层。6.2 CLAHE 预处理低成本把车从暗部里捞出来如果训练完发现夜间暗部的车辆召回依旧不理想我会先试一个简单且几乎零成本的预处理CLAHE 局部对比度增强把暗部细节拉出来再喂给模型做训练或推理。这个操作不需要重新标注数据适合做训练增强的补充试验核心参数是 clipLimit 和 tileGridSize 两组处理车灯过曝场景比全局直方图均衡更稳。import cv2 def clahe_preprocess(img): lab cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l_ch, a_ch, b_ch cv2.split(lab) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) l_ch clahe.apply(l_ch) merged cv2.merge((l_ch, a_ch, b_ch)) return cv2.cvtColor(merged, cv2.COLOR_LAB2BGR)这段逻辑是把 BGR 图像转到 LAB 色彩空间对亮度通道单独做 CLAHE再把三个通道合并回去。参数 clipLimit 控制对比度增强强度夜间场景给 2.0 到 3.0 比较合适太大会放大噪声tileGridSize 控制局部窗口大小默认 8×8 就行窗口太小会出现块状伪影。加了这一步之后再实测夜间视频暗色车身的召回率往往能涨几个点虽然不会逆转小目标的物理限制但属于投入产出比较高的土办法。做夜间检测浮层实验时这个思路也可以迁移到其他低照度数据上先做预处理再进模型比直接换模型结构来得实在。我踩了这么多次坑后养成的习惯是拿到这一类数据集先跑基线、再查数据健康度、最后做小目标专项验证希望这三板斧和这份训练脚本能帮你把夜航检测少走几个来回。本文还有配套的精品资源点击获取