简介:面向计算机视觉与目标检测开发者的红外微小目标检测完整方案,聚焦无人机、直升机、飞机、飞鸟四类目标的识别与定位;压缩包内共2000个文件,以XML标注文件为主,辅以Python脚本和3份PDF教程,总体积587.15MB。资源既包含基于yolov5训练好的模型权重,也提供4000余张红外图像数据集,数据已划分train/val/test,并附带配置好的data.yaml,开发者可直接基于yolov5、yolov7或yolov8进行迁移训练或二次优化。配套的PyQt图形界面支持图片、视频和摄像头实时检测,便于快速搭建演示系统或验证模型效果;3份PDF教程覆盖yolov3至yolov8的环境配置与PyQt使用说明,从数据准备、训练到部署给出完整指引。目前已有546人学习,适合需要快速落地红外小目标检测项目的研究人员、学生及工程开发者参考。

1. 红外微小目标检测为什么要把无人机和飞鸟放进同一个模型

反无人机和低空空域监控里,目标在红外画面里往往只有几个像素到几十个像素,直升机旋翼的热特征会被下采样抹成噪点,飞鸟又天生和无人机共享一部分运动特征。拿通用 YOLOv5 的 COCO 权重直接跑这种红外小目标场景,漏检和虚警会轮流出现,误报高到没法用。这套「YOLOv5 红外微小无人机-直升机-飞机-飞鸟目标检测模型权重 + 4000 数据集」的价值在于把同一光电平台上最容易被混淆的四类目标放在一个推理链路里,配合 PyQt 界面,从权重、数据到可视化推理是一条能落地的检测流程。下面按数据集口径、训练超参、界面集成的顺序把能复现的部分展开。

2. 4000 张红外数据集的类别划分、标注口径与预处理

2.1 四类目标的样本量与平衡策略:别让飞鸟吃掉无人机

标题里的 4000 张数据集是个偏中小规模的数据集,四类目标里最容易出现的问题是飞鸟样本过多、无人机样本偏少。因为飞鸟在野外红外场景里几乎随处可采,无人机却需要专门放飞或者找固定翼机型来采集,采集成本高出一截。如果不做平衡,模型会倾向把"低置信度的空中动目标"判成飞鸟,反无人机场景里这就是漏警。

一个常见的做法是按类别分层切分训练集和验证集,而不是直接 train_test_split 整个目录。下面这段是按类别比例划分的基准写法:

from pathlib import Path
from sklearn.model_selection import train_test_split

DATA_DIR = Path("ir_dataset/images")
train_files, val_files = [], []

for cls in ["drone", "helicopter", "airplane", "bird"]:
    imgs = sorted((DATA_DIR / cls).glob("*.jpg"))
    tr, va = train_test_split(imgs, test_size=0.2,
                              random_state=42, stratify=[cls] * len(imgs))
    train_files.extend(tr)
    val_files.extend(va)

这里 stratify 对单类列表没有实际分层作用,真正要点是对四个类别分别执行切分,保证每一类在验证集里都有固定比例。随机状态固定为 42,便于复现和对比不同轮次的训练。实际项目中我会先把无人机类放到 1200 张左右,直升机和飞机保持在 300 到 500 张,飞鸟控制在 2000 张以内,宁可对飞鸟类做下采样也不让它超过一半。

2.2 标注口径:微小目标用 YOLO 格式与框边界的取舍

数据集的标注格式按 YOLO 惯例保存为与图片同名的 .txt ,每行是 class x_center y_center width height ,坐标经过归一化。对这种几个像素到几十像素的微小目标,框回归误差会被放得很大,标注时要把目标完整包住,但不要为了"框得稳"刻意扩边,扩边会让两个相邻目标合并成一个。

# 按图中场景另存为 infrared.yaml 后,检查标签与图像是否一一对应
python check_detections.py --data infrared.yaml

没有这个脚本就先跑一段目录级检查:

import os
img_dir = "ir_dataset/images/train"
lbl_dir = "ir_dataset/labels/train"
broken = 0
for name in os.listdir(img_dir):
    if not name.endswith(".jpg"):
        continue
    txt = os.path.join(lbl_dir, os.path.splitext(name)[0] + ".txt")
    if not os.path.exists(txt) or os.path.getsize(txt) == 0:
        broken += 1
print("missing or empty labels:", broken)

这段代码把"图片有但标签缺失"的文件数统计出来,空 txt 文件训练时会被 YOLOv5 忽略,但会拉偏批次分布。目标边缘占整张图 1% 以下的样本,要重点检查标注框是否落在下采样后仍可辨识的像素范围内。

2.3 16 位红外原始图像转 8 位的预处理链路

大部分红外设备输出的是 16 位 RAW,而通用 YOLOv5 数据加载链路默认按 8 位图处理。把 16 位数据直接除以 256 转 8 位会损失大量暗部细节,因为飞机蒙皮和鸟体热辐射本来就集中在低灰度段。一般做法是按百分位截断再拉伸,配合 CLAHE 提升局部对比度:

import cv2
import numpy as np

def ir_16bit_to_8bit(raw):
    raw = np.float32(raw)
    lo, hi = np.percentile(raw, (1, 99.5))
    img = np.clip((raw - lo) / (hi - lo) * 255.0, 0, 255).astype(np.uint8)
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    return clahe.apply(img)

这里 percentile 取 1% 和 99.5% 是在切掉传感器暗电流噪声和高亮过热区,这两个值要按实际直方图调整;如果目标是天空背景下低对比度的无人机,把 99.5 改成 99.9 能保留更多可用灰度。CLAHE 的 clipLimit 不宜调大,红外小目标本身信噪比低,对比度提升过头会把背景云层纹理放大成假目标。

2.4 数据增强的取舍:Mosaic 和 scale 对微小目标的副作用

YOLOv5 默认增强对中大型目标友好,但对红外微小目标反而是负担。Mosaic 会把四张图拼成一张,原本就小的目标再缩一半,经过骨干网络下采样后特征几乎消失。scale 增强同理,0.9 的缩放幅度可以把 8 像素的目标压到 4 像素。

# hyp.ir.yaml —— 从 hyp.scratch-low.yaml 拷贝后修改
mosaic: 0.5
mixup: 0.0
scale: 0.15
flipud: 0.0
hsv_h: 0.0
hsv_s: 0.0
hsv_v: 0.0

scale 降到 0.15 表示只允许 15% 的尺度抖动,保证目标的像素尺寸不会跌破训练分布下限; flipud 关闭是因为红外航拍图像里天空与地面语义不对称,上下翻转后模型会把地面热源学成空中目标;HSV 增强对单通道红外图没有意义,反而会引入伪色噪声。对这些增强参数的验证,最终要看验证集上的小目标召回率变化,而不是只看 mAP。

3. YOLOv5 红外小目标训练流程与超参数调优

3.1 网络结构选择:YOLOv5s 到底够不够用

红外小目标检测的瓶颈不在模型容量,而在特征图的感受野与目标尺寸是否匹配。YOLOv5s 的 P3 特征图步长为 8,下采样倍率已经决定了小于 8 像素的目标很难被有效表达。对 4000 张数据集的规模,YOLOv5m 和 YOLOv5l 虽然能提升一点精度,但训练轮次拉长后更容易过拟合,而且推理速度下降明显。

结构 参数量 推理耗时参考 适用判断
YOLOv5n 约 1.9M 最低 目标基本大于 10 像素的轻量场景
YOLOv5s 约 7.2M 适中 4000 张红外小目标数据的首选
YOLOv5m 约 21M 数据量到 10000 张以上再考虑
YOLOv5l 约 46M 很高 对单帧多目标、密集小目标有强需求时

我的建议是先用 YOLOv5s 跑通训练链路,保留权重后,再在相同数据上训练 YOLOv5m 做对比。用同一条验证集分别计算 mAP@0.5,如果提升小于两个点就回到 YOLOv5s,毕竟 PyQt 界面端需要的实时性远比那一点 mAP 重要。

3.2 用 train.py 训练红外数据集的命令与参数说明

训练自己的数据集要先把数据组织成 YOLOv5 的标准结构, images labels 分目录存放,再写数据描述文件。

# infrared.yaml
train: ./ir_dataset/images/train
val: ./ir_dataset/images/val
nc: 4
names: ['drone', 'helicopter', 'airplane', 'bird']

训练命令建议打开 --multi-scale ,让输入尺寸在训练过程里随机波动,对提升不同距离下微小目标的尺度鲁棒性有明显帮助:

python train.py \
  --data infrared.yaml \
  --cfg models/yolov5s.yaml \
  --weights yolov5s.pt \
  --imgsz 960 \
  --batch-size 16 \
  --epochs 200 \
  --hyp hyp.ir.yaml \
  --multi-scale \
  --patience 30

--imgsz 960 是关键参数,训练和推理尺寸不一致是所有权重复现失败的常见原因;960 的输入把目标的有效像素放大了 50%,比增加模型深度更直接。 --batch-size 16 在 960 分辨率下对 24GB 显存比较稳妥,显存小就降到 8,同时按比例减少 --epochs 还是时间成本的问题。 patience 30 是早停参数,连续 30 个 epoch 验证集没有提升就停止,避免无效训练浪费机器时间。

3.3 anchors 重新聚类与 P2 输出层

YOLOv5 默认锚框是为 COCO 这种大中目标设计的,对红外 4 类小目标来说明显偏大。重聚类可以让锚框贴近真实目标尺寸,减少回归头的初始压力:

python utils/autoanchor.py --data infrared.yaml --imgsz 960

这个命令会输出新的锚框值,训练时如果配置里已经包含,YOLOv5 会自动采用。需要注意的是,重聚类结果受数据集标注质量影响很大,如果有几百个异常宽大的标注框,聚类出的锚框会整体偏大,跑完要人工看一眼生成的三组锚框最大值,超过目标实际宽度两倍的说明标注里有离群框。

如果数据里存在大量 4 到 8 像素的目标,标准 YOLOv5s 的 P3 层依然会丢失部分信息,可以考虑实验 P2 输出层。P2 层的特征图分辨率是输入的 1/4,保留了更底层的空间细节,代价是计算量上升。把模型配置换成带 P2 输出的版本后训练轮次要适当减少,因为底层特征参数量更大,4000 张数据容易在这个分支上过拟合。

3.4 权重文件 best.pt 的使用方式与验证

训练结束后,模型权重在 runs/train/exp*/weights/ 下, best.pt 是验证集 mAP 最优的权重, last.pt 是最后一个 epoch 的权重。实际部署推荐用 best.pt ,但要注意它选出的可能不是小目标召回最优的那个 epoch,验证时要单独看每一类的召回率。

python val.py \
  --data infrared.yaml \
  --weights runs/train/exp/weights/best.pt \
  --imgsz 960 \
  --classwise \
  --save-json

--classwise 会输出每个类别的精确率和召回率,这是判断权重是否真正适配标题中四类目标的关键。无人机类召回率如果低于飞鸟类,问题一般出在样本不平衡,回到第一阶段的标注分布去补数据,而不是继续堆 epoch。

4. PyQt 界面接入 YOLOv5 权重的推理架构

4.1 推理线程与界面线程分离:避免 PyQt 卡顿的核心

PyQt 界面上直接跑检测循环会让主线程被推理阻塞,表现为拖拽窗口卡死、视频画面掉帧。常见做法是用 QThread 做推理工作线程,主线程只管显示和交互。下面是一个带有限长度队列的线程实现,队列满时丢弃旧帧保证实时性:

import queue
import cv2
import torch
from PyQt5.QtCore import QThread, pyqtSignal

class IRDetector:
    def __init__(self, weights="best.pt", conf=0.35, iou=0.5):
        self.model = torch.hub.load("ultralytics/yolov5", "custom",
                                    path=weights, force_reload=False)
        self.model.conf = conf       # 置信度阈值
        self.model.iou = iou         # NMS 的 IoU 阈值

    def detect(self, frame):
        results = self.model(frame, size=960)
        return results.xyxy[0].cpu().numpy()  # x1, y1, x2, y2, conf, cls

class DetectThread(QThread):
    result_ready = pyqtSignal(object, object)

    def __init__(self, detector):
        super().__init__()
        self.detector = detector
        self._queue = queue.Queue(maxsize=2)
        self.running = True

    def push_frame(self, frame):
        if self._queue.full():
            try:
                self._queue.get_nowait()
            except queue.Empty:
                pass
        self._queue.put(frame.copy())

    def run(self):
        while self.running:
            try:
                frame = self._queue.get(timeout=0.1)
            except queue.Empty:
                continue
            dets = self.detector.detect(frame)
            self.result_ready.emit(frame, dets)

conf 参数对红外小目标非常敏感,0.35 是兼顾虚警和召回的经验起点;调高到 0.5 会明显减少飞鸟误报,但也可能漏掉低对比度的无人机。线程队列上限设 2 表示最多缓存两帧,推理速度跟不上输入帧率时丢弃最旧帧,保证界面看到的永远是最近结果。 result_ready 信号把帧和检测结果一起传回主线程,绘制操作必须在主线程完成,否则 QImage 更新会崩溃。

4.2 实时检测结果绘制与置信度滑条联动

检测结果在 PyQt 界面上的绘制用一个独立槽函数完成:

def draw_detections(self, frame, dets, names):
    for x1, y1, x2, y2, conf, cls in dets:
        color = (0, 255, 0) if int(cls) == 0 else (0, 200, 255)
        label = f"{names[int(cls)]} {conf:.2f}"
        cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), color, 2)
        cv2.putText(frame, label, (int(x1), int(y1) - 8),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
    h, w, ch = frame.shape
    qimg = QImage(frame.data, w, h, ch * w, QImage.Format_RGB888).rgbSwapped()
    self.label_video.setPixmap(QPixmap.fromImage(qimg))

颜色区分按类别写死在代码里,四个类分别用不同颜色在真实项目中更直观。 rgbSwapped 是把 OpenCV 的 BGR 顺序转成 QImage 的 RGB 顺序,漏掉这一步画面会偏蓝。界面上放一个 QSlider 关联到检测器的置信度阈值,滑动时直接修改 detector.model.conf ,比每次改代码重启程序高效得多。

4.3 摄像头、视频和单帧三种输入的统一处理

红外相机在 PyQt 里通常通过 OpenCV 的 VideoCapture 访问,由于红外设备驱动差异大,统一的输入接口能减少适配成本。

class FrameSource:
    def __init__(self, uri):
        self.uri = uri
        self.cap = None

    def open(self):
        self.cap = cv2.VideoCapture(self.uri)  # 数字为摄像头索引,字符串为视频路径

    def read(self):
        ok, frame = self.cap.read()
        return frame if ok else None

摄像头索引传 0 1 ,视频和图片路径传字符串,PyQt 的文件选择对话框直接返回路径给这个类。红外相机输出灰度或伪彩色,读取后统一转成 cv2.COLOR_GRAY2BGR 或伪彩色映射,再进检测器,避免推理链路上出现通道数不一致的报错。视频流输入时加上 5 到 10 毫秒的 QThread.msleep ,防止推理线程占满 CPU 导致系统性能下降。

5. 微小目标漏检验证:滑窗裁剪与混淆矩阵结合

验证微调阶段只看 mAP 是不够的,红外场景里飞鸟和无人机的混淆才是最需要量化的指标。训练结束后,用 val.py 生成的 confusion_matrix.png 检查两类之间的误判比例。图中 drone -> bird bird -> drone 两个单元格如果超过 15%,说明两类特征在热辐射形态上过于接近,单靠 YOLOv5 分类头难以区分,需要回到标注层面重新框选目标边缘。

验证集里目标小于 10 像素的样本,直接整图推理容易漏检,实际部署时常用滑窗裁剪。把 960 分辨率输入切成 4 个 640 重叠区域,分别过一遍权重,再把检测框坐标映射回原图。这个技巧能显著提升小目标召回率,代价是推理次数翻倍,适合在 PyQt 的离线分析模式下启用,实时模式下仍用整图推理。

def slide_window_detect(detector, frame, win=640, stride=480):
    h, w = frame.shape[:2]
    all_dets = []
    for y in range(0, h - win + 1, stride):
        for x in range(0, w - win + 1, stride):
            crop = frame[y:y+win, x:x+win]
            dets = detector.detect(crop)
            for x1, y1, x2, y2, conf, cls in dets:
                all_dets.append((x1+x, y1+y, x2+x, y2+y, conf, cls))
    return all_dets

stride=480 让相邻窗口有 160 像素重叠,避免目标正好被窗口边缘切开,重叠区域的目标会被检测两次,由 NMS 合并即可。窗口大小不需要固定 640,按部署平台的红外分辨率调整,原则是窗口内的目标占比要和训练集分布一致。滑窗产生的重复框多时要提高 IoU 阈值到 0.5 以上,减少同一个小目标被绘制成多个框的观感。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐