☰
YOLOv5红外小目标检测:夜间飞鸟与无人机识别方案
2026/9/28 17:07:54 网站建设 项目流程

简介:这是一套面向红外场景下微小无人机、直升机、飞机、飞鸟四类目标检测的完整资源,适合目标检测研究者,也适合低空安防、反无人机、交通监控等应用开发者。内含训练好的YOLOv5模型权重,以及4000多张红外图像数据集,标签已经整理为YOLO格式,并预先划分好train、val、test集合,提供data.yaml配置文件,使用YOLOv5、YOLOv7、YOLOv8等主流算法时可直接加载训练。压缩包共2000个文件,以标注文件为主,另有3个Python脚本和3份PDF教程,整体体积约587.15MB,便于离线使用和学习。配套的PyQt5图形界面支持对图片、视频以及调用摄像头进行实时检测,可快速验证模型效果;PDF教程覆盖YOLOv3至YOLOv8的环境配置和PyQt5界面使用说明,能显著减少环境搭建与推理部署中遇到的常见问题。目前已有548人学习下载,适合需要红外小目标数据、预训练权重与可运行交互界面的开发者直接上手。

1. yolov5 红外小目标检测:夜间飞鸟与无人机场景下的一个现成落地方案

晚上用红外相机对着天空,飞鸟、无人机在画面里只是几个像素大小的亮斑,普通检测模型在这种场景下基本不可用,而 yolov5 配合红外灰度数据训练后,依然能把 Airplane、Bird、Drone、Helicopter 四类目标稳定框出来。这份资源就是把整套方案打包好了:4000 多张已划分 train/val/test 的红外小目标数据集、配套的 data.yaml、训练好的 yolov5 权重,外加一个支持图片、视频、摄像头三种输入的 PyQt 界面和两份环境配置教程。你不需要从抽帧、标注开始折腾,解压配好环境就能先看到检测效果,再决定要不要继续复训。

适合谁:想快速验证红外小目标检测方案、又没时间自己攒数据的从业者。我拆这套资源时最关注三件事——数据集类别分布是否均衡、权重和类别顺序对不对得上、PyQt 界面里推理线程会不会卡死窗口。后面逐一说清楚,坑都提前替你们踩过了。

2. 数据集的真实结构:从 Roboflow xml 到 YOLO txt 的组织与校验

2.1 目录结构:images 与 labels 的对应关系

解压后数据在 datasets/IRmini 下,train、val、test 分开,每部分内部都有 images 和 labels 两个子目录。images 里是红外 JPG 帧,labels 里是同名 txt。txt 每一行表示一个目标框:

3 0.421875 0.536542 0.024305 0.037514 1 0.689236 0.710938 0.019097 0.031654 0 0.350694 0.432292 0.013889 0.020833

每一列含义:class_id、x_center、y_center、width、height。五个值都是归一化到 0~1 的浮点数,不是像素坐标。class_id 从 0 开始,对应 data.yaml 里 names 数组的下标:0 是 Airplane,1 是 Bird,2 是 Drone,3 是 Helicopter。如果 txt 里出现 class_id=4,要么标签错了,要么 names 没对齐。顺带说一句空标签的事,Roboflow 导出时如果原始图中目标太小被过滤掉,就会留下「图片在、txt 空」的文件,统计时单独列出来就行,不必删图。

拿到任何一个目标检测数据集,第一步不是直接开训,而是先跑一遍统计脚本,看类别分布和标签完整性。我一般会写十几行检查代码:

# check_labels.py —— 统计每个类别的目标框数量,找出空标签 import os from collections import Counter label_dir = "datasets/IRmini/labels/train" stats = Counter() empty_files = [] for name in sorted(os.listdir(label_dir)): if not name.endswith(".txt"): continue path = os.path.join(label_dir, name) with open(path, "r", encoding="utf-8") as f: lines = [line.strip() for line in f if line.strip()] if not lines: empty_files.append(name) continue for line in lines: cls_id = line.split()[0] stats[cls_id] += 1 print("各类别框数量:", dict(stats)) print("空标签数量:", len(empty_files)) print("空标签示例:", empty_files[:10])

逻辑很简单:遍历训练标签目录,逐行读取,第一列是类别 id,累加到 Counter;txt 完全无内容就记进 empty_files。输出里如果某个类别框数量明显偏少,比如 Drone 只有一两百个框,另外三类各自上千,那模型在 Drone 上的表现大概率不行,需要补数据或做类别权重。空标签文件数量也顺手打出来,数量太多就说明数据清洗环节没做好,训练时这些图会反复贡献背景 loss。

2.2 data.yaml 的配置与 xml 备份的作用

data.yaml 是这个数据集的入口配置,内容如下:

train: /path/to/IRmini/train/images val: /path/to/IRmini/val/images test: /path/to/IRmini/test/images nc: 4 names: ['Airplane', 'Bird', 'Drone', 'Helicopter']

train、val、test 三个路径指向对应划分的 images 目录。yolov5 训练时只读 train 和 val,test 在 val.py 做最终评估时才用。nc 必须和 names 长度一致,这条很多人改漏:改了 names 忘了 nc,训练直接断言报错。names 的排列顺序决定 txt 里 class_id 的语义,所以如果自己的类别集合跟这套四类不同,names 要整体替换,不能只删一个名字。

压缩包里还带了一批 xml 文件,文件名类似 IR_BIRD_027_frame_173_jpg.rf.ad13a23f0a417eaef8b89cdbf3d571b3.xml。从命名能看出原始来源:IR 是红外,BIRD 是场景标签,027 是视频段编号,frame_173 是第 173 帧——这套数据集是从多段红外视频抽帧得到的。rf 是 Roboflow 导出时生成的随机文件 id,xml 本身是 Pascal VOC 格式,里面记录 object 的 name 和 bndbox。也就是说,txt 标签可以直接喂 YOLO,xml 是原始标注备份,方便日后重新划分数据集时从源头转换,不用回到标注工具里手工改。

2.3 VOC xml 转 YOLO txt 的脚本与易错点

如果想在原有标签基础上重新划分数据集,比如把 7/2/1 的比例改成 8/1/1,就要拿 xml 重新生成 txt。转换脚本不长,核心是坐标归一化:

# xml_to_txt.py —— 把 Pascal VOC xml 转换成 YOLO 格式 txt import xml.etree.ElementTree as ET class_names = ['Airplane', 'Bird', 'Drone', 'Helicopter'] def convert(xml_path, out_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = float(size.find("width").text) img_h = float(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_names: continue cls_id = class_names.index(name) box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) cx = (x1 + x2) / 2.0 / img_w cy = (y1 + y2) / 2.0 / img_h bw = (x2 - x1) / img_w bh = (y2 - y1) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(lines) + "\n")

三个易错点。第一,bndbox 的坐标先转成 float 再做除法,不要整型直接相除,否则归一化精度直接丢。第二,有些 xml 的 xmax 比图像宽度大一个像素,这是标注工具的老毛病,转换前最好 clamp 一下,否则框越界。第三,xml 里出现 names 之外的类别时,脚本里是 continue 跳过,可能静默丢标签,建议改成打印警告。转换完再用 check_labels.py 统计一遍,对比前后类别框数量是否一致,这步能拦住九成的手滑。

提示:txt 里 class_id 从 0 开始,xml 里类名是字符串,转换脚本里 class_names.index(name) 就是做这个映射。如果之后改了 names 顺序,旧 txt 的语义就全变了,训练前务必重建一次标签。

3. 环境配置与模型加载:把教程 PDF 里的坑提前绕开

3.1 两份环境配置 PDF 的版本组合逻辑

资源里带了两份环境配置教程 PDF,覆盖 yolov3 到 yolov8 的通用环境搭建。这类教程在 Windows 下最常见的路线是 Miniconda + PyTorch GPU 版,我复现时按最常见的组合走:Python 3.8 + CUDA 11.1 + PyTorch 1.8.1 + torchvision 0.9.1。这套组合和 yolov5 5.0 前后的代码兼容性最稳,教程 PDF 里的步骤也基本是按这个方向讲的。

组件推荐版本说明
Python3.8yolov5 老版本代码兼容最好
CUDA11.1匹配 cu111 的 wheel
PyTorch1.8.1稳定,不必追新
torchvision0.9.1必须与 torch 1.8.1 配套
opencv-python4.x负责图像读写和摄像头采集

如果你的显卡较新也没关系,驱动支持 CUDA 11 以上的都可以装这套。注意用 conda 创建独立环境,别动系统 Python:

conda create -n yolo python=3.8 -y conda activate yolo pip install torch==1.8.1+cu111 torchvision==0.9.1+cu111 -f https://download.pytorch.org/whl/torch_stable.html

+cu111 表示 CUDA 11.1 编译的 wheel,torchvision 必须跟 torch 对应。装完马上验证 GPU 是否真的能用:

import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))

第二行输出 False,说明装成了 CPU 版或驱动太旧。这个验证步骤虽然基础,但能拦住后面一半以上的玄学报错。我见过机器有集显和独显两块卡,CUDA 默认设备号不对,推理跑到了核显上,速度惨不忍睹,排查半天才发现。

3.2 用 test.py 跑一次推理:参数说明

环境配好后,解压权重文件,在项目根目录执行:

python test.py --weights best.pt --source data/test/images --conf 0.35 --img 640

--weights 指向训练好的权重;--source 可以是单张图片、整个目录、mp4 视频或摄像头索引;--conf 是置信度阈值,默认 0.25,这个参数对红外小目标影响很大;--img 是推理输入分辨率,默认训练是 640 就保持 640。如果你的 test.py 是二次封装过的,先执行 python test.py --help 看参数列表,有些封装会把 --conf 改成 --thresh,字段名不同。

权重文件注意区分 best.pt 和 last.pt:best 是验证集上指标最优的保存点,部署用它;last 是最后一个 epoch 的保存点,一般只用来继续训练。跑完输出在 runs/detect 下。第一次跑建议挑一张包含多个目标的红外帧单独试,别直接上整个测试集——红外测试集里可能混着大图,推理耗时长,容易被误判成卡死。

3.3 置信度阈值、输入尺寸对小目标的影响

这套模型处理的是红外小目标,目标在 640×640 输入里往往只占不到 20×20 像素。置信度阈值从 0.25 调到 0.35,能肉眼可见地减少误检,但也会把低置信度的真目标一起滤掉。我习惯先按 0.1 跑一遍,把所有框的置信度记录下来,观察真目标和误检的分布区间,如果两者在 0.2 附近有明显分界,就取中间值当阈值。只看一两张图就拍板阈值,后面换一段视频必翻车。

输入尺寸可以往下调,--img 480 推理速度能快 30% 左右,但小目标本来就小,降分辨率等于自废武功。往上调到 1280 对小目标提升明显,显存占用翻倍,RTX 3060 12G 勉强够,6G 就别想了。资源里的权重如果是在 640 下训练的,推理用 1280 也能出框,只是 CPU 上会慢到没法用。

4. PyQt 界面拆解:图片、视频、摄像头三路输入的实现思路

4.1 share.py 与 test.py 的分工

压缩包里代码文件不多:share.py、test.py、init.py。从命名和调用关系看,share.py 是公共推理模块,负责加载模型、前处理、把推理结果整理成界面能显示的数据;test.py 是 PyQt 主程序,负责画界面、接按钮事件、显示画面;init.py 把目录变成包,让 test.py 里可以写 from share import Detector 这类导入。这种拆分比全部逻辑堆在一个文件里干净:界面代码不需要知道模型内部怎么做推理,换权重时只改 share.py 里的路径,不动界面逻辑。配套的 pyqt5 使用说明 PDF 里应该有完整的界面操作流程,照着点一遍就知道三个入口分别对应什么参数。

我的建议是:不要直接改 test.py 去适配你的业务,先把 share.py 里的模型加载部分独立出来,在命令行验证通了,再接回界面。界面调试成本远高于命令行,底层先跑稳,再谈按钮和布局。

4.2 视频和摄像头检测为什么必须用 QThread

图片检测很简单,按钮点击后拿文件路径,调一次推理函数,把结果图转成 QImage 显示到 QLabel,耗时几百毫秒,主线程卡一下能接受。但视频和摄像头是持续流,如果每帧的读取、推理、显示都放在主线程里,Qt 事件循环被长时间占用,窗口会直接转圈假死。这不是代码 bug,是线程模型问题。

常见做法是开一个 QThread,在 run() 里循环读帧,推理完成后通过信号把结果帧抛回主线程刷新:

# 摄像头/视频检测线程骨架 from PyQt5.QtCore import QThread, pyqtSignal import cv2 class DetectThread(QThread): frame_ready = pyqtSignal(object) def __init__(self, detector, src=0, every_n=3): super().__init__() self.detector = detector self.src = src # 0 表示默认摄像头,也可以是视频文件路径 self.every_n = every_n # 每 N 帧做一次推理,其余直接显示原帧 self._running = True def run(self): cap = cv2.VideoCapture(self.src) if not cap.isOpened(): self.frame_ready.emit(None) return idx = 0 while self._running and cap.isOpened(): ok, frame = cap.read() if not ok: break if idx % self.every_n == 0: frame = self.detector.infer(frame) self.frame_ready.emit(frame) idx += 1 cap.release() def stop(self): self._running = False self.wait()

几个关键点:every_n 用来降频推理,比如每 3 帧才检测一次,中间 2 帧直接显示原帧,画面流畅度不会太差,推理负载也降了;stop 里的 wait() 是等线程内循环退出再关闭程序,否则退出时容易崩;detector.infer(frame) 返回画好框的帧,界面槽函数收到后刷新 QLabel 就行。信号传递的是 numpy 数组,pyqtSignal 里用 object 类型,不要用 QImage——跨线程构造 QImage 容易出问题。

4.3 摄像头打不开、画面延迟两个高频问题

摄像头打不开,十个里有八个是索引问题。笔记本自带摄像头不一定是 0,外接 USB 摄像头可能占 1 或 2。先撇开 PyQt,单独跑几行验证:

import cv2 cap = cv2.VideoCapture(0) if not cap.isOpened(): print("camera 0 failed") else: ok, frame = cap.read() print(ok, frame.shape if ok else None) cap.release()

索引从 0 换到 1、2 逐个试。全打不开就检查摄像头是不是被其他软件占用,Windows 下微信、钉钉这类软件占着摄像头,PyQt 程序自然抢不到。画面延迟超过 200ms 时,优先调大 every_n 而不是降分辨率——红外小目标对分辨率敏感。实在不行再把推理输入尺寸从 640 降到 416,但最好在界面里留一个开关,方便对比检测质量,而不是拍脑袋定死。

5. 避坑记录:这套红外目标检测资源复现时的 5 个关键坑

5.1 训练一开始 loss 就变成 nan

现象:用这套数据集在 yolov5 上开训,前几十个 batch 的 loss 直接变成 nan,训练中断。

原因:红外灰度图的像素分布和自然图像差异大,默认学习率 0.01 对这个数据偏高,前向传播过程梯度爆炸;另外如果标签里类别 id 超出 nc,也可能在 loss 计算阶段出问题。

解决:把学习率降到 0.001 并开启 warmup,yolov5 的 hyp 文件里 lr0 和 lrf 都调小;同时用 check_labels.py 确认所有 txt 的 class_id 在 0~3 以内。我复现时直接用低学习率的 hyp 配置文件,全程没有出现 nan。

5.2 Drone 类别漏检严重、Bird 误检一堆

现象:推理时 Bird 频繁出框,Drone 几乎检不出,或者把 Bird 大量识别成 Drone。

原因:类别框数量不平衡,Bird 样本可能占了一半以上,Drone 样本少,模型学不够。低分辨率红外图里鸟和无人机外形高度相似,错误分类会被放大。

解决:先跑统计脚本看各类别框数分布,悬殊超过 5:1 就先做样本均衡,而不是急着调网络结构。快速做法是在 data.yaml 里加类别权重,或对少样本类别提高 loss 权重;正规做法是补 Drone 标注数据,用原始红外视频抽帧后只标 Drone。数据分布决定模型上限,调参只是补救。

5.3 PyQt 界面点开始按钮就转圈假死

现象:在 test.py 界面里点「开始检测」,窗口立刻无响应,过几分钟才恢复。

原因:推理循环写在了主线程的按钮槽函数里,Qt 事件循环被阻塞,界面无法处理重绘和鼠标事件。

解决:按 4.2 的 QThread 结构,把视频读取和推理移到子线程,主线程只负责接收信号并刷新 QLabel。改完以后即使推理慢,窗口也能拖动、能点停止。这个坑只在视频和摄像头场景出现,所以很多人图片功能调试正常,一接摄像头就暴露。

5.4 换了自己的数据集后报类别数不匹配

现象:把自己的标注数据替换进目录,用资源里权重继续训练,报类似 class id 4 not in names 的断言错误。

原因:data.yaml 的 names 改成了自己的类别,但权重文件还是四类的输出头,模型最后一层卷积通道数和新的类别数对不上;旧训练缓存没有清理。

解决:换数据时删掉 runs/ 下之前训练产生的缓存文件。如果目标类别数量变化,不要用旧权重直接续训,先从 COCO 预训练权重开始,或 --weights '' 从头训练。想省时间用资源原权重微调也可以,前提是类别数和 names 完全一致。

5.5 mAP@0.5 很高但实际红外图漏检

现象:val 阶段 mAP@0.5 到 0.8 以上,看着很好,实际对着红外监控画面跑,很多真目标没框出来。

原因:mAP@0.5 只看 IoU 阈值 0.5 的匹配,红外小目标框只有十几个像素,偏移五六个像素 IoU 就掉到阈值以下,指标虚高掩盖了定位精度差;同时测试集和真实场景分布有差异。

解决:评估时看 mAP@0.5:0.95,这是 COCO 风格的多 IoU 平均指标。两者差距大说明框不准,复训时把输入分辨率提到 1280,或对回归分支增加 IoU 损失权重。红外模型评估我一律先看 mAP@0.5:0.95,低于 0.5 就认为没到部署标准。

6. 验证模型精度与进一步复训:分类别 AP 与迁移调整

6.1 用 val.py 输出分类别 AP 再决定下一步

模型好不好,不能只看总 mAP,要把每个类别的 AP 单独打出来。用 val.py 跑一遍测试集:

python val.py --data data.yaml --weights best.pt --img 640 --conf 0.001 --iou 0.6

--conf 0.001 是为了评估时不丢候选框;--iou 0.6 是 NMS 的 IoU 阈值,评估场景常用。跑完在 runs/val/exp 下生成 results.csv 和各类别 AP 明细。如果 Bird 的 AP 0.9、Drone 的 AP 0.4,就别讨论模型结构了,老老实实补 Drone 数据。如果四类 AP 都及格,只是某个方向框偏了,再考虑调输入尺寸或数据增强。

6.2 冻结 backbone 的迁移复训参数

要在自己的红外场景继续提升精度,用资源里的权重做迁移学习是最快路径。我一般冻结前 10 层骨干参数,只训练颈部加检测头:

python train.py --data data.yaml --weights best.pt --freeze 10 --img 640 --epochs 100 --batch 16

同时把 hyp 文件里的 hsv_h 调成 0——红外图本质是灰度图,色相增强不仅无效还会干扰训练;hsv_s 也可以调低。最后确认 batch size 和显存匹配,6G 显存跑 640 分辨率 batch 16 比较稳,12G 可以上 batch 32。

从那以后我每次拿到一套红外权重,都强制先跑一遍 val.py 看分类别 AP,再谈部署和二次复训——被 Drone 漏检坑过一次就长记性了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询