3. 双边滤波保持边缘的ToF深度图去噪

双边滤波保持边缘的ToF深度图去噪 学习卡片

任务说明

ToF(Time-of-Flight)深度相机在弱反射、多路径干扰等场景下,深度图常含有高斯噪声与离群脉冲噪声,同时物体边缘处深度跳变明显。本案例实现一种保持边缘的深度图去噪算法:采用双边滤波(Bilateral Filter)对深度图进行处理,在平滑噪声的同时保留深度不连续边缘。输入为单张16位深度图(单位mm),输出为滤波后的16位深度图,并给出可量化的边缘保持评价指标。

算法原理与实现

双边滤波的权重由空间邻近度(高斯核)与灰度/深度相似度(高斯核)共同决定:

I'(p) = (1/W_p) · Σ_{q∈N(p)} I(q) · exp(-||p-q||²/(2σ_s²)) · exp(-|I(p)-I(q)|²/(2σ_r²))

其中σ_s控制空间平滑范围,σ_r控制深度值差异的敏感度。当像素位于边缘两侧时,深度差异大,对应权重趋近于0,从而避免边缘被模糊。本实现采用分离式加速:先计算空间权重(仅与坐标差有关,可预计算),再对每个像素遍历邻域累加。为处理深度图中的无效像素(值为0),滤波时跳过这些点并重新归一化权重。

代码实现


#include <opencv2/opencv.hpp>
#include <vector>
#include <cmath>
#include <iostream>

// 保持边缘的双边滤波(针对16位深度图)
// src: 输入CV_16UC1深度图(单位mm,0表示无效)
// dst: 输出滤波后深度图
// sigmaSpace: 空间高斯标准差(像素)
// sigmaRange: 深度值高斯标准差(mm)
// radius: 邻域半径(实际窗口为2*radius+1)
void bilateralDepthFilter(const cv::Mat& src, cv::Mat& dst,
                          double sigmaSpace, double sigmaRange, int radius) {
    CV_Assert(src.type() == CV_16UC1);
    dst = cv::Mat::zeros(src.size(), CV_16UC1);

    // 预计算空间权重表
    int winSize = 2 * radius + 1;
    std::vector<float> spaceWeight(winSize * winSize);
    float spaceCoeff = -0.5f / (sigmaSpace * sigmaSpace);
    for (int dy = -radius; dy <= radius; ++dy) {
        for (int dx = -radius; dx <= radius; ++dx) {
            int idx = (dy + radius) * winSize + (dx + radius);
            spaceWeight[idx] = std::exp(spaceCoeff * (dx * dx + dy * dy));
        }
    }

    float rangeCoeff = -0.5f / (sigmaRange * sigmaRange);
    int rows = src.rows, cols = src.cols;

    for (int y = 0; y < rows; ++y) {
        const uint16_t* srcRow = src.ptr<uint16_t>(y);
        uint16_t* dstRow = dst.ptr<uint16_t>(y);
        for (int x = 0; x < cols; ++x) {
            uint16_t centerVal = srcRow[x];
            if (centerVal == 0) continue; // 无效像素保持0

            double sum = 0.0, weightSum = 0.0;
            for (int dy = -radius; dy <= radius; ++dy) {
                int ny = y + dy;
                if (ny < 0 || ny >= rows) continue;
                const uint16_t* neighborRow = src.ptr<uint16_t>(ny);
                for (int dx = -radius; dx <= radius; ++dx) {
                    int nx = x + dx;
                    if (nx < 0 || nx >= cols) continue;
                    uint16_t neighborVal = neighborRow[nx];
                    if (neighborVal == 0) continue; // 跳过无效点

                    int idx = (dy + radius) * winSize + (dx + radius);
                    float w = spaceWeight[idx];
                    if (w == 0.0f) continue;

                    int diff = (int)centerVal - (int)neighborVal;
                    w *= std::exp(rangeCoeff * diff * diff);
                    sum += w * neighborVal;
                    weightSum += w;
                }
            }
            if (weightSum > 0) {
                dstRow[x] = (uint16_t)std::round(sum / weightSum);
            } else {
                dstRow[x] = centerVal; // 邻域全无效时保留原值
            }
        }
    }
}

// 计算边缘保持率:边缘像素中滤波前后深度差小于阈值的比例
double edgePreservationRate(const cv::Mat& src, const cv::Mat& dst, int edgeThreshold = 50) {
    cv::Mat srcEdge, dstEdge;
    cv::Sobel(src, srcEdge, CV_32F, 1, 0, 3);
    cv::Sobel(dst, dstEdge, CV_32F, 1, 0, 3);
    cv::Mat srcMag, dstMag;
    cv::magnitude(srcEdge, srcEdge, srcMag);
    cv::magnitude(dstEdge, dstEdge, dstMag);

    // 边缘像素:原图梯度幅值大于阈值
    cv::Mat edgeMask = srcMag > edgeThreshold;
    int edgeCount = cv::countNonZero(edgeMask);
    if (edgeCount == 0) return 1.0;

    // 边缘处深度变化量
    cv::Mat diff;
    cv::absdiff(src, dst, diff);
    cv::Mat edgeDiff;
    diff.copyTo(edgeDiff, edgeMask);
    int preserved = cv::countNonZero(edgeDiff <= 30); // 30mm内视为保持
    return (double)preserved / edgeCount;
}

int main() {
    // 生成模拟深度图:200x200,含两个深度平面(1000mm和2000mm)及高斯噪声
    cv::Mat depth(200, 200, CV_16UC1, cv::Scalar(1000));
    for (int y = 0; y < 200; ++y) {
        for (int x = 0; x < 200; ++x) {
            if (x > 100) depth.at<uint16_t>(y, x) = 2000;
        }
    }
    // 添加高斯噪声(sigma=15mm)
    cv::Mat noise = cv::Mat::zeros(depth.size(), CV_16SC1);
    cv::randn(noise, 0, 15);
    cv::Mat depthNoisy;
    depth.convertTo(depthNoisy, CV_16SC1);
    depthNoisy += noise;
    depthNoisy.convertTo(depth, CV_16UC1);

    // 执行双边滤波
    cv::Mat filtered;
    bilateralDepthFilter(depth, filtered, 5.0, 30.0, 3);

    // 验证:计算边缘保持率与平滑度
    double edgeRate = edgePreservationRate(depth, filtered);
    cv::Mat smoothRegion = depth(cv::Rect(20, 20, 60, 60)); // 左平面平滑区
    cv::Mat smoothFiltered = filtered(cv::Rect(20, 20, 60, 60));
    cv::Scalar meanSrc, stdSrc, meanDst, stdDst;
    cv::meanStdDev(smoothRegion, meanSrc, stdSrc);
    cv::meanStdDev(smoothFiltered, meanDst, stdDst);

    std::cout << "边缘保持率: " << edgeRate << std::endl;
    std::cout << "平滑区噪声标准差(滤波前): " << stdSrc[0] << " mm" << std::endl;
    std::cout << "平滑区噪声标准差(滤波后): " << stdDst[0] << " mm" << std::endl;

    // 保存结果
    cv::imwrite("depth_noisy.png", depth);
    cv::imwrite("depth_filtered.png", filtered);
    return 0;
}

编译与运行


# 依赖:OpenCV 4.x(含core、imgproc、imgcodecs模块)
g++ -o bilateral_depth bilateral_depth.cpp $(pkg-config --cflags --libs opencv4)
./bilateral_depth

验证方法与判定标准

程序输出两个关键指标:

  • 边缘保持率:应大于0.85(即85%以上的边缘像素深度变化不超过30mm),证明边缘未被过度平滑;
  • 平滑区噪声标准差:滤波后应显著低于滤波前(本模拟场景中滤波前约15mm,滤波后应降至5mm以下),证明噪声被有效抑制。

同时可打开输出的depth_noisy.png与depth_filtered.png,目视检查垂直边缘是否清晰锐利,而平坦区域是否更均匀。若边缘保持率偏低,可增大σ_r;若噪声残留较多,可增大σ_s或radius。

双边滤波深度图去噪处理流程 输入深度图 16位,含噪声 + 边缘跳变 参数设置 σ_s = 5.0 (空间) σ_r = 30.0 mm (深度) radius = 3 (窗口7×7) 无效像素(0)自动跳过 双边滤波核心 空间权重 × 深度权重 逐像素邻域加权 边缘处深度差大 → 权重趋近0 输出深度图 噪声抑制 边缘保持 验证指标 ① 边缘保持率 = 边缘像素中 |Δ深度| ≤ 30mm 的比例 ② 平滑区噪声标准差(滤波后应显著降低) 判定标准 ✓ 边缘保持率 > 0.85 ✓ 噪声标准差降至5mm以下 关键参数调节策略 边缘保持率偏低 → 增大σ_r(如30→50mm) 噪声残留较多 → 增大σ_s或radius