跳到正文
TranquilYu's Blog
返回

【论文阅读 | TIM 2021 | STDFusionNet:基于显著目标检测的红外-可见光图像融合网络】

约 31 分钟 · 4,917 字
论文阅读
文章目录40 节
  1. 1. 摘要
  2. 2. 引言与动机
  3. 3. 贡献
  4. 4. 方法(STDFusionNet)
  5. 4.1 符号与“期望信息”定义
  6. 4.2 总体框架
  7. 4.2.1 输入与“分区”操作(Fig.3左)
  8. 4.2.2 Feature Extraction Network(Fig.3顶部)
  9. 4.2.3 Feature Reconstruction Network(Fig.3顶部右侧)
  10. 4.2.4 Loss Function(Fig.3中部“Loss Function”块)
  11. 4.3 Loss 公式(式(2)-(6))——像素一致 + 梯度一致 + 显著/背景分区
  12. 4.3.1 像素损失 Pixel loss(显著区域 / 背景区域)
  13. 4.3.2 梯度损失 Gradient loss(显著区域 / 背景区域)
  14. 4.3.3 总损失——区域权重 + 同区域内 pixel/grad 等权
  15. 4.4 显著目标mask的获取与应用(Fig.3下半+Fig.4)
  16. 4.5 网络结构细节(Fig.3顶部/底部 ResBlock)
  17. 4.5.1 Feature Extraction Network(两条分支,pseudosiamese)
  18. 4.5.2 ResBlock(Fig.3 底部局部结构:每一个点/算子)
  19. 4.5.3 Feature Reconstruction Network(融合与重建)
  20. 4.5.4 padding/stride(“无下采样”)
  21. 5. 方法实现
  22. 5.1 前向传播(Inference/Training都需要)
  23. 5.2 Sobel 梯度算子(对应式(4)(5)中的 ∇)
  24. 5.3 Loss 计算(对应式(2)-(6),含 mask 分区)
  25. 5.4 训练流程(“Training Details”)
  26. 6. 实验设置(Datasets / Metrics / Training Details)
  27. 6.1 数据集(TNO & RoadScene)
  28. 6.2 指标(EN / MI / VIF / SF)
  29. 6.3 训练细节(Training Details)
  30. 7. 对比实验结果
  31. 7.1 对比方法(9个)
  32. 7.2 TNO:主观结果(Figs.5–8)与论文给出的观察
  33. 7.3 TNO:客观结果(Fig.9 + Table I)与论文对指标的解释
  34. 7.4 泛化实验(RoadScene):彩色可见图像的融合策略 + 论文观察
  35. 7.5 效率对比(Table II)与论文结论
  36. 8. “显著目标检测”可视化(Fig.15)
  37. 9. 消融实验(Fig.16 + Table III):期望信息定义 & 梯度loss
  38. 9.1 w/o desired information(去掉“期望信息定义”的消融)
  39. 9.2 w/o gradient loss(去掉梯度loss的消融)
  40. 10. 结论(Conclusion)
文章目录40 节
  1. 1. 摘要
  2. 2. 引言与动机
  3. 3. 贡献
  4. 4. 方法(STDFusionNet)
  5. 4.1 符号与“期望信息”定义
  6. 4.2 总体框架
  7. 4.2.1 输入与“分区”操作(Fig.3左)
  8. 4.2.2 Feature Extraction Network(Fig.3顶部)
  9. 4.2.3 Feature Reconstruction Network(Fig.3顶部右侧)
  10. 4.2.4 Loss Function(Fig.3中部“Loss Function”块)
  11. 4.3 Loss 公式(式(2)-(6))——像素一致 + 梯度一致 + 显著/背景分区
  12. 4.3.1 像素损失 Pixel loss(显著区域 / 背景区域)
  13. 4.3.2 梯度损失 Gradient loss(显著区域 / 背景区域)
  14. 4.3.3 总损失——区域权重 + 同区域内 pixel/grad 等权
  15. 4.4 显著目标mask的获取与应用(Fig.3下半+Fig.4)
  16. 4.5 网络结构细节(Fig.3顶部/底部 ResBlock)
  17. 4.5.1 Feature Extraction Network(两条分支,pseudosiamese)
  18. 4.5.2 ResBlock(Fig.3 底部局部结构:每一个点/算子)
  19. 4.5.3 Feature Reconstruction Network(融合与重建)
  20. 4.5.4 padding/stride(“无下采样”)
  21. 5. 方法实现
  22. 5.1 前向传播(Inference/Training都需要)
  23. 5.2 Sobel 梯度算子(对应式(4)(5)中的 ∇)
  24. 5.3 Loss 计算(对应式(2)-(6),含 mask 分区)
  25. 5.4 训练流程(“Training Details”)
  26. 6. 实验设置(Datasets / Metrics / Training Details)
  27. 6.1 数据集(TNO & RoadScene)
  28. 6.2 指标(EN / MI / VIF / SF)
  29. 6.3 训练细节(Training Details)
  30. 7. 对比实验结果
  31. 7.1 对比方法(9个)
  32. 7.2 TNO:主观结果(Figs.5–8)与论文给出的观察
  33. 7.3 TNO:客观结果(Fig.9 + Table I)与论文对指标的解释
  34. 7.4 泛化实验(RoadScene):彩色可见图像的融合策略 + 论文观察
  35. 7.5 效率对比(Table II)与论文结论
  36. 8. “显著目标检测”可视化(Fig.15)
  37. 9. 消融实验(Fig.16 + Table III):期望信息定义 & 梯度loss
  38. 9.1 w/o desired information(去掉“期望信息定义”的消融)
  39. 9.2 w/o gradient loss(去掉梯度loss的消融)
  40. 10. 结论(Conclusion)

STDFusionNet:基于显著目标检测的红外-可见光图像融合网络


1. 摘要


2. 引言与动机

Fig. 1:现有方法对有用信息的削弱示例
Fig. 1:现有方法对有用信息的削弱示例
Fig. 2:STDFusionNet示例对比(GTF / DenseFuse / STDFusionNet)
Fig. 2:STDFusionNet示例对比(GTF / DenseFuse / STDFusionNet)

3. 贡献


4. 方法(STDFusionNet)

Fig. 3:STDFusionNet总体结构 + ResBlock局部结构
Fig. 3:STDFusionNet总体结构 + ResBlock局部结构

4.1 符号与“期望信息”定义

Id=ImIir+(1Im)Ivi(1)I_d = I_m \circ I_{ir} + (1 - I_m) \circ I_{vi} \tag{1}

其中 \circ 表示逐元素乘(element-wise multiplication)。

Fig.3:左侧的“Salient target mask”与其“背景mask(反相)”,在图中通过“逐元素乘”节点把源图像分成“显著区域”和“背景区域”。


4.2 总体框架

4.2.1 输入与“分区”操作(Fig.3左)

对应实现:这部分是“mask分区”,属于训练期loss构建的前处理;并不意味着mask被送进主干网络作为输入。本文强调 mask仅用于训练期引导,不需要在测试期输入网络。

4.2.2 Feature Extraction Network(Fig.3顶部)

4.2.3 Feature Reconstruction Network(Fig.3顶部右侧)

4.2.4 Loss Function(Fig.3中部“Loss Function”块)


4.3 Loss 公式(式(2)-(6))——像素一致 + 梯度一致 + 显著/背景分区

4.3.1 像素损失 Pixel loss(显著区域 / 背景区域)

Lsalientpixel=1HW(ImIf)(ImIir)1(2)L^{pixel}_{salient}=\frac{1}{HW}\left\| (I_m\circ I_f)-(I_m\circ I_{ir})\right\|_1 \tag{2} Lbackpixel=1HW((1Im)If)((1Im)Ivi)1(3)L^{pixel}_{back}=\frac{1}{HW}\left\| ((1-I_m)\circ I_f)-((1-I_m)\circ I_{vi})\right\|_1 \tag{3}

4.3.2 梯度损失 Gradient loss(显著区域 / 背景区域)

Lsalientgrad=1HW(ImIf)(ImIir)1(4)L^{grad}_{salient}=\frac{1}{HW}\left\| (I_m\circ \nabla I_f)-(I_m\circ \nabla I_{ir})\right\|_1 \tag{4} Lbackgrad=1HW((1Im)If)((1Im)Ivi)1(5)L^{grad}_{back}=\frac{1}{HW}\left\| ((1-I_m)\circ \nabla I_f)-((1-I_m)\circ \nabla I_{vi})\right\|_1 \tag{5}

4.3.3 总损失——区域权重 + 同区域内 pixel/grad 等权

L=(Lbackpixel+Lbackgrad)+α(Lsalientpixel+Lsalientgrad)(6)L = (L^{pixel}_{back}+L^{grad}_{back})+\alpha(L^{pixel}_{salient}+L^{grad}_{salient}) \tag{6}

4.4 显著目标mask的获取与应用(Fig.3下半+Fig.4)

Fig. 4:源图像与mask示例(TNO)
Fig. 4:源图像与mask示例(TNO)

4.5 网络结构细节(Fig.3顶部/底部 ResBlock)

4.5.1 Feature Extraction Network(两条分支,pseudosiamese)

4.5.2 ResBlock(Fig.3 底部局部结构:每一个点/算子)

4.5.3 Feature Reconstruction Network(融合与重建)

4.5.4 padding/stride(“无下采样”)


5. 方法实现

按 Fig.3 + 式(1)-(6) 整理的训练/推理流程

数据预处理(归一化到 [-1,1]、裁剪 stride=24、patch=128×128;测试不裁剪)
来自 utils.input_setuptrain.py。训练时每张源图/掩码都按 stride=24 滑窗裁成 128×128 patch,并用 (imread(...) - 127.5)/127.5 归一化到 [-1,1]。

def input_setup(sess, config, data_dir, index=0):
    """
    Read image files and make their sub-images and saved them as a h5 file format.
    """
    # Load data path
    if config.is_train:
        data = prepare_data(sess, dataset=data_dir)
    else:
        data = prepare_data(sess, dataset=data_dir)

    sub_input_sequence = []

    if config.is_train:
        for i in range(len(data)):
            input_ = (imread(data[i]) - 127.5) / 127.5
            if len(input_.shape) == 3:
                h, w, _ = input_.shape
            else:
                h, w = input_.shape
            for x in range(0, h - config.image_size + 1, config.stride):
                for y in range(0, w - config.image_size + 1, config.stride):
                    sub_input = input_[x:x + config.image_size, y:y + config.image_size]
                    # Make channel value
                    if data_dir == "Train":
                        sub_input = cv2.resize(sub_input, (config.image_size / 4, config.image_size / 4),
                                               interpolation=cv2.INTER_CUBIC)
                        sub_input = sub_input.reshape([config.image_size / 4, config.image_size / 4, 1])
                        print('error')
                    else:
                        sub_input = sub_input.reshape([config.image_size, config.image_size, 1])

                    sub_input_sequence.append(sub_input)

    else:
        input_ = (imread(data[index]) - 127.5) / 127.5 // 归一化 
        if len(input_.shape) == 3:
            h_real, w_real, _ = input_.shape // RGB只关心HW
        else:
            h_real, w_real = input_.shape // IR只关心HW
        input_ = np.lib.pad(input_, ((padding, padding_h), (padding, padding_w)), 'edge')
        h, w = input_.shape
        # print(input_.shape)
        # Numbers of sub-images in height and width of image are needed to compute merge operation.
        nx = ny = 0
        for x in range(0, h - config.image_size + 1, config.stride):
            nx += 1
            ny = 0
            for y in range(0, w - config.image_size + 1, config.stride):
                ny += 1
                sub_input = input_[x:x + config.image_size, y:y + config.image_size]  # [33 x 33]
                sub_input = sub_input.reshape([config.image_size, config.image_size, 1])
                // 单通道输出,只关心亮度
                sub_input_sequence.append(sub_input)
// 128x128 每次64x64,右下,滑窗
    """
    len(sub_input_sequence) : the number of sub_input (33 x 33 x ch) in one image
    (sub_input_sequence[0]).shape : (33, 33, 1)
    """
    # Make list to numpy array. With this transform
    arrdata = np.asarray(sub_input_sequence)  # [?, 33, 33, 1]
    # print(arrdata.shape)
    make_data(sess, arrdata, data_dir)

    if not config.is_train:
        print(nx, ny)
        print(h_real, w_real)
        return nx, ny, h_real, w_real

5.1 前向传播(Inference/Training都需要)

class STDFusionNet():
    def vi_feature_extraction_network(self, vi_image): // 这里定义的是可见光图像类
        # 可见光编码器,输入 vi_image 形状: [N, H, W, 1]
        with tf.compat.v1.variable_scope('vi_extraction_network'):
            with tf.compat.v1.variable_scope('conv1'):
                # 首层 5x5 卷积提取低层特征,输出 16 通道
                weights = tf.compat.v1.get_variable("w", [5, 5, 1, 16],
                                                    initializer=tf.truncated_normal_initializer(stddev=1e-3))
                #weights = weights_spectral_norm(weights)
                # 每个输出通道一个偏置
                bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                # 步长 1 且 SAME 填充,保持空间尺寸
                conv1 = tf.nn.conv2d(vi_image, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                # conv1 = tf.contrib.layers.batch_norm(conv1, decay=0.9, updates_collections=None, epsilon=1e-5, scale=True)
                # Leaky ReLU 激活缓解神经元死亡
                conv1 = tf.nn.leaky_relu(conv1)
            block1_input = conv1
            # state size: 16
                        
    
    

    // 主分支 1×13×31×1 且 Conv1/Conv2 后接 leaky ReLU,旁路 identity 1×1 升维;`conv3 + identity_conv` 后再过 leaky ReLU,对应 Fig.3 ResBlock 的“+”与激活。
    
            with tf.compat.v1.variable_scope('block1'):
                with tf.compat.v1.variable_scope('conv1'):
                    # 1x1 卷积混合通道,不改变空间分辨率
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # 点卷积投影
                    conv1 = tf.nn.conv2d(block1_input, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv1 = tf.nn.leaky_relu(conv1)

                with tf.compat.v1.variable_scope('conv2'):
                    # 3x3 卷积聚合空间上下文
                    weights = tf.compat.v1.get_variable("w", [3, 3, 16, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # SAME 填充保持特征图尺寸
                    conv2 = tf.nn.conv2d(conv1, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv2 = tf.nn.leaky_relu(conv2)
                with tf.compat.v1.variable_scope('conv3'):
                    # 1x1 卷积生成残差输出
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # 残差分支输出
                    conv3 = tf.nn.conv2d(conv2, weights, strides=[1, 1, 1, 1], padding='SAME') + bias

                # 残差连接:主分支与输入相加
                block1_output = tf.nn.leaky_relu(conv3 + block1_input)
            block2_input = block1_output
            
    
    
    
            with tf.compat.v1.variable_scope('block2'):
                with tf.compat.v1.variable_scope('conv1'):
                    # 先用 1x1 通道混合,再做空间卷积
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # 点卷积投影
                    conv1 = tf.nn.conv2d(block2_input, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv1 = tf.nn.leaky_relu(conv1)

                with tf.compat.v1.variable_scope('conv2'):
                    # 3x3 卷积扩大感受野
                    weights = tf.compat.v1.get_variable("w", [3, 3, 16, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # SAME 填充保持分辨率
                    conv2 = tf.nn.conv2d(conv1, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv2 = tf.nn.leaky_relu(conv2)
                with tf.compat.v1.variable_scope('conv3'):
                    # 1x1 卷积将通道升维到 32
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 32],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [32], initializer=tf.constant_initializer(0.0))
                    # 残差主分支输出
                    conv3 = tf.nn.conv2d(conv2, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                with tf.variable_scope('identity_conv'):
                    # 投影捷径,将通道从 16 映射到 32 以便相加
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 32],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    identity_conv = tf.nn.conv2d(block2_input, weights, strides=[1, 1, 1, 1], padding='SAME')
                # 残差相加后再激活
                block2_output = tf.nn.leaky_relu(conv3 + identity_conv)
                block3_input = block2_output
                            
    
    
    
            with tf.compat.v1.variable_scope('block3'):
                with tf.compat.v1.variable_scope('conv1'):
                    # 1x1 通道混合,保持 32 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 32, 32],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [32], initializer=tf.constant_initializer(0.0))
                    # 点卷积投影
                    conv1 = tf.nn.conv2d(block3_input, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv1 = tf.nn.leaky_relu(conv1)

                with tf.compat.v1.variable_scope('conv2'):
                    # 3x3 卷积,通道仍为 32
                    weights = tf.compat.v1.get_variable("w", [3, 3, 32, 32],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [32], initializer=tf.constant_initializer(0.0))
                    # SAME 填充的空间卷积
                    conv2 = tf.nn.conv2d(conv1, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv2 = tf.nn.leaky_relu(conv2)
                with tf.compat.v1.variable_scope('conv3'):
                    # 1x1 卷积将通道升到 64
                    weights = tf.compat.v1.get_variable("w", [1, 1, 32, 64],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [64], initializer=tf.constant_initializer(0.0))
                    # 残差主分支输出
                    conv3 = tf.nn.conv2d(conv2, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                with tf.variable_scope('identity_conv'):
                    # 捷径分支投影,将 32 通道升到 64
                    weights = tf.compat.v1.get_variable("w", [1, 1, 32, 64],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    identity_conv = tf.nn.conv2d(block3_input, weights, strides=[1, 1, 1, 1], padding='SAME')
                # 残差相加后的可见光编码输出
                block3_output = tf.nn.leaky_relu(conv3 + identity_conv)
                encoding_feature = block3_output
        return encoding_feature
    
    
    
    
    
    
    
 
    def ir_feature_extraction_network(self, ir_image): // 这里定义的是红外图像类
        # 红外编码器,输入 ir_image 形状: [N, H, W, 1]
        with tf.compat.v1.variable_scope('ir_extraction_network'):
            with tf.compat.v1.variable_scope('conv1'):
                # 首层 5x5 卷积提取低层红外特征,输出 16 通道
                weights = tf.compat.v1.get_variable("w", [5, 5, 1, 16],
                                                    initializer=tf.truncated_normal_initializer(stddev=1e-3))
                #weights = weights_spectral_norm(weights)
                # 每通道偏置
                bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                # SAME 填充保持尺寸,步长 1
                conv1 = tf.nn.conv2d(ir_image, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                # conv1 = tf.contrib.layers.batch_norm(conv1, decay=0.9, updates_collections=None, epsilon=1e-5, scale=True)
                # Leaky ReLU 激活
                conv1 = tf.nn.leaky_relu(conv1)
            block1_input = conv1
            # state size: 16
                        
    
    
    
    // 主分支 1×13×31×1 且 Conv1/Conv2 后接 leaky ReLU,旁路 identity 1×1 升维;`conv3 + identity_conv` 后再过 leaky ReLU,对应 Fig.3 ResBlock 的“+”与激活。
    
            with tf.compat.v1.variable_scope('block1'):
                with tf.compat.v1.variable_scope('conv1'):
                    # 1x1 通道混合,保持空间
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # 点卷积投影
                    conv1 = tf.nn.conv2d(block1_input, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv1 = tf.nn.leaky_relu(conv1)

                with tf.compat.v1.variable_scope('conv2'):
                    # 3x3 卷积获取局部上下文
                    weights = tf.compat.v1.get_variable("w", [3, 3, 16, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # SAME 填充,步长 1
                    conv2 = tf.nn.conv2d(conv1, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv2 = tf.nn.leaky_relu(conv2)
                with tf.compat.v1.variable_scope('conv3'):
                    # 1x1 卷积生成残差输出(仍 16 通道)
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # 残差主分支输出
                    conv3 = tf.nn.conv2d(conv2, weights, strides=[1, 1, 1, 1], padding='SAME') + bias

                # 残差连接:与输入相加再激活
                block1_output = tf.nn.leaky_relu(conv3 + block1_input)
            block2_input = block1_output
                        
    
    
    
            with tf.compat.v1.variable_scope('block2'):
                with tf.compat.v1.variable_scope('conv1'):
                    # 1x1 通道混合,为升维做准备
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # 点卷积投影
                    conv1 = tf.nn.conv2d(block2_input, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv1 = tf.nn.leaky_relu(conv1)

                with tf.compat.v1.variable_scope('conv2'):
                    # 3x3 卷积扩大感受野
                    weights = tf.compat.v1.get_variable("w", [3, 3, 16, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # SAME 填充保持尺寸
                    conv2 = tf.nn.conv2d(conv1, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv2 = tf.nn.leaky_relu(conv2)
                with tf.compat.v1.variable_scope('conv3'):
                    # 1x1 卷积升维至 32 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 32],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [32], initializer=tf.constant_initializer(0.0))
                    # 残差主分支输出
                    conv3 = tf.nn.conv2d(conv2, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                with tf.variable_scope('identity_conv'):
                    # 捷径投影,将 16 通道映射到 32 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 32],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    identity_conv = tf.nn.conv2d(block2_input, weights, strides=[1, 1, 1, 1], padding='SAME')
                # 残差相加并激活
                block2_output = tf.nn.leaky_relu(conv3 + identity_conv)
                block3_input = block2_output
                            
    
    
    
            with tf.compat.v1.variable_scope('block3'):
                with tf.compat.v1.variable_scope('conv1'):
                    # 1x1 通道混合,保持 32 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 32, 32],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [32], initializer=tf.constant_initializer(0.0))
                    # 点卷积投影
                    conv1 = tf.nn.conv2d(block3_input, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv1 = tf.nn.leaky_relu(conv1)

                with tf.compat.v1.variable_scope('conv2'):
                    # 3x3 卷积保持 32 通道
                    weights = tf.compat.v1.get_variable("w", [3, 3, 32, 32],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [32], initializer=tf.constant_initializer(0.0))
                    # SAME 填充卷积
                    conv2 = tf.nn.conv2d(conv1, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv2 = tf.nn.leaky_relu(conv2)
                with tf.compat.v1.variable_scope('conv3'):
                    # 1x1 卷积升维到 64 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 32, 64],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [64], initializer=tf.constant_initializer(0.0))
                    # 残差主分支输出
                    conv3 = tf.nn.conv2d(conv2, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                with tf.variable_scope('identity_conv'):
                    # 捷径分支投影,32 -> 64 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 32, 64],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    identity_conv = tf.nn.conv2d(block3_input, weights, strides=[1, 1, 1, 1], padding='SAME')
                # 残差输出,得到红外编码特征
                block3_output = tf.nn.leaky_relu(conv3 + identity_conv)
                encoding_feature = block3_output
        return encoding_feature

    
    
    
    
    
    
    
    def feature_reconstruction_network(self, feature): // decoder重建网络
        # 解码重建网络,将拼接特征还原为融合图像
        with tf.compat.v1.variable_scope('reconstruction_network'):
            block1_input = feature
            with tf.compat.v1.variable_scope('block1'):
                with tf.compat.v1.variable_scope('conv1'):
                    # 1x1 通道混合,保持 128 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 128, 128],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [128], initializer=tf.constant_initializer(0.0))
                    # 点卷积
                    conv1 = tf.nn.conv2d(block1_input, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv1 = tf.nn.leaky_relu(conv1)

                with tf.compat.v1.variable_scope('conv2'):
                    # 3x3 卷积保持通道,提炼空间信息
                    weights = tf.compat.v1.get_variable("w", [3, 3, 128, 128],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [128], initializer=tf.constant_initializer(0.0))
                    # SAME 填充卷积
                    conv2 = tf.nn.conv2d(conv1, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv2 = tf.nn.leaky_relu(conv2)
                with tf.compat.v1.variable_scope('conv3'):
                    # 1x1 卷积降维至 64 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 128, 64],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [64], initializer=tf.constant_initializer(0.0))
                    # 残差主分支输出
                    conv3 = tf.nn.conv2d(conv2, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                with tf.variable_scope('identity_conv'):
                    # 捷径投影 128 -> 64,匹配相加
                    weights = tf.compat.v1.get_variable("w", [1, 1, 128, 64],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    identity_conv = tf.nn.conv2d(block1_input, weights, strides=[1, 1, 1, 1], padding='SAME')
                # 残差相加,使用 ELU 稳定梯度
                block1_output = tf.nn.elu(conv3 + identity_conv)
            block2_input = block1_output
            with tf.compat.v1.variable_scope('block2'):
                with tf.compat.v1.variable_scope('conv1'):
                    # 1x1 通道混合,维持 64 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 64, 64],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [64], initializer=tf.constant_initializer(0.0))
                    # 点卷积
                    conv1 = tf.nn.conv2d(block2_input, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv1 = tf.nn.leaky_relu(conv1)

                with tf.compat.v1.variable_scope('conv2'):
                    # 3x3 卷积保持 64 通道
                    weights = tf.compat.v1.get_variable("w", [3, 3, 64, 64],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [64], initializer=tf.constant_initializer(0.0))
                    # SAME 卷积
                    conv2 = tf.nn.conv2d(conv1, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv2 = tf.nn.leaky_relu(conv2)
                with tf.compat.v1.variable_scope('conv3'):
                    # 1x1 卷积降维到 32 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 64, 32],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [32], initializer=tf.constant_initializer(0.0))
                    # 残差主分支
                    conv3 = tf.nn.conv2d(conv2, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                with tf.variable_scope('identity_conv'):
                    # 捷径投影 64 -> 32
                    weights = tf.compat.v1.get_variable("w", [1, 1, 64, 32],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    identity_conv = tf.nn.conv2d(block2_input, weights, strides=[1, 1, 1, 1], padding='SAME')
                # 残差相加再 ELU 激活
                block2_output = tf.nn.elu(conv3 + identity_conv)
                block3_input = block2_output
            with tf.compat.v1.variable_scope('block3'):
                with tf.compat.v1.variable_scope('conv1'):
                    # 1x1 卷积保持 32 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 32, 32],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [32], initializer=tf.constant_initializer(0.0))
                    # 点卷积
                    conv1 = tf.nn.conv2d(block3_input, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv1 = tf.nn.leaky_relu(conv1)

                with tf.compat.v1.variable_scope('conv2'):
                    # 3x3 卷积保持 32 通道
                    weights = tf.compat.v1.get_variable("w", [3, 3, 32, 32],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [32], initializer=tf.constant_initializer(0.0))
                    # SAME 卷积
                    conv2 = tf.nn.conv2d(conv1, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv2 = tf.nn.leaky_relu(conv2)
                with tf.compat.v1.variable_scope('conv3'):
                    # 1x1 卷积降到 16 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 32, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # 残差主分支
                    conv3 = tf.nn.conv2d(conv2, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                with tf.variable_scope('identity_conv'):
                    # 捷径投影 32 -> 16
                    weights = tf.compat.v1.get_variable("w", [1, 1, 32, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    identity_conv = tf.nn.conv2d(block3_input, weights, strides=[1, 1, 1, 1], padding='SAME')
                # 残差相加后激活
                block3_output = tf.nn.leaky_relu(conv3 + identity_conv)
                block4_input = block3_output
            with tf.compat.v1.variable_scope('block4'):
                with tf.compat.v1.variable_scope('conv1'):
                    # 1x1 卷积保持 16 通道
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # 点卷积
                    conv1 = tf.nn.conv2d(block4_input, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv1 = tf.nn.leaky_relu(conv1)

                with tf.compat.v1.variable_scope('conv2'):
                    # 3x3 卷积保持 16 通道
                    weights = tf.compat.v1.get_variable("w", [3, 3, 16, 16],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [16], initializer=tf.constant_initializer(0.0))
                    # SAME 卷积
                    conv2 = tf.nn.conv2d(conv1, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                    conv2 = tf.nn.leaky_relu(conv2)
                with tf.compat.v1.variable_scope('conv3'):
                    # 1x1 卷积生成单通道输出
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 1],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    bias = tf.compat.v1.get_variable("b", [1], initializer=tf.constant_initializer(0.0))
                    # 残差主分支输出
                    conv3 = tf.nn.conv2d(conv2, weights, strides=[1, 1, 1, 1], padding='SAME') + bias
                with tf.variable_scope('identity_conv'):
                    # 捷径分支保持 1 通道,便于相加
                    weights = tf.compat.v1.get_variable("w", [1, 1, 16, 1],
                                                        initializer=tf.truncated_normal_initializer(stddev=1e-3))
                    #weights = weights_spectral_norm(weights)
                    identity_conv = tf.nn.conv2d(block4_input, weights, strides=[1, 1, 1, 1], padding='SAME')
                # 残差相加后用 tanh 输出融合图像
                block4_output = tf.nn.tanh(conv3 + identity_conv)
                fusion_image = block4_output
        return fusion_image
    def STDFusion_model(self, vi_image, ir_image):
        # 整体模型:编码可见光与红外,拼接后解码得到融合图像
        with tf.variable_scope("STDFusion_model"):
            # 提取可见光特征
            vi_feature = self.vi_feature_extraction_network(vi_image)
            # 提取红外特征
            ir_feature = self.ir_feature_extraction_network(ir_image)
            # 通道维拼接形成联合特征
            feature = tf.concat([vi_feature, ir_feature], axis=-1)
            # 解码重建融合图像
            f_image = self.feature_reconstruction_network(feature)
        return f_image

5.2 Sobel 梯度算子(对应式(4)(5)中的 ∇)

def gradient(input):
    filter1 = tf.reshape(tf.constant([[-1., 0., 1.], [-2., 0., 2.], [-1., 0., 1.]]), [3, 3, 1, 1])
    filter2 = tf.reshape(tf.constant([[-1., -2., -1.], [0., 0., 0.], [1., 2., 1.]]), [3, 3, 1, 1])
    Gradient1 = tf.nn.conv2d(input, filter1, strides=[1, 1, 1, 1], padding='SAME')
    Gradient2 = tf.nn.conv2d(input, filter2, strides=[1, 1, 1, 1], padding='SAME')
    Gradient = tf.abs(Gradient1) + tf.abs(Gradient2)
    return Gradient

5.3 Loss 计算(对应式(2)-(6),含 mask 分区)

with tf.name_scope('g_loss'):
    self.ir_mask = (self.ir_mask + 1) / 2.0
    self.ir_p_loss_train = tf.multiply(self.ir_mask, tf.abs(self.fusion_images - self.ir_images))
    self.vi_p_loss_train = tf.multiply(1 - self.ir_mask, tf.abs(self.fusion_images - self.vi_images))
    self.ir_grad_loss_train = tf.multiply(self.ir_mask, tf.abs(gradient(self.fusion_images) - gradient(self.ir_images)))
    self.vi_grad_loss_train = tf.multiply(1 - self.ir_mask, tf.abs(gradient(self.fusion_images) - gradient(self.vi_images)))

    self.ir_p_loss = tf.reduce_mean(self.ir_p_loss_train)
    self.vi_p_loss = tf.reduce_mean(self.vi_p_loss_train)
    self.ir_grad_loss = tf.reduce_mean(self.ir_grad_loss_train)
    self.vi_grad_loss = tf.reduce_mean(self.vi_grad_loss_train)
    self.g_loss_2 = 1 * self.vi_p_loss + 1 * self.vi_grad_loss + 7 * self.ir_p_loss + 7 * self.ir_grad_loss

5.4 训练流程(“Training Details”)

flags.DEFINE_integer("epoch", 30, "Number of epoch [10]")
flags.DEFINE_integer("batch_size", 32, "The size of batch images [128]")
flags.DEFINE_integer("image_size", 128, "The size of image to use [33]")
flags.DEFINE_integer("stride", 24, "The size of stride to apply input image [14]")
flags.DEFINE_float("learning_rate", 1e-3, "The learning rate of gradient descent algorithm [1e-4]")
with tf.name_scope('train_step'):
    self.train_generator_op = tf.train.AdamOptimizer(config.learning_rate).minimize(self.g_loss_total, var_list=self.g_vars)
for ep in range(config.epoch):
    lr = self.init_lr if ep < self.decay_epoch else self.init_lr * (config.epoch - ep) / (config.epoch - self.decay_epoch)
    batch_idxs = len(train_data_ir) // config.batch_size
    for idx in range(0, batch_idxs):
        batch_vi_images = train_data_vi[idx * config.batch_size: (idx + 1) * config.batch_size]
        batch_ir_images = train_data_ir[idx * config.batch_size: (idx + 1) * config.batch_size]
        batch_ir_mask = train_data_ir_mask[idx * config.batch_size: (idx + 1) * config.batch_size]
        batch_ir_mask = (batch_ir_mask + 1.0) / 2.0
        _, err_g, batch_vi_p_loss, batch_ir_p_loss, batch_vi_grad_loss, batch_ir_grad_loss, summary_str = self.sess.run(
            [self.train_generator_op, self.g_loss_total, self.vi_p_loss, self.ir_p_loss,
             self.vi_grad_loss, self.ir_grad_loss, self.summary_op],
            feed_dict={self.vi_images: batch_vi_images, self.ir_images: batch_ir_images,
                       self.ir_mask: batch_ir_mask, self.lr: lr})

6. 实验设置(Datasets / Metrics / Training Details)

6.1 数据集(TNO & RoadScene)

6.2 指标(EN / MI / VIF / SF)

6.3 训练细节(Training Details)


7. 对比实验结果

7.1 对比方法(9个)


7.2 TNO:主观结果(Figs.5–8)与论文给出的观察


7.3 TNO:客观结果(Fig.9 + Table I)与论文对指标的解释


7.4 泛化实验(RoadScene):彩色可见图像的融合策略 + 论文观察


7.5 效率对比(Table II)与论文结论


8. “显著目标检测”可视化(Fig.15)


9. 消融实验(Fig.16 + Table III):期望信息定义 & 梯度loss

9.1 w/o desired information(去掉“期望信息定义”的消融)

9.2 w/o gradient loss(去掉梯度loss的消融)


10. 结论(Conclusion)



分享文章:

  1. 【论文阅读 | TCSVT 2024 | MMI-Det:探索可见光与红外目标检测的多模态融合】论文阅读 · MMI-Det:探索可见光与红外目标检测的多模态融合
  2. 【论文阅读 | CVPR 2024 | RSDet_去除再选择:一种用于 RGB - 红外目标检测的由粗到精融合视角】论文阅读 · RSDet_去除再选择:一种用于 RGB - 红外目标检测的由粗到精融合视角
  3. 【论文阅读 | Information Fusion 2025 | DAFusion:面向退化场景的红外与可见光图像融合网络】论文阅读 · A Degradation-Aware Guided Fusion Network for Infrared and Visible Image
上一篇
【论文阅读 | TPAMI 2025 | RWKVFusion:利用统一语言与掩码引导的高效图像融合网络】
下一篇
Slurm 使用入门教程