Advertisement

视觉SLAM笔记(63) RGB-D 稠密建图

阅读量:

视觉SLAM中,通过RGB-D相机进行稠密建图,主要步骤包括:
点云生成:利用RGB-D相机的彩色图和深度图,结合相机内外参,将图像转换为三维点云。每帧图像中,去除深度值无效或过大的点,得到单帧点云。
滤波处理:

  • 统计外点去除:通过统计滤波器去除孤立噪声点,保留“粘在一起”的点,提高点云质量。
  • 体素滤波:使用VoxelGrid滤波器对点云进行降采样,减少存储空间,提高效率。
    点云地图:生成的点云地图作为基础数据,虽然在定位和导航方面存在局限(如缺乏物体表面信息),但在调试和可视化方面具有基础作用。
    该方法通过高效处理RGB-D图像,生成高质量的点云地图,为后续的SLAM应用提供基础数据。

视觉SLAM笔记(63) RGB-D 稠密建图

  • 1. 建立点云地图
  • 2. 点云地图
  • 3. 其他重建方法

1. 建立点云地图

点云通常被定义为由离散点组成的地图表示。每个点的基本属性包括 x、y、z 三维坐标,同时可附加 r、g、b 颜色信息。通过 RGB-D 相机,可以同时获取彩色图像和深度数据,从而方便地构建 RGB-D 点云。

通过某种手段,相机的位姿得以确定。点云的直接叠加即可生成全局点云。该资源详细描述了通过相机内外参进行点云拼接的过程。不过,在实际应用中,会对点云进行滤波处理,以提升视觉效果。

在本程序中,主要采用两种滤波器进行处理:外点剔除滤波器和降采样滤波器。由于部分代码与上一版本相同,重点在于分析修改的部分。

复制代码
    int main(int argc, char** argv)
    {
    vector<cv::Mat> colorImgs, depthImgs;    // 彩色图和深度图
    vector<Eigen::Isometry3d> poses;         // 相机位姿
    
    ifstream fin("../data/pose.txt");
    if (!fin)
    {
        cerr << "cannot find pose file" << endl;
        return 1;
    }
    
    for (int i = 0; i < 5; i++)
    {
        boost::format fmt("../data/%s/%d.%s"); //图像文件格式
        colorImgs.push_back(cv::imread((fmt%"color" % (i + 1) % "png").str()));
        depthImgs.push_back(cv::imread((fmt%"depth" % (i + 1) % "pgm").str(), -1)); // 使用-1读取原始图像
    
        double data[7] = { 0 };
        for (int i = 0; i < 7; i++)
        {
            fin >> data[i];
        }
        Eigen::Quaterniond q(data[6], data[3], data[4], data[5]);
        Eigen::Isometry3d T(q);
        T.pretranslate(Eigen::Vector3d(data[0], data[1], data[2]));
        poses.push_back(T);
    }
    
    // 计算点云并拼接
    // 相机内参 
    double cx = 325.5;
    double cy = 253.5;
    double fx = 518.0;
    double fy = 519.0;
    double depthScale = 1000.0;
    
    cout << "正在将图像转换为点云..." << endl;
    
    // 定义点云使用的格式:这里用的是XYZRGB
    typedef pcl::PointXYZRGB PointT;
    typedef pcl::PointCloud<PointT> PointCloud;
    
    // 新建一个点云
    PointCloud::Ptr pointCloud(new PointCloud);
    for (int i = 0; i < 5; i++)
    {
        PointCloud::Ptr current(new PointCloud);
        cout << "转换图像中: " << i + 1 << endl;
        cv::Mat color = colorImgs[i];
        cv::Mat depth = depthImgs[i];
        Eigen::Isometry3d T = poses[i];
        for (int v = 0; v < color.rows; v++)
            for (int u = 0; u < color.cols; u++)
            {
                unsigned int d = depth.ptr<unsigned short>(v)[u]; // 深度值
                if (d == 0) continue; // 为0表示没有测量到
                if (d >= 7000) continue; // 深度太大时不稳定,去掉
                Eigen::Vector3d point;
                point[2] = double(d) / depthScale;
                point[0] = (u - cx)*point[2] / fx;
                point[1] = (v - cy)*point[2] / fy;
                Eigen::Vector3d pointWorld = T * point;
    
                PointT p;
                p.x = pointWorld[0];
                p.y = pointWorld[1];
                p.z = pointWorld[2];
                p.b = color.data[v*color.step + u * color.channels()];
                p.g = color.data[v*color.step + u * color.channels() + 1];
                p.r = color.data[v*color.step + u * color.channels() + 2];
                current->points.push_back(p);
            }
        // 利用统计滤波器方法去除孤立点
        PointCloud::Ptr tmp(new PointCloud);
        pcl::StatisticalOutlierRemoval<PointT> statistical_filter;
        statistical_filter.setMeanK(50);
        statistical_filter.setStddevMulThresh(1.0);
        statistical_filter.setInputCloud(current);
        statistical_filter.filter(*tmp);
        (*pointCloud) += *tmp;
    }
    
    pointCloud->is_dense = false;
    cout << "点云共有" << pointCloud->size() << "个点." << endl;
    
    // 体素滤波器
    pcl::VoxelGrid<PointT> voxel_filter;
    voxel_filter.setLeafSize(0.01, 0.01, 0.01);       // resolution 
    PointCloud::Ptr tmp(new PointCloud);
    voxel_filter.setInputCloud(pointCloud);
    voxel_filter.filter(*tmp);
    tmp->swap(*pointCloud);
    
    cout << "滤波之后,点云共有" << pointCloud->size() << "个点." << endl;
    
    pcl::io::savePCDFileBinary("map.pcd", *pointCloud);
    return 0;
    }

思路没有太大变化,主要不同之处在于:

在生成每帧点云时,去除深度值过大或无效的点。这主要归因于Kinect的有效测量范围,超出量程后的深度值会产生显著的误差。

  1. 通过统计滤波器方法去除孤立点
    该滤波器统计每个点与最近 N 个点的距离分布,随后去除距离均值超出范围的点。
    这样处理后,保留了那些彼此靠近的点,剔除了孤立的噪声点。

最后,采用体素滤波器(Voxel Filter)进行降采样。在存在多个视角且存在视野重叠的情况下,重叠区域内的大量位置相近的点会导致内存空间的过度占用。体素滤波器通过确保在特定大小的立方体(体素)内仅保留一个点来实现降采样。这不仅实现了对三维空间的降采样,还有效降低了存储空间的需求。

运行程序

复制代码
    $ ./pointcloud_mapping
在这里插入图片描述

最后,把生成的点云以 pcd 格式存储在 map.pcd

用 PCL 提供的可视化程序打开这个文件:

复制代码
    $ pcl_viewer map.pcd
在这里插入图片描述

可能需要放大才能看清,或者自己尝试一下

在这里插入图片描述

左侧为视觉SLAM笔记(25) 拼接点云生成的点云地图,右侧为经过滤波处理后的点云地图。

在滤波前,白色框区域中存在许多孤立的点,这些点由噪声引起。通过外点去除统计,成功消除了这些噪声,从而让整个地图更加干净。在体素滤波器设置中,将分辨率设定为 0.01,意味着每立方厘米仅存一个点。这一分辨率水平已经相当高,因此在截图中几乎察觉不到地图的变化。然而,在程序输出中,点数显著减少,从最初的 90 万个点降至 44 万个点,减少了一半左右。

点云地图提供了基础的可视化地图,大致呈现环境的轮廓。它采用三维形式存储,使得用户能够快速浏览场景的各个角落,并实现自由漫游。点云系统的一大显著优势是可以直接由RGB-D图像高效生成,无需额外处理。其滤波处理也相当直观,且处理效率尚可接受。


2. 点云地图

不过,目前点云技术在地图中的应用仍处于初级阶段。

定位需求基于特征点的视觉里程计处理方式,该方式存在不足,无法实现基于特征点的定位方法。具体而言,如果采用基于特征点的视觉里程计,由于点云数据中不存储特征点信息,无法实现基于特征点的定位方法。另一方面,如果前端采用点云的ICP算法,可以考虑将局部点云与全局点云进行ICP配准以估计位姿。然而,这种处理方式存在不足,即要求全局点云具有较高的精度。此外,在这种处理点云的方式下,未对点云数据进行优化处理,因此无法满足精度要求。

导航与避障的需求无法直接由点云实现。点云无法直接表示障碍物的存在,也无法在点云中查询任意空间点的状态。这正是导航和避障所需的基础条件。不过,通过点云数据的处理与优化,可以构建出更适合导航与避障使用的地图表示形式。

  1. 可视化和交互:具有基本的可视化与交互能力
    能够看到场景的外观,也能在场景里漫游
    从可视化角度来说,由于点云只含有离散的点,而没有物体表面信息(例如法线)
    所以不太符合人们对可视化习惯
    例如,点云地图的物体从正面看和背面看是一样的,而且还能透过物体看到它背后的东西
    这些都不符合日常的经验,因为没有物体表面的信息

总体而言,点云地图被定义为“基础型的”或“初级阶段的”,这表明它更接近传感器采集的原始数据。该系统通常包含一些基础功能,主要用于调试和基础展示,直接应用于应用程序前通常需要额外配置。


3. 其他重建方法

如果希望地图具备更高级的功能,点云地图则可作为提升地图功能的起点。例如,就导航功能而言,从点云数据出发构建占据式网格地图(Occupancy Grid)是一种有效的方法,该方法可为导航算法查询某点是否可以通过。再如,SfM(结构从运动)中常用的泊松重建方法,便能通过基础的点云重建物体的占据式网格地图,从而获得物体的表面信息。除了泊松重建方法外,Surfel作为一种表示物体表面的方法,以面元作为地图的基本单元,同样能够构建出令人满意的可视化地图。

在这里插入图片描述

展示了泊松重建和 surfel 的一个典型实例。可以看作它们的视觉效果明显优于纯粹点云建模,同时,它们都可以通过点云进行构建。许多由点云转换而来的地图形式均包含在 PCL 库中。


参考:

《视觉SLAM十四讲》


相关推荐:

视觉SLAM笔记(62) 单目稠密估计
视觉SLAM笔记(61) 单目稠密图构建
视觉SLAM笔记(60) 图构建
视觉SLAM笔记(59) 相似度计算优化
视觉SLAM笔记(58) 词汇表


谢谢!

全部评论 (0)

还没有任何评论哟~