全文包含可直接复制运行的代码,建议点赞+收藏 + 关注,后续持续更新双模态检测改进创新系列内容。本文是双模态系列的第8篇,收藏文集持续跟进顶刊创新思路。
针对 Ultralytics YOLO 框架原生仅支持单模态图像输入的局限,无法直接适配双模态数据(不仅仅是RGB-IR,也可以是RGB-T、RGB-D等等)融合训练的实际需求,本文结合模型结构改造与数据加载逻辑优化,实现可运行的训练方案,有效支撑双模态目标检测任务。
一、环境准备
正常配置yolo的运行环境即可使用,也可以按照我的环境进行配置:
PyTorch 2.3.0
Python 3.12(ubuntu22.04)
CUDA 12.1
# 二、数据集的要求 数据集的格式如下(images放RGB图像、image放IR图像或者其他模态图像)`Bimodal/
├── image/
│ ├── train/
│ ├── val/
│ └── test/
├── images/
│ ├── train/
│ ├── val/
│ └── test/
└── labels/
├── train/
├── val/
└── test/`
对应的yaml文件如下:
# ==========================================
# 通用双模态目标检测数据集配置文件 (RGB + 次模态)
# ==========================================
# ----------------- 类别信息 -----------------
# 1. 类别数量 (Number of classes)
nc: <填写类别总数,例如 1>
# 2. 类别名称字典 (Class names)
# 务必与 txt 标注文件中的 ID (0, 1, 2...) 严格对应
names:
0: <填写类别名称_0,例如 PWD>
# 1: <填写类别名称_1>
# 2: <填写类别名称_2>
# ----------------- 路径信息 -----------------
# 强烈建议使用绝对路径 (Absolute paths),以彻底避免路径找不到的玄学报错
# 3. 训练集路径 (Training sets)
train: <填写主模态_训练集的绝对路径,例如 /autodl-fs/data/DatasetName/images/train>
train_ir: <填写次模态_训练集的绝对路径,例如 /autodl-fs/data/DatasetName/image/train>
# 4. 验证集路径 (Validation sets)
val: <填写主模态_验证集的绝对路径,例如 /autodl-fs/data/DatasetName/images/val>
val_ir: <填写次模态_验证集的绝对路径,例如 /autodl-fs/data/DatasetName/image/val>
# 5. 测试集路径 (Test sets - 可选)
test: <填写主模态_测试集的绝对路径,例如 /autodl-fs/data/DatasetName/images/test>
test_ir: <填写次模态_测试集的绝对路径,例如 /autodl-fs/data/DatasetName/image/test>
1. 数据多样性
双模态目标检测需要涵盖可见光(RGB)和红外(IR)或其他模态(如深度、雷达等)的图像数据。数据集应包含不同场景(白天/夜间、室内/室外)、天气条件(晴天、雨天、雾天)以及目标物体的多样姿态和尺度。
2. 模态对齐
RGB和IR图像需严格对齐(空间和时间同步),确保同一目标的像素级对应。时间同步可通过硬件同步或后期配准实现,空间同步需标注同一目标在两种模态下的边界框和类别。
3. 标注一致性
标注应覆盖两种模态,包括目标类别、边界框、分割掩码(如适用)。标注需考虑模态差异(如红外图像中目标的热特征),避免因模态特性导致的标注歧义。(也就说两种模态不同但是其余要相同,同时要对应相同的标注文本文件)
4. 数据规模与平衡
数据集需包含足够数量的样本(通常数万张图像以上),并平衡类别分布。夜间或低光照场景的样本比例应适当增加,以充分验证红外模态的优势。
5. 基准任务支持
数据集需支持主流检测任务(如目标分类、定位、多模态融合)。部分数据集需提供挑战性场景(如遮挡、小目标、动态背景)以评估算法鲁棒性。
典型数据集示例
- KAIST Multispectral Pedestrian Dataset:包含RGB和热红外图像,标注行人检测框。
- FLIR ADAS:提供昼夜场景下的热红外与可见光数据,覆盖车辆、行人等类别。
- MS-COCO(扩展版):部分研究通过融合红外数据扩展为双模态版本。等等
具体可以参考下面的blog,列举出很多经典的双模态数据集。
三、修改ultralytics下的部分文件
这是一趟非常深入的源码魔改之旅!你已经触及了 YOLO 框架最核心的数据加载、计算图路由、特性提取和增强逻辑。
为了方便你复盘和最终的论文/代码整理,我将所有成功修复的代码按照文件路径进行了系统性的汇总。
1. 数据加载层:双模态图片拼接
📂 文件位置: /autodl-fs/data/YOLOv11/ultralytics/data/base.py
🔍 修改目标: BaseDataset 类中的 load_image 方法。
📝 核心代码:
def load_image(self, i, rect_mode=True):
"""Loads 1 bimodal (RGB + NIR) image from dataset index 'i'"""
im, f, fn = self.ims[i], self.im_files[i], self.npy_files[i]
if im is None:
if fn.exists():
try:
im = np.load(fn)
except Exception as e:
Path(fn).unlink(missing_ok=True)
im = None
if im is None:
# --- 🌟 双模态拼接逻辑 ---
im_rgb = cv2.imread(f)
if im_rgb is None:
raise FileNotFoundError(f"Image Not Found {f}")
# 替换路径读取 NIR 图像
f_nir = f.replace('/images/', '/image/').replace('\images\', '\image\')
im_nir = cv2.imread(f_nir)
if im_nir is None:
raise FileNotFoundError(f"NIR Image Not Found {f_nir}")
# 尺寸对齐
if im_rgb.shape[:2] != im_nir.shape[:2]:
im_nir = cv2.resize(im_nir, (im_rgb.shape[1], im_rgb.shape[0]))
# RGB(3) + NIR(3) → 拼接成 6 通道
im = np.concatenate((im_rgb, im_nir), axis=-1)
# -----------------------
# 保存 npy 缓存加速下次读取
np.save(fn, im)
# 缓存到内存
self.ims[i] = im
# 矩形训练模式(YOLO 原版必须)
if rect_mode:
h0, w0 = im.shape[:2]
r = self.img_size / max(h0, w0)
if r != 1:
im = cv2.resize(im, (int(w0 * r), int(h0 * r)), interpolation=cv2.INTER_LINEAR)
h, w = im.shape[:2]
return im, (h, w), (h0, w0), (h, w)
else:
# 普通模式
h0, w0 = im.shape[:2]
im = cv2.resize(im, (self.img_size, self.img_size))
h, w = self.img_size, self.img_size
return im, (h, w), (h0, w0), (h, w)
2. 数据增强层:隔离 HSV 增强
📂 文件位置: ultralytics/data/augment.py
🔍 修改目标: RandomHSV 类的 __call__ 方法。
📝 核心代码:
def __call__(self, labels):
img = labels["img"]
if self.hgain or self.sgain or self.vgain:
r = np.random.uniform(-1, 1, 3) * [self.hgain, self.sgain, self.vgain] + 1
# --- 🌟 仅对前 3 通道做增强 ---
is_bimodal = img.shape[-1] == 6
img_target = img[..., :3] if is_bimodal else img
hue, sat, val = cv2.split(cv2.cvtColor(img_target, cv2.COLOR_BGR2HSV))
dtype = img_target.dtype
x = np.arange(0, 256, dtype=r.dtype)
lut_hue = ((x * r[0]) % 180).astype(dtype)
lut_sat = np.clip(x * r[1], 0, 255).astype(dtype)
lut_val = np.clip(x * r[2], 0, 255).astype(dtype)
im_hsv = cv2.merge((cv2.LUT(hue, lut_hue), cv2.LUT(sat, lut_sat), cv2.LUT(val, lut_val)))
img_aug = cv2.cvtColor(im_hsv, cv2.COLOR_HSV2BGR)
if is_bimodal:
labels["img"] = np.concatenate((img_aug, img[..., 3:]), axis=-1) # 重新装回字典
else:
img[...] = img_aug
# -----------------------
return labels # 必须返回
3. 计算图与状态评估:修复 FLOPs 统计
📂 文件位置: ultralytics/utils/torch_utils.py
🔍 修改目标: get_flops 函数。
📝 核心代码:
def get_flops(model, imgsz=640):
try:
model = de_parallel(model)
p = next(model.parameters())
if not isinstance(imgsz, list): imgsz = [imgsz, imgsz]
# --- 🌟 动态推导真实输入通道数,取代写死的 3 ---
ch = p.shape[1]
if hasattr(model, 'yaml') and isinstance(model.yaml, dict):
ch = model.yaml.get('ch', ch)
if ch == 3: ch = 6
stride = 640
im = torch.empty((1, ch, stride, stride), device=p.device) # 修复维度顺序
flops = thop.profile(deepcopy(model), inputs=[im], verbose=False)[0] / 1e9 * 2
return flops * imgsz[0] / stride * imgsz[1] / stride
except Exception:
try:
im = torch.empty((1, ch, *imgsz), device=p.device)
return thop.profile(deepcopy(model), inputs=[im], verbose=False)[0] / 1e9 * 2
except Exception:
return 0.0
4. 可视化修复:防止 6 通道绘图崩溃
📂 文件位置: ultralytics/utils/plotting.py
🔍 修改目标: plot_images 函数开头处。
📝 核心代码: 用下面代码替代原来的代码
if isinstance(images, torch.Tensor):
images = images.cpu().float().numpy()
# --- 🌟 如果是 6 通道,只取 RGB 拿去画图 ---
if images.shape[1] == 6:
images = images[:, :3, :, :]
# -----------------------
5. 数据加载代码
📂 文件位置: ultralytics/nn/modules/block.py
📝 核心代码: 定义下面代码
import torch
import torch.nn as nn
class Concat(nn.Module):
"""
双模态前段融合专用的简单拼接模块
将来自不同模态的特征图在指定维度(通常是通道维度 dim=1)进行拼接
"""
def __init__(self, dimension=1):
super().__init__()
self.d = dimension # 默认 dim=1,即通道维度 (B, C, H, W) 中的 C
def forward(self, x):
# 注意:这里的输入 x 是一个包含多个张量的列表,例如 [rgb_feat, ir_feat]
# torch.cat 会沿着 self.d 维度将它们缝合在一起
return torch.cat(x, self.d)
class IN(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x):
return x
class Multiin(nn.Module): # stereo attention block
def __init__(self, out=1):
super().__init__()
self.out = out
def forward(self, x):
x1, x2 = x[:, :3, :, :], x[:, 3:, :, :]
if self.out == 1:
x = x1
else:
x = x2
return x
同时要导入进task.py文件里面parse_model 函数中处理模块解析那长长的一串 if-elif 判断(IN直接导入即可)
elif m is Multiin:
c2 = ch[f]//2
elif m is Concat:
# 增加一个判断:如果 f 是整数,转为列表
if isinstance(f, int):
f = [f]
c2 = sum(ch[x] for x in f)
细微的错误可用AI解决
四、训练策略选择
如下图所示,在双模态目标检测中,如何将RGB图像和IR(红外)/其他模态图像有效结合是核心问题。目前主流的融合策略分为以下三种:
1.输入级融合(早期融合)
- 原理:在模型输入前,直接将RGB(3通道)和IR(单通道或3通道)在通道维度拼接,形成4通道或6通道的输入张量。
- 优点:实现简单,几乎不需要修改模型结构,计算开销最小。
- 缺点:RGB和IR的底层特征分布差异极大,过早融合可能导致网络难以提取各自的独立特征,融合效果有限,上限较低。
2.特征级融合(中期融合)
- 原理:使用双主干网络(Dual Backbone)分别提取RGB和IR的特征,然后在不同尺度(如Neck部分的P3, P4, P5)通过相加(Add)、拼接(Concat)或注意力机制(Attention)进行特征融合。
- 优点:能够充分提取模态特有特征(RGB的纹理细节、IR的热辐射轮廓),并通过深层语义融合实现互补,是目前顶刊最容易出创新点的地方。
- 缺点:参数量会有所增加。
3.决策级融合(后期融合)
- 原理:两个模态分别经过完整的网络输出检测框,最后通过NMS(非极大值抑制)或加权机制融合最终的预测结果。
- 缺点:无法实现特征层面的交互,且计算量直接翻倍,实时性较差,工业界较少使用。
五、修改yaml文件(以yolo11为例)
1.输入级融合
# Ultralytics YOLO 🚀, AGPL-3.0 license
# YOLO11 object detection model with P3-P5 outputs.
# Parameters
ch: 6
nc: 80 # number of classes
scales: # model compound scaling constants
# [depth, width, max_channels]
n: [0.50, 0.25, 1024]
s: [0.50, 0.50, 1024]
m: [0.50, 1.00, 512]
l: [1.00, 1.00, 512]
x: [1.00, 1.50, 512]
# YOLO11n backbone
backbone:
# [from, repeats, module, args]
# 假设输入是一个包含两个模态张量的列表,例如 [RGB, IR]
- [[-1, -1], 1, Concat, [1]] # 0: 假设输入是已切分的列表,进行通道拼接
- [-1, 1, Conv, [64, 3, 2]] # 1-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 2-P2/4
- [-1, 2, C3k2, [256, False, 0.25]] # 3
- [-1, 1, Conv, [256, 3, 2]] # 4-P3/8
- [-1, 2, C3k2, [512, False, 0.25]] # 5
- [-1, 1, Conv, [512, 3, 2]] # 6-P4/16
- [-1, 2, C3k2, [512, True]] # 7
- [-1, 1, Conv, [1024, 3, 2]] # 8-P5/32
- [-1, 2, C3k2, [1024, True]] # 9
- [-1, 1, SPPF, [1024, 5]] # 10
- [-1, 2, C2PSA, [1024]] # 11
# YOLO11n head
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 12
- [[-1, 7], 1, Concat, [1]] # 13 cat backbone P4
- [-1, 2, C3k2, [512, False]] # 14
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 15
- [[-1, 5], 1, Concat, [1]] # 16 cat backbone P3
- [-1, 2, C3k2, [256, False]] # 17 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]] # 18
- [[-1, 14], 1, Concat, [1]] # 19 cat head P4
- [-1, 2, C3k2, [512, False]] # 20 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]] # 21
- [[-1, 11], 1, Concat, [1]] # 22 cat head P5
- [-1, 2, C3k2, [1024, True]] # 23 (P5/32-large)
- [[17, 20, 23], 1, Detect, [nc]] # Detect(P3, P4, P5)
2.特征级融合
# Ultralytics YOLO 🚀, AGPL-3.0 license
# YOLO11 Dual-Stream with Independent Enhancement Layers (SPPF & C2PSA per stream)
# Parameters
nc: 80 # number of classes
scales:
n: [0.50, 0.25, 1024]
s: [0.50, 0.50, 1024]
m: [0.50, 1.00, 512]
l: [1.00, 1.00, 512]
x: [1.00, 1.50, 512]
ch: 6 # 3(RGB) + 1(IR)
# YOLO11 Dual-Stream Backbone
backbone:
# [from, repeats, module, args]
- [-1, 1, IN, []] # 0: 输入占位
# --- 可见光流分支 (Stream 1: Visible) ---
- [0, 1, Multiin, [1]] # 1: 提取 RGB
- [-1, 1, Conv, [64, 3, 2]] # 2-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 3-P2/4
- [-1, 2, C3k2, [256, False, 0.25]] # 4
- [-1, 1, Conv, [256, 3, 2]] # 5-P3/8
- [-1, 2, C3k2, [512, False, 0.25]] # 6 (Visible P3 特征)
- [-1, 1, Conv, [512, 3, 2]] # 7-P4/16
- [-1, 2, C3k2, [512, True]] # 8 (Visible P4 特征)
- [-1, 1, Conv, [1024, 3, 2]] # 9-P5/32
- [-1, 2, C3k2, [1024, True]] # 10 (Visible P5 原始)
- [-1, 1, SPPF, [1024, 5]] # 11: 可见光独立 SPPF
- [-1, 2, C2PSA, [1024]] # 12: 可见光独立 C2PSA (Visible P5 增强)
# --- 红外流分支 (Stream 2: Infrared) ---
- [0, 1, Multiin, [2]] # 13: 提取 IR (注意 from 0)
- [-1, 1, Conv, [64, 3, 2]] # 14-P1/2
- [-1, 1, Conv, [128, 3, 2]] # 15-P2/4
- [-1, 2, C3k2, [256, False, 0.25]] # 16
- [-1, 1, Conv, [256, 3, 2]] # 17-P3/8
- [-1, 2, C3k2, [512, False, 0.25]] # 18 (IR P3 特征)
- [-1, 1, Conv, [512, 3, 2]] # 19-P4/16
- [-1, 2, C3k2, [512, True]] # 20 (IR P4 特征)
- [-1, 1, Conv, [1024, 3, 2]] # 21-P5/32
- [-1, 2, C3k2, [1024, True]] # 22 (IR P5 原始)
- [-1, 1, SPPF, [1024, 5]] # 23: 红外独立 SPPF
- [-1, 2, C2PSA, [1024]] # 24: 红外独立 C2PSA (IR P5 增强)
# --- 特征融合层 (Fusion) ---
- [[6, 18], 1, Concat, [1]] # 25: P3 尺度融合 (Visible P3 + IR P3)
- [[8, 20], 1, Concat, [1]] # 26: P4 尺度融合 (Visible P4 + IR P4)
- [[12, 24], 1, Concat, [1]] # 27: P5 尺度融合 (增强后的 Visible P5 + IR P5)
# YOLO11 Head (PAN-FPN)
head:
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 28 (来自 27-P5)
- [[-1, 26], 1, Concat, [1]] # 29: 接入融合后的 P4 (Layer 26)
- [-1, 2, C3k2, [512, False]] # 30
- [-1, 1, nn.Upsample, [None, 2, "nearest"]] # 31
- [[-1, 25], 1, Concat, [1]] # 32: 接入融合后的 P3 (Layer 25)
- [-1, 2, C3k2, [256, False]] # 33 (P3/8 输出层)
- [-1, 1, Conv, [256, 3, 2]] # 34
- [[-1, 30], 1, Concat, [1]] # 35: Head P4 融合
- [-1, 2, C3k2, [512, False]] # 36 (P4/16 输出层)
- [-1, 1, Conv, [512, 3, 2]] # 37
- [[-1, 27], 1, Concat, [1]] # 38: Head P5 融合
- [-1, 2, C3k2, [1024, True]] # 39 (P5/32 输出层)
- [[33, 36, 39], 1, Detect, [nc]] # 40: 检测头
3.决策级融合
# Parameters
ch: 6 # 3(RGB) + 3(IR)
nc: 1 # 类别数量
scales:
# [depth, width, max_channels]
n: [0.50, 0.25, 1024]
s: [0.50, 0.50, 1024]
m: [0.50, 1.00, 512]
l: [1.00, 1.00, 512]
x: [1.00, 1.50, 512]
# YOLO11 Dual-Stream Backbone
backbone:
# [from, repeats, module, args]
- [-1, 1, IN, []] # 0: 原始 6 通道输入
- [-1, 1, Multiin, [1]] # 1: 可见光流起始
- [-2, 1, Multiin, [2]] # 2: 红外流起始
# --- P1/2 (下采样到 1/2) ---
- [-2, 1, Conv, [64, 3, 2]] # 3: Stream A (Visible)
- [-2, 1, Conv, [64, 3, 2]] # 4: Stream B (Infrared)
# --- P2/4 (下采样到 1/4) ---
- [-2, 1, Conv, [128, 3, 2]] # 5: Stream A
- [-2, 1, Conv, [128, 3, 2]] # 6: Stream B
- [-2, 3, C3k2, [128, False, 0.25]] # 7: Stream A
- [-2, 3, C3k2, [128, False, 0.25]] # 8: Stream B
# --- P3/8 (下采样到 1/8) ---
- [-2, 1, Conv, [256, 3, 2]] # 9: Stream A (P3 Skip Source)
- [-2, 1, Conv, [256, 3, 2]] # 10: Stream B (P3 Skip Source)
- [-2, 6, C3k2, [256, False, 0.25]] # 11: Stream A
- [-2, 6, C3k2, [256, False, 0.25]] # 12: Stream B
# --- P4/16 (下采样到 1/16) ---
- [-2, 1, Conv, [512, 3, 2]] # 13: Stream A (P4 Skip Source)
- [-2, 1, Conv, [512, 3, 2]] # 14: Stream B (P4 Skip Source)
- [-2, 6, C3k2, [512, True]] # 15: Stream A
- [-2, 6, C3k2, [512, True]] # 16: Stream B
# --- P5/32 (下采样到 1/32) ---
- [-2, 1, Conv, [1024, 3, 2]] # 17: Stream A
- [-2, 1, Conv, [1024, 3, 2]] # 18: Stream B
- [-2, 3, C3k2, [1024, True]] # 19: Stream A
- [-2, 3, C3k2, [1024, True]] # 20: Stream B
# --- 独立增强层 ---
- [-2, 1, SPPF, [1024, 5]] # 21: Stream A
- [-2, 1, SPPF, [1024, 5]] # 22: Stream B
- [-2, 1, C2PSA, [1024]] # 23: Stream A 最终特征 (P5)
- [-2, 1, C2PSA, [1024]] # 24: Stream B 最终特征 (P5)
# YOLO11 Dual-Stream Head (PAN-FPN)
head:
# --- P4 Top-down ---
- [-2, 1, nn.Upsample, [None, 2, 'nearest']] # 25 (Visible)
- [-2, 1, nn.Upsample, [None, 2, 'nearest']] # 26 (Infrared)
- [[-2, 15], 1, Concat, [1]] # 27: 拼接 Backbone P4 (V)
- [[-2, 16], 1, Concat, [1]] # 28: 拼接 Backbone P4 (I)
- [-2, 3, C3k2, [512, False]] # 29 (V)
- [-2, 3, C3k2, [512, False]] # 30 (I)
# --- P3 Top-down ---
- [-2, 1, nn.Upsample, [None, 2, 'nearest']] # 31 (V)
- [-2, 1, nn.Upsample, [None, 2, 'nearest']] # 32 (I)
- [[-2, 11], 1, Concat, [1]] # 33: 拼接 Backbone P3 (V)
- [[-2, 12], 1, Concat, [1]] # 34: 拼接 Backbone P3 (I)
- [-2, 3, C3k2, [256, False]] # 35 (V) (P3/8-Small)
- [-2, 3, C3k2, [256, False]] # 36 (I) (P3/8-Small)
# --- P4 Bottom-up ---
- [-2, 1, Conv, [256, 3, 2]] # 37 (V)
- [-2, 1, Conv, [256, 3, 2]] # 38 (I)
- [[-1, 29], 1, Concat, [1]] # 39: 拼接 Head P4 (V)
- [[-1, 30], 1, Concat, [1]] # 40: 拼接 Head P4 (I)
- [-2, 3, C3k2, [512, False]] # 41 (V) (P4/16-Medium)
- [-2, 3, C3k2, [512, False]] # 42 (I) (P4/16-Medium)
# --- P5 Bottom-up ---
- [-2, 1, Conv, [512, 3, 2]] # 43 (V)
- [-2, 1, Conv, [512, 3, 2]] # 44 (I)
- [[-1, 23], 1, Concat, [1]] # 45: 拼接 Backbone P5 (V)
- [[-1, 24], 1, Concat, [1]] # 46: 拼接 Backbone P5 (I)
- [-2, 3, C3k2, [1024, True]] # 47 (V) (P5/32-Large)
- [-2, 3, C3k2, [1024, True]] # 48 (I) (P5/32-Large)
# Detect 层:同时接收来自两路流的 P3, P4, P5 特征图
- [[35, 36, 41, 42, 47, 48], 1, Detect, [nc]]
六、运行train.py
将里面的数据集以及模型文件yaml的地址输入即可。
如果大家觉得一步步太麻烦的话,关注我免费分享给你。有问题欢迎在评论区指出
后续将进行更新!!!!以及进行二次创新,发顶刊必备。。。敬请关注!!!
笔者整理可运行的双模态检测的文件,免费分享给粉丝,需要关注后领取。