视觉SLAM笔记(62) 单目稠密重建


1. 测试数据集

将使用 REMODE 的测试数据集
它提供了一架无人机采集的单目俯视图像,共有 200 张,同时提供了每张图像的真实位姿
下面来考虑在这些数据的基础上,估算第一帧图像每个像素对应的深度值,即进行单目稠密重建

首先,从 http://rpg.ifi.uzh.ch/datasets/remode_test_data.zip 处下载所用的数据
解压后,将在 test_data/Images 中发现从 0 至 200 的所有图像
在这里插入图片描述
并在 test_data 目录下看到一个文本文件 first_200_frames_traj_over_table_input_sequence.txt
它记录了每张图像对应的位姿:
在这里插入图片描述
可以看到场景主要由地面、桌子以及桌子上的杂物组成
如果深度估计大致正确,那么至少可以看出桌子与地面的深度值不同之处


2. 稠密深度估计

下面,按照之前的讲解,书写稠密深度估计程序
为了方便理解,把程序书写成了 C 语言风格,放在单个文件 dense_mapping.cpp
程序稍微有点长,这里在重点列出几个重要函数

#include <iostream>
#include <vector>
#include <fstream>
using namespace std;
#include <boost/timer.hpp>

// for sophus 
#include <sophus/se3.h>
using Sophus::SE3;

// for eigen 
#include <Eigen/Core>
#include <Eigen/Geometry>
using namespace Eigen;

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace cv;

// ------------------------------------------------------------------
// 参数
const int boarder = 20;     // 边缘宽度
const int width = 640;      // 宽度 
const int height = 480;      // 高度
const double fx = 481.2f;    // 相机内参
const double fy = -480.0f;
const double cx = 319.5f;
const double cy = 239.5f;
const int ncc_window_size = 2;    // NCC 取的窗口半宽度
const int ncc_area = (2 * ncc_window_size + 1)*(2 * ncc_window_size + 1); // NCC窗口面积
const double min_cov = 0.1;    // 收敛判定:最小方差
const double max_cov = 10;    // 发散判定:最大方差

// ------------------------------------------------------------------
// 重要的函数 
// 从 REMODE 数据集读取数据  
bool readDatasetFiles(
    const string& path,
    vector<string>& color_image_files,
    vector<SE3>& poses
);

// 根据新的图像更新深度估计
bool update(
    const Mat& ref,
    const Mat& curr,
    const SE3& T_C_R,
    Mat& depth,
    Mat& depth_cov
);

// 极线搜索 
bool epipolarSearch(
    const Mat& ref,
    const Mat& curr,
    const SE3& T_C_R,
    const Vector2d& pt_ref,
    const double& depth_mu,
    const double& depth_cov,
    Vector2d& pt_curr
);

// 更新深度滤波器 
bool updateDepthFilter(
    const Vector2d& pt_ref,
    const Vector2d& pt_curr,
    const SE3& T_C_R,
    Mat& depth,
    Mat& depth_cov
);

// 计算 NCC 评分 
double NCC(const Mat& ref, const Mat& curr, const Vector2d& pt_ref, const Vector2d& pt_curr);

// 双线性灰度插值 
inline double getBilinearInterpolatedValue(const Mat& img, const Vector2d& pt) {
    uchar* d = &img.data[int(pt(1, 0))*img.step + int(pt(0, 0))];
    double xx = pt(0, 0) - floor(pt(0, 0));
    double yy = pt(1, 0) - floor(pt(1, 0));
    return  ((1 - xx) * (1 - yy) * double(d[0]) +
        xx * (1 - yy) * double(d[1]) +
        (1 - xx) *yy* double(d[img.step]) +
        xx * yy*double(d[img.step + 1])) / 255.0;
}

// ------------------------------------------------------------------
// 一些小工具 
// 显示估计的深度图 
bool plotDepth(const Mat& depth);

// 像素到相机坐标系 
inline Vector3d px2cam(const Vector2d px) {
    return Vector3d(
        (px(0, 0) - cx) / fx,
        (px(1, 0) - cy) / fy,
        1
    );
}

// 相机坐标系到像素 
inline Vector2d cam2px(const Vector3d p_cam) {
    return Vector2d(
        p_cam(0, 0)*fx / p_cam(2, 0) + cx,
        p_cam(1, 0)*fy / p_cam(2, 0) + cy
    );
}

// 检测一个点是否在图像边框内
inline bool inside(const Vector2d& pt) {
    return pt(0, 0) >= boarder && pt(1, 0) >= boarder
        && pt(0, 0) + boarder < width && pt(1, 0) + boarder <= height;
}

// 显示极线匹配 
void showEpipolarMatch(const Mat& ref, const Mat& curr, const Vector2d& px_ref, const Vector2d& px_curr);

// 显示极线 
void showEpipolarLine(const Mat& ref, const Mat& curr, const Vector2d& px_ref, const Vector2d& px_min_curr, const Vector2d& px_max_curr);
// ------------------------------------------------------------------


int main(int argc, char** argv)
{
    if (argc != 2)
    {
        cout << "Usage: dense_mapping path_to_test_dataset" << endl;
        return -1;
    }

    // 从数据集读取数据
    vector<string> color_image_files;
    vector<SE3> poses_TWC;
    bool ret = readDatasetFiles(argv[1], color_image_files, poses_TWC);
    if (ret == false)
    {
        cout << "Reading image files failed!" << endl;
        return -1;
    }
    cout << "read total " << color_image_files.size() << " files." << endl;

    // 第一张图
    Mat ref = imread(color_image_files[0], 0);                // gray-scale image 
    SE3 pose_ref_TWC = poses_TWC[0];
    double init_depth = 3.0;    // 深度初始值
    double init_cov2 = 3.0;    // 方差初始值 
    Mat depth(height, width, CV_64F, init_depth);             // 深度图
    Mat depth_cov(height, width, CV_64F, init_cov2);          // 深度图方差 

    for (int index = 1; index < color_image_files.size(); index++)
    {
        cout << "*** loop " << index << " ***" << endl;
        Mat curr = imread(color_image_files[index], 0);
        if (curr.data == nullptr) continue;
        SE3 pose_curr_TWC = poses_TWC[index];
        SE3 pose_T_C_R = pose_curr_TWC.inverse() * pose_ref_TWC; // 坐标转换关系: T_C_W * T_W_R = T_C_R 
        update(ref, curr, pose_T_C_R, depth, depth_cov);
        plotDepth(depth);
        imshow("image", curr);
        waitKey(1);
    }

    cout << "estimation returns, saving depth map ..." << endl;
    imwrite("depth.png", depth);
    cout << "done." << endl;

    return 0;
}

bool readDatasetFiles(
    const string& path,
    vector< string >& color_image_files,
    std::vector<SE3>& poses
)
{
    ifstream fin(path + "../test_data/first_200_frames_traj_over_table_input_sequence.txt");
    if (!fin) return false;

    while (!fin.eof())
    {
        // 数据格式:图像文件名 tx, ty, tz, qx, qy, qz, qw ,注意是 TWC 而非 TCW
        string image;
        fin >> image;
        double data[7];
        for (double& d : data) fin >> d;

        color_image_files.push_back(path + string("/images/") + image);
        poses.push_back(
            SE3(Quaterniond(data[6], data[3], data[4], data[5]),
                Vector3d(data[0], data[1], data[2]))
        );
        if (!fin.good()) break;
    }
    return true;
}

// 对整个深度图进行更新
bool update(const Mat& ref, const Mat& curr, const SE3& T_C_R, Mat& depth, Mat& depth_cov)
{
#pragma omp parallel for
    for (int x = boarder; x < width - boarder; x++)
#pragma omp parallel for
        for (int y = boarder; y < height - boarder; y++)
        {
            // 遍历每个像素
            if (depth_cov.ptr<double>(y)[x] < min_cov || depth_cov.ptr<double>(y)[x] > max_cov) // 深度已收敛或发散
                continue;
            // 在极线上搜索 (x,y) 的匹配 
            Vector2d pt_curr;
            bool ret = epipolarSearch(
                ref,
                curr,
                T_C_R,
                Vector2d(x, y),
                depth.ptr<double>(y)[x],
                sqrt(depth_cov.ptr<double>(y)[x]),
                pt_curr
            );

            if (ret == false) // 匹配失败
                continue;

            // 取消该注释以显示匹配
            // showEpipolarMatch( ref, curr, Vector2d(x,y), pt_curr );

            // 匹配成功,更新深度图 
            updateDepthFilter(Vector2d(x, y), pt_curr, T_C_R, depth, depth_cov);
        }
}

// 极线搜索
bool epipolarSearch(
    const Mat& ref, const Mat& curr,
    const SE3& T_C_R, const Vector2d& pt_ref,
    const double& depth_mu, const double& depth_cov,
    Vector2d& pt_curr)
{
    Vector3d f_ref = px2cam(pt_ref);
    f_ref.normalize();
    Vector3d P_ref = f_ref * depth_mu;    // 参考帧的 P 向量

    Vector2d px_mean_curr = cam2px(T_C_R*P_ref); // 按深度均值投影的像素
    double d_min = depth_mu - 3 * depth_cov, d_max = depth_mu + 3 * depth_cov;
    if (d_min < 0.1) d_min = 0.1;
    Vector2d px_min_curr = cam2px(T_C_R*(f_ref*d_min));    // 按最小深度投影的像素
    Vector2d px_max_curr = cam2px(T_C_R*(f_ref*d_max));    // 按最大深度投影的像素

    Vector2d epipolar_line = px_max_curr - px_min_curr;    // 极线(线段形式)
    Vector2d epipolar_direction = epipolar_line;        // 极线方向 
    epipolar_direction.normalize();
    double half_length = 0.5*epipolar_line.norm();    // 极线线段的半长度
    if (half_length > 100) half_length = 100;   // 我们不希望搜索太多东西 

    // 取消此句注释以显示极线(线段)
    // showEpipolarLine( ref, curr, pt_ref, px_min_curr, px_max_curr );

    // 在极线上搜索,以深度均值点为中心,左右各取半长度
    double best_ncc = -1.0;
    Vector2d best_px_curr;
    for (double l = -half_length; l <= half_length; l += 0.7)  // l+=sqrt(2) 
    {
        Vector2d px_curr = px_mean_curr + l * epipolar_direction;  // 待匹配点
        if (!inside(px_curr))
            continue;
        // 计算待匹配点与参考帧的 NCC
        double ncc = NCC(ref, curr, pt_ref, px_curr);
        if (ncc > best_ncc)
        {
            best_ncc = ncc;
            best_px_curr = px_curr;
        }
    }
    if (best_ncc < 0.85f)      // 只相信 NCC 很高的匹配
        return false;
    pt_curr = best_px_curr;
    return true;
}

double NCC(
    const Mat& ref, const Mat& curr,
    const Vector2d& pt_ref, const Vector2d& pt_curr
)
{
    // 零均值-归一化互相关
    // 先算均值
    double mean_ref = 0, mean_curr = 0;
    vector<double> values_ref, values_curr; // 参考帧和当前帧的均值
    for (int x = -ncc_window_size; x <= ncc_window_size; x++)
        for (int y = -ncc_window_size; y <= ncc_window_size; y++)
        {
            double value_ref = double(ref.ptr<uchar>(int(y + pt_ref(1, 0)))[int(x + pt_ref(0, 0))]) / 255.0;
            mean_ref += value_ref;

            double value_curr = getBilinearInterpolatedValue(curr, pt_curr + Vector2d(x, y));
            mean_curr += value_curr;

            values_ref.push_back(value_ref);
            values_curr.push_back(value_curr);
        }

    mean_ref /= ncc_area;
    mean_curr /= ncc_area;

    // 计算去均值化 NCC
    double numerator = 0, demoniator1 = 0, demoniator2 = 0;
    for (int i = 0; i < values_ref.size(); i++)
    {
        double n = (values_ref[i] - mean_ref) * (values_curr[i] - mean_curr);
        numerator += n;
        demoniator1 += (values_ref[i] - mean_ref)*(values_ref[i] - mean_ref);
        demoniator2 += (values_curr[i] - mean_curr)*(values_curr[i] - mean_curr);
    }
    return numerator / sqrt(demoniator1*demoniator2 + 1e-10);   // 防止分母出现零
}

bool updateDepthFilter(
    const Vector2d& pt_ref,
    const Vector2d& pt_curr,
    const SE3& T_C_R,
    Mat& depth,
    Mat& depth_cov
)
{
    // 用三角化计算深度
    SE3 T_R_C = T_C_R.inverse();
    Vector3d f_ref = px2cam(pt_ref);
    f_ref.normalize();
    Vector3d f_curr = px2cam(pt_curr);
    f_curr.normalize();

    // 方程
    // d_ref * f_ref = d_cur * ( R_RC * f_cur ) + t_RC
    // => [ f_ref^T f_ref, -f_ref^T f_cur ] [d_ref] = [f_ref^T t]
    //    [ f_cur^T f_ref, -f_cur^T f_cur ] [d_cur] = [f_cur^T t]
    // 二阶方程用克莱默法则求解并解之
    Vector3d t = T_R_C.translation();
    Vector3d f2 = T_R_C.rotation_matrix() * f_curr;
    Vector2d b = Vector2d(t.dot(f_ref), t.dot(f2));
    double A[4];
    A[0] = f_ref.dot(f_ref);
    A[2] = f_ref.dot(f2);
    A[1] = -A[2];
    A[3] = -f2.dot(f2);
    double d = A[0] * A[3] - A[1] * A[2];
    Vector2d lambdavec =
        Vector2d(A[3] * b(0, 0) - A[1] * b(1, 0),
            -A[2] * b(0, 0) + A[0] * b(1, 0)) / d;
    Vector3d xm = lambdavec(0, 0) * f_ref;
    Vector3d xn = t + lambdavec(1, 0) * f2;
    Vector3d d_esti = (xm + xn) / 2.0;  // 三角化算得的深度向量
    double depth_estimation = d_esti.norm();   // 深度值

    // 计算不确定性(以一个像素为误差)
    Vector3d p = f_ref * depth_estimation;
    Vector3d a = p - t;
    double t_norm = t.norm();
    double a_norm = a.norm();
    double alpha = acos(f_ref.dot(t) / t_norm);
    double beta = acos(-a.dot(t) / (a_norm*t_norm));
    double beta_prime = beta + atan(1 / fx);
    double gamma = M_PI - alpha - beta_prime;
    double p_prime = t_norm * sin(beta_prime) / sin(gamma);
    double d_cov = p_prime - depth_estimation;
    double d_cov2 = d_cov * d_cov;

    // 高斯融合
    double mu = depth.ptr<double>(int(pt_ref(1, 0)))[int(pt_ref(0, 0))];
    double sigma2 = depth_cov.ptr<double>(int(pt_ref(1, 0)))[int(pt_ref(0, 0))];

    double mu_fuse = (d_cov2*mu + sigma2 * depth_estimation) / (sigma2 + d_cov2);
    double sigma_fuse2 = (sigma2 * d_cov2) / (sigma2 + d_cov2);

    depth.ptr<double>(int(pt_ref(1, 0)))[int(pt_ref(0, 0))] = mu_fuse;
    depth_cov.ptr<double>(int(pt_ref(1, 0)))[int(pt_ref(0, 0))] = sigma_fuse2;

    return true;
}
  1. main 函数非常简单
    它只负责从数据集中读取图像,然后交给 update 函数,对深度图进行更新
  2. update 函数中,遍历了参考帧的每个像素
    先在当前帧中寻找极线匹配,若能匹配上,则利用极线匹配的结果更新深度图的估计
  3. 极线搜索原理大致和 视觉SLAM笔记(61) 单目稠密建图 介绍的相同
    但实现上添加了一些细节:因为假设深度值服从高斯分布,就以均值为中心,左右各取 ±3σ 作为半径,然后在当前帧中寻找极线的投影。然后,遍历此极线上的像素(步长取 2 / 2 \sqrt{2}/2 的近似值 0.7),寻找 NCC 最高的点,作为匹配点
    如果最高的 NCC 也低于阈值(这里取 0.85),则认为匹配失败
  4. NCC 的计算使用了去均值化后的做法,即对于图像块 A, B,取:
    在这里插入图片描述

编译此程序后,以数据集目录作为参数

$ ./dense_mapping ../test_data/

程序输出的信息比较简洁,仅显示了迭代次数、当前图像和深度图

在这里插入图片描述

关于深度图,显示的是深度值乘以 0.4 后的结果
也就是纯白点(数值为 1.0)的深度约 2.5 米,颜色越深表示深度值越小,也就是物体离我们越近
如果实际运行了程序,应该会发现深度估计是一个动态的过程
从一个不怎么确定的初始值逐渐收敛到稳定值的过程
初始值使用了均值和方差均为 3.0 的分布
当然也可以修改初始分布,看看对结果会产生怎样的影响

当迭代次数超过一定次之后,深度图趋于稳定,不再对新的数据产生改变
观察稳定之后的深度图,发现大致可以看出地板和桌子的区别,而桌上的物体深度则接近于桌子
整个估计大部分是正确的,但也存在着大量错误估计
它们表现为深度图中,与周围数据不一致的地方,为过大或过小的估计
此外,位于边缘处的地方,由于运动过程中看到的次数较少,所以亦没有得到正确的估计
综上所述,认为这个深度图的大部分是正确的,但没有达到预想的效果


3. 像素梯度的问题

对深度图像进行观察,会发现一件明显的事实
块匹配的正确与否,依赖于图像块是否具有区分度
显然,如果图像块仅是一片黑或者一片白,缺少有效的信息
那么在NCC 计算中,就很可能错误地将它与周围的某块像素给匹配起来

观察打印机表面,由于它是均匀的白色,非常容易引起误匹配
因此打印机表面的深度信息,多半是不正确的
程序的空间表面出现了明显不该有的条纹状深度估计,而根据直观想象,打印机表面肯定是光滑的

这里牵涉到了一个问题,该问题在直接法中已经见过一次
在进行块匹配(和 NCC的计算)时,必须假设小块不变,然后将该小块与其他小块进行对比
这时,有明显梯度的小块将具有良好的区分度,不易引起误匹配
对于 梯度不明显的像素,由于在块匹配时没有区分性
所以将难以有效地估计其深度
反之,像素梯度比较明显的地方,得到的深度信息也相对准确
例如桌面上的杂志、电话等具有明显纹理的物体

因此,反映了立体视觉中一个非常常见的问题: 对物体纹理的依赖性
该问题在双目视觉中也极其常见,体现了立体视觉的重建质量,十分依赖于环境纹理
这里刻意使用了纹理较好的环境:例如像棋盘格一般的地板,带有木纹的桌面等等
因此能得到一个看似不错的结果

然而在实际中,像墙面、光滑物体表面等亮度均匀的地方将经常出现,影响对它的深度估计
从某种角度来说,如果依然只关心某个像素周围的邻域(小块)的话
该问题是 无法在现有的算法流程上加以改进并解决的

在这里插入图片描述
举两种比较极端的情况:像素梯度垂直于极线方向,以及平行于极线方向

先来看垂直的情况
能够精确地确定匹配度最高点出现在何处

反之,在平行的例子里
在平行的例子里,即使小块有明显梯度
但是当沿着极线去做块匹配时,会发现匹配程度都是一样的
因此得不到有效的匹配

而实际当中,梯度与极线的情况很可能位于二者之间:既不是完全垂直亦不是完全平行

这时,当像素梯度与极线夹角较小时,极线匹配的不确定性大;
而当夹角较大时,匹配的不确定性变小

而在演示中,统一地把这些情况都当成一个像素的误差,实质是不够精细的
考虑到极线与像素梯度的关系后,应该使用更精确的不确定性模型


4. 逆深度

从另一个角度来看,不妨可以问:把像素深度假设成高斯分布,是否合适呢?
这里关系到一个参数化的问题(Parameterization)

经常用一个点的世界坐标 x,y,z 三个量来描述它,这是一种参数化形式
认为 x,y,z 三个量都是随机的,它们服从(三维的)高斯分布
然而,这里使用了图像坐标 u,v 和深度值 d 来描述某个空间点(即稠密建图)
认为 u,v 不动,而 d 服从(一维的)高斯分布,这是另一种参数化形式

那么要问:这两种参数化形式有什么不同吗?
是否也能假设 u,v 服从高斯分布,从而形成另一种参数化形式呢?
不同的参数化形式,实际都描述了同一个量,也就是某个三维空间点
考虑到当在相机看到某个点时,它的图像坐标 u,v,是比较确定的‹,而深度值 d 则是非常不确定的

此时,若用世界坐标 x,y,z 描述这个点
那么根据相机当前的位姿, x,y,z 三个量之间可能存在明显的相关性
反映在协方差矩阵中,表现为非对角元素不为零
而如果用 u,v,d参数化一个点,那么它的 u,v 和 d 至少是近似独立的
甚至亦能认为 u,v 也是独立的——从而它的协方差矩阵近似为对角阵,更为简洁

逆深度(Inverse depth)是近年 SLAM 研究中,出现的一种广泛使用的参数化技巧
在演示中,假设深度值满足高斯分布: d ∼ N(µ, σ2)
仔细想想,深度的正态分布确实存在一些问题:

  1. 实际想表达的是:这个场景深度大概是 5-10 米,可能有一些更远的点
    但近处肯定不会小于相机焦距(或认为深度不会小于 0)
    这个分布并不是像高斯分布那样,形成一个对称的形状
    它的尾部可能稍长,而负数区域则为零
  2. 在一些室外应用中,可能存在距离非常远,乃至无穷远处的点
    初始值中难以涵盖这些点,并且用高斯分布描述它们会有一些数值计算上的困难

于是,逆深度应运而生
在仿真中发现,假设深度的倒数,也就是逆深度,为高斯分布是比较有效的
随后,在实际应用中,逆深度也具有更好的数值稳定性
从而逐渐成为一种通用的技巧,存在于现有 SLAM 方案中的标准做法中

把演示从正深度改成逆深度亦不复杂
只要在现深度的推导中,将 d 改成逆深度 d−1 即可


5. 图像间的变换

在块匹配之前,做一次图像到图像间的变换亦是一种常见的预处理方式
这是因为,假设了图像小块在相机运动时保持不变
而这个假设在相机平移时(示例数据集基本都是这样的例子)能够保持成立
但当相机发生明显的旋转时,就难以继续保持了

特别地,当相机绕光心旋转时,一个下黑上白的图像可能会变成一个上黑下白的图像块,导致相关
性直接变成了负数(尽管仍然是同样一个块)
为了防止这种情况的出现,通常需要在块匹配之前,把参考帧与当前帧之间的运动考虑进来
根据相机模型,参考帧上的一个像素 PR 与真实的三维点世界坐标 PW 有以下关系:
在这里插入图片描述
类似的,对于当前帧,它亦有 PW 在它上边的投影,记作 PC
在这里插入图片描述
代入并消去 PW,即得两个图像之间的像素关系:
在这里插入图片描述
当知道 dR, PR 时,可以计算出 PC 的投影位置
此时,再给 PR 两个分量各一个增量 d d u, d d v,就可以求得 PC 的增量 d d uc, d d vc
通过这种方式,算出在局部范围内,参考帧和当前帧图像坐标变换的一个线性关系,构成仿射变换:

在这里插入图片描述
根据仿射变换矩阵,可以把当前帧(或参考帧)的像素进行变换后,再进行块匹配,以期获得对旋转更好的效果


6. 并行化:效率的问题

在实验当中也看到,稠密深度图的估计非常费时
这是因为要估计的点从原先的数百个特征点,一下子变成了几十万个像素点
即使现在主流的 CPU 无法实时地计算那样庞大的数量

不过,该问题亦有另一个性质:这几十万个像素点的深度估计是彼此无关的!
这使并行化有了用武之地

在程序中,在一个二重循环里遍历了所有像素,并逐个对它们进行极线搜索
当使用 CPU 时,这个过程是串行进行的:必须是上一个像素计算完毕后,再计算下一个像素

然而实际上,下一个像素完全没有必要等待上一个像素的计算结束
因为它们之间并没有明显的联系
所以可以用多个线程,分别计算每个像素,然后将结果统一起来
理论上,如果有 30 万个线程,那么该问题的计算时间和计算一个像素的时间是一样的

GPU 的并行计算架构非常适合这样的问题
因此,在单双和双目的稠密重建中,经常看到利用 GPU 进行并行加速的方式
根据一些类似的工作,利用 GPU 的稠密深度估计是可以在主流 GPU 上实时化的


7. 其他的改进

事实上,还能提出许多对本例程进行改进的方案,例如:

  1. 现在各像素完全是独立计算的,可能存在这个像素深度很小,边上一个又很大的情况
    可以假设深度图中相邻的深度变化不会太大,从而给深度估计加上了空间正则项
    这种做法会使得到的深度图更加平滑
  2. 没有显式地处理外点(Outlier)的情况
    事实上,由于遮挡、光照、运动模糊等各种因素的影响,不可能对每个像素都能保持成功的匹配
    而演示程序的做法中,只要 NCC 大于一定值,就认为出现了成功的匹配,没有考虑到错误匹配的情况
    处理错误匹配亦有若干种方式,例如均匀——高斯混合分布下的深度滤波器,显式地将内点与外点进行区别并进行概率建模,能够较好的处理外点数据,然而这种类型的滤波器理论较为复杂

从上面的讨论可以看出,存在着许多可能的改进方案
如果细致地改进每一步的做法,最后是有希望得到一个良好的稠密建图的方案的
然而,正如所讨论的,有一些问题存在理论上的困难,例如对环境纹理的依赖,例如像素梯度与极线方向的关联(以及平行的情况)
这些问题很难通过调整代码实现来解决
所以,直到目前为止,尽管双目和移动单目能够建立稠密的地图
通常认为它们过于依赖于环境纹理和光照,不够可靠


参考:

《视觉SLAM十四讲》


相关推荐:

视觉SLAM笔记(61) 单目稠密建图
视觉SLAM笔记(60) 建图
视觉SLAM笔记(59) 相似度计算
视觉SLAM笔记(58) 字典
视觉SLAM笔记(57) 回环检测


谢谢!

发布了217 篇原创文章 · 获赞 290 · 访问量 288万+

猜你喜欢

转载自blog.csdn.net/qq_32618327/article/details/103215672