4. 联合双边滤波利用彩色图引导深度图增强

任务说明

本案例实现联合双边滤波(Joint Bilateral Filter, JBF),利用高分辨率彩色图作为引导图,对低质量ToF深度图进行边缘保持的增强处理。输入为一张与深度图已对齐的彩色引导图(单通道灰度或三通道彩色均可)和一张待增强的深度图,输出为滤波后的增强深度图。核心目标是:在平滑深度噪声的同时,利用彩色图的边缘信息防止深度边缘被模糊。

算法原理与实现

联合双边滤波的核心思想是在传统双边滤波的基础上,将像素间的相似性度量从“深度值差异”替换为“引导图颜色差异”。对于像素\(p\)和邻域像素\(q\),权重由两部分构成:空间邻近度(高斯核)和颜色相似度(高斯核)。由于彩色图通常比深度图具有更清晰、准确的边缘,因此颜色差异项能有效引导滤波器在深度边缘处停止平滑,从而保持深度不连续。

本实现采用如下计算流程:

  1. 读取引导图(彩色或灰度)与深度图,确保二者尺寸一致;
  2. 对引导图进行灰度化(若为彩色)并归一化到[0,1];
  3. 对深度图进行归一化处理,便于统一计算;
  4. 对每个像素,在设定的窗口半径内计算空间权重与颜色权重,加权求和得到滤波结果;
  5. 将结果反归一化并保存为16位PNG(与ToF深度图常见格式一致)。

为控制计算复杂度,本实现使用C++编写,并采用OpenMP并行化(可选)。代码中所有参数(窗口半径、空间sigma、颜色sigma)均在main函数中显式定义,便于复现。

代码实现

// jbf_depth_enhance.cpp
// 编译: g++ -O3 -fopenmp jbf_depth_enhance.cpp -o jbf -lopencv_core -lopencv_imgcodecs -lopencv_imgproc
// 运行: ./jbf guide.png depth.png output.png 5 2.0 0.1
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <cmath>

cv::Mat jointBilateralFilter(const cv::Mat& guide, const cv::Mat& depth,
                             int radius, float sigmaSpace, float sigmaColor) {
    CV_Assert(guide.size() == depth.size());
    CV_Assert(depth.type() == CV_16UC1 || depth.type() == CV_32FC1);
    
    // 转换为浮点并归一化
    cv::Mat guideFloat, depthFloat;
    if (guide.channels() == 3) {
        cv::cvtColor(guide, guideFloat, cv::COLOR_BGR2GRAY);
    } else {
        guideFloat = guide.clone();
    }
    guideFloat.convertTo(guideFloat, CV_32FC1, 1.0/255.0);
    
    if (depth.type() == CV_16UC1) {
        depth.convertTo(depthFloat, CV_32FC1, 1.0/65535.0);
    } else {
        depthFloat = depth.clone();
    }
    
    cv::Mat result = cv::Mat::zeros(depth.size(), CV_32FC1);
    int rows = depth.rows, cols = depth.cols;
    
    // 预计算空间权重
    int winSize = 2 * radius + 1;
    std::vector<float> spaceWeight(winSize * winSize);
    float spaceSigma2 = 2.0f * sigmaSpace * sigmaSpace;
    float colorSigma2 = 2.0f * sigmaColor * sigmaColor;
    for (int dy = -radius; dy <= radius; ++dy) {
        for (int dx = -radius; dx <= radius; ++dx) {
            float dist2 = dx*dx + dy*dy;
            spaceWeight[(dy+radius)*winSize + (dx+radius)] = std::exp(-dist2 / spaceSigma2);
        }
    }
    
    #pragma omp parallel for schedule(dynamic)
    for (int y = 0; y < rows; ++y) {
        for (int x = 0; x < cols; ++x) {
            float centerGuide = guideFloat.at<float>(y, x);
            float centerDepth = depthFloat.at<float>(y, x);
            float sumW = 0.0f, sumD = 0.0f;
            
            for (int dy = -radius; dy <= radius; ++dy) {
                int ny = y + dy;
                if (ny < 0 || ny >= rows) continue;
                for (int dx = -radius; dx <= radius; ++dx) {
                    int nx = x + dx;
                    if (nx < 0 || nx >= cols) continue;
                    
                    float guideDiff = guideFloat.at<float>(ny, nx) - centerGuide;
                    float colorW = std::exp(-(guideDiff*guideDiff) / colorSigma2);
                    float w = spaceWeight[(dy+radius)*winSize + (dx+radius)] * colorW;
                    sumW += w;
                    sumD += w * depthFloat.at<float>(ny, nx);
                }
            }
            result.at<float>(y, x) = (sumW > 1e-6f) ? (sumD / sumW) : centerDepth;
        }
    }
    
    // 反归一化到16位
    cv::Mat output16U;
    result.convertTo(output16U, CV_16UC1, 65535.0);
    return output16U;
}

int main(int argc, char** argv) {
    if (argc != 7) {
        std::cerr << "用法: " << argv[0] << " guide.png depth.png output.png radius sigmaSpace sigmaColor\n";
        return -1;
    }
    cv::Mat guide = cv::imread(argv[1], cv::IMREAD_UNCHANGED);
    cv::Mat depth = cv::imread(argv[2], cv::IMREAD_UNCHANGED);
    if (guide.empty() || depth.empty()) {
        std::cerr << "图像读取失败\n";
        return -1;
    }
    int radius = std::atoi(argv[4]);
    float sigmaSpace = std::atof(argv[5]);
    float sigmaColor = std::atof(argv[6]);
    
    cv::Mat enhanced = jointBilateralFilter(guide, depth, radius, sigmaSpace, sigmaColor);
    cv::imwrite(argv[3], enhanced);
    std::cout << "滤波完成,输出已保存至 " << argv[3] << "\n";
    return 0;
}

落地与验证

依赖环境:OpenCV 3.4+(需包含imgcodecs、imgproc模块),支持C++11的编译器,可选OpenMP。

输入数据准备:准备一张彩色引导图(如RGB纹理图)和一张16位深度图(单位通常为毫米),二者需像素级对齐且尺寸一致。若无真实ToF数据,可用仿真数据:取一张彩色图作为引导图,将深度图设为平滑渐变并在边缘处添加噪声来模拟。

运行命令:

./jbf guide.png depth.png enhanced.png 5 2.0 0.1

验证方法:

  1. 目视检查输出图像:深度图平坦区域噪声应明显减少,而物体边缘(对应彩色图颜色突变处)应保持锐利,不应出现边缘过度平滑或“光晕”伪影;
  2. 定量评估(可选):若存在真实深度真值,计算滤波前后的均方根误差(RMSE),增强后RMSE应下降;
  3. 参数敏感性测试:增大sigmaColor(如从0.1增至0.3)会增强平滑程度但可能损失边缘;减小sigmaSpace会使滤波更局部化。通过调整参数观察输出变化是否符合联合双边滤波的预期行为。

判定标准:程序正常退出且输出PNG文件可读;在深度平坦区域噪声明显低于输入,同时深度边缘(与彩色图边缘对齐处)的梯度变化未被明显削弱,即视为通过。

联合双边滤波数据流与权重计算示意 彩色引导图 RGB / 灰度 提供边缘结构信息 ToF深度图 16-bit / 浮点 含噪声与边缘模糊 联合双边滤波 权重 = 空间核 × 颜色核 空间核: exp(-Δd²/2σ_s²) 颜色核: exp(-ΔI²/2σ_c²) 引导值 深度值 增强深度图 边缘保持 噪声抑制 关键点: • 颜色核使用引导图灰度差,深度边缘处颜色差异大 → 权重小 → 边缘不被平滑 • 平坦区域颜色接近 → 权重近似空间高斯 → 等效于高斯平滑去噪