LVI-SAM中激光点云辅助视觉特征点获取深度

LVI-SAM的VIS提取视觉特征点,得到特征点的归一化平面坐标(归一化平面位于相机光心前方Z=1处 )。接下来将视觉特征点的归一化平面坐标投影到单位球上,也就是对归一化平面坐标再进行一次归一化,使坐标向量模长为1,得到的单位向量仅代表特征点相对于相机光心的方向:

cpp 复制代码
pcl::PointCloud<PointType>::Ptr features_3d_sphere(new pcl::PointCloud<PointType>());
for (int i = 0; i < (int)features_2d.size(); ++i)
{
    // normalize 2d feature to a unit sphere
    Eigen::Vector3f feature_cur(features_2d[i].x, features_2d[i].y, features_2d[i].z); // z always equal to 1
    feature_cur.normalize();

    // carema --> lidar
    PointType p;
    p.x =  feature_cur(2);
    p.y = -feature_cur(0);
    p.z = -feature_cur(1);
    p.intensity = -1;     // intensity will be used to save depth
    features_3d_sphere->push_back(p);
}

代码中还将相机坐标系的点转换到雷达坐标系。因为通常相机的Z轴指向正前方,激光雷达的Z轴指向正上方,如图所示:

与此同时,获取缓存的5s内的地图中机器人附近的激光点云,并根据此时的相机在世界坐标系中的位姿将点云转换到相机坐标系。接下来构造激光点云的深度直方图对激光点云按照角度进行下采样。

具体而言,将相机方向正前方水平180度(-90度到+90度)、垂直180度(-90度到+90度)按照0.5度/格的分辨率划分为360x360的二维数组。然后遍历激光点云计算每个点的俯仰角(点与平面Z=0的夹角)与方位角(点与平面X=0的夹角),再用角度 除以分辨率 得到点在深度直方图中的二维索引

cpp 复制代码
for (int i = 0; i < (int)depth_cloud_local->size(); ++i)
{
    PointType p = depth_cloud_local->points[i];
    // filter points not in camera view
    if (p.x < 0 || abs(p.y / p.x) > 10 || abs(p.z / p.x) > 10)
        continue;
    
    // 行索引:俯仰角(上下方向)→ 映射到 0~num_bins
    float row_angle = atan2(p.z, sqrt(p.x * p.x + p.y * p.y)) * 180.0 / M_PI + 90.0; // degrees, bottom -> up, 0 -> 360
    int row_id = round(row_angle / bin_res);
    // 列索引:方位角(左右方向)→ 映射到 0~num_bins
    float col_angle = atan2(p.x, p.y) * 180.0 / M_PI; // degrees, left -> right, 0 -> 360
    int col_id = round(col_angle / bin_res);            
    // id may be out of boundary
    if (row_id < 0 || row_id >= num_bins || col_id < 0 || col_id >= num_bins)
        continue; // 超出索引范围

    // only keep points that's closer 仅仅保留最近的激光点深度
    float dist = pointDistance(p);
    if (dist < rangeImage.at<float>(row_id, col_id))
    {
        rangeImage.at<float>(row_id, col_id) = dist; // 深度
        pointsArray[row_id][col_id] = p;             // 激光点
    }
}

因为可能有多个点投影到同一个格子,代码中比较了投影到同一个格子的点的深度,仅保存较近点。通过这样的操作,将激光点云根据角度方向分布完成了下采样。

接下来,同样对下采样得到的激光点云坐标进行归一化,将保留下来的激光点投影到单位球上:

cpp 复制代码
// 提取深度直方图中过滤后的点云
pcl::PointCloud<PointType>::Ptr depth_cloud_local_filter2(new pcl::PointCloud<PointType>());
for (int i = 0; i < num_bins; ++i)
{
    for (int j = 0; j < num_bins; ++j)
    {
        if (rangeImage.at<float>(i, j) != FLT_MAX)
            depth_cloud_local_filter2->push_back(pointsArray[i][j]);
    }
}
*depth_cloud_local = *depth_cloud_local_filter2;

// 将保留下来的激光点投影到单位球上
pcl::PointCloud<PointType>::Ptr depth_cloud_unit_sphere(new pcl::PointCloud<PointType>());
for (int i = 0; i < (int)depth_cloud_local->size(); ++i)
{
    PointType p = depth_cloud_local->points[i];
    float range = pointDistance(p);
    p.x /= range;
    p.y /= range;
    p.z /= range;
    p.intensity = range;
    depth_cloud_unit_sphere->push_back(p);
}
if (depth_cloud_unit_sphere->size() < 10)
    return depth_of_point;

然后使用单位球面上的激光点云构造KDTree:

cpp 复制代码
pcl::KdTreeFLANN<PointType>::Ptr kdtree(new pcl::KdTreeFLANN<PointType>());
kdtree->setInputCloud(depth_cloud_unit_sphere);

代码遍历投影至单位球面上的视觉特征点,利用 KDTree 搜索出单位球面上距离当前视觉特征点最近的三个激光点。球面投影示意图:

由于在单位球面上,点间距离本质上反映了射线之间的方向差异 (即夹角大小),这一步有效剔除了深度值的影响,实现了纯粹基于视线方向的关联。在锁定方向最接近的三个激光点后,代码恢复它们的真实三维坐标以构建局部平面,最后通过计算视觉射线与该平面的交点,从而估计出视觉特征点的深度:

cpp 复制代码
// 在单位球上, 寻找与视觉特征点最近的三个激光点, 来估计视觉特征点的深度
vector<int> pointSearchInd;
vector<float> pointSearchSqDis;
// 设置匹配阈值(方向差异约束):将角度分辨率转换为单位球上的弧长 * 5,再求平方
// 效果:只有角度差异小于~2.5°(0.5°×5)的激光点才会被视为有效匹配。
float dist_sq_threshold = pow(sin(bin_res / 180.0 * M_PI) * 5.0, 2);
for (int i = 0; i < (int)features_3d_sphere->size(); ++i)
{
    kdtree->nearestKSearch(features_3d_sphere->points[i], 3, pointSearchInd, pointSearchSqDis);
    // 有效匹配判断:成功找到 3 个激光点,且最远的那个激光点的方向差异仍在阈值内(3 个点都在同一方向附近,确保平面拟合有效)。
    if (pointSearchInd.size() == 3 && pointSearchSqDis[2] < dist_sq_threshold)
    {
        // 单位球上最近的三个激光点 A B C (实际坐标)
        float r1 = depth_cloud_unit_sphere->points[pointSearchInd[0]].intensity; // A的深度
        Eigen::Vector3f A(depth_cloud_unit_sphere->points[pointSearchInd[0]].x * r1,
                            depth_cloud_unit_sphere->points[pointSearchInd[0]].y * r1,
                            depth_cloud_unit_sphere->points[pointSearchInd[0]].z * r1);

        float r2 = depth_cloud_unit_sphere->points[pointSearchInd[1]].intensity; 
        Eigen::Vector3f B(depth_cloud_unit_sphere->points[pointSearchInd[1]].x * r2,
                            depth_cloud_unit_sphere->points[pointSearchInd[1]].y * r2,
                            depth_cloud_unit_sphere->points[pointSearchInd[1]].z * r2);

        float r3 = depth_cloud_unit_sphere->points[pointSearchInd[2]].intensity;
        Eigen::Vector3f C(depth_cloud_unit_sphere->points[pointSearchInd[2]].x * r3,
                            depth_cloud_unit_sphere->points[pointSearchInd[2]].y * r3,
                            depth_cloud_unit_sphere->points[pointSearchInd[2]].z * r3);

        // 视觉特征点在单位球上的坐标 V
        // https://math.stackexchange.com/questions/100439/determine-where-a-vector-will-intersect-a-plane
        Eigen::Vector3f V(features_3d_sphere->points[i].x,
                            features_3d_sphere->points[i].y,
                            features_3d_sphere->points[i].z);

        // 估计视觉特征点的深度 s
        // 向量 BA X CB,得到垂直于平面的法向量,其模长与平面内两个向量构成的平行四边形面积成正比。
        Eigen::Vector3f N = (A - B).cross(B - C);
        // NOTE:求深度s

        float s = (N(0) * A(0) + N(1) * A(1) + N(2) * A(2))  // (BA X CB) * OA
                / (N(0) * V(0) + N(1) * V(1) + N(2) * V(2)); // (BA X CB) * OV

        float min_depth = min(r1, min(r2, r3));
        float max_depth = max(r1, max(r2, r3));
        if (max_depth - min_depth > 2 || s <= 0.5)
        {
            continue;
        } else if (s - max_depth > 0) {
            s = max_depth;
        } else if (s - min_depth < 0) {
            s = min_depth;
        }
        // convert feature into cartesian space if depth is available
        features_3d_sphere->points[i].x *= s;
        features_3d_sphere->points[i].y *= s;
        features_3d_sphere->points[i].z *= s;
        // the obtained depth here is for unit sphere, VINS estimator needs depth for normalized feature (by value z), (lidar x = camera z)
        // 存储最终深度(激光系x轴对应相机前向,深度即x)
        // 因为激光点云转换到相机系后,前向点的 x 轴坐标(激光系)≈ 相机系的 z 轴坐标(光轴方向);
        // 视觉中所谓的"深度"就是点到相机光心的距离,即Z轴距离
        features_3d_sphere->points[i].intensity = features_3d_sphere->points[i].x; // ???
    }
}

代码中恢复视觉特征点的深度s的过程如下:

A、B、C为方向离视觉特征点P最近的三个激光点,三点可以拟合一个平面ABC。求射线OP与平面ABC的交点P,这个交点P到相机原点O的距离就是深度s,即。已知的单位向量为,则。然后:

则平面的法向量

且线段AP在平面ABC上,则垂直于,即:

则:

公式推导过程与代码一致。

相关推荐
行智科技18 天前
FAST-LIVO2 源码精读(二):环境搭建与编译避坑
算法·ubuntu·自动驾驶·slam
大江东去浪淘尽千古风流人物20 天前
【PromptStereo】零样本立体匹配新范式:用结构与运动Prompt驱动迭代优化(CVPR 2026)
深度学习·3d·slam·视觉定位·dust3r·3d重建·mast3r
吾名招财20 天前
开源可SLAM的3D扫描仪硬件方案(成本低至6000元)
slam·3d扫描仪·mid360
暂未成功人士!21 天前
简单了解李群和李代数的相关概念以及典型应用
人工智能·机器人·slam·姿态·李群李代数
MIXLLRED23 天前
Ubuntu 22.04 + ROS2 Humble 上部署 ScaRF‑SLAM指南
ubuntu·slam·ros2·离线建图
大江东去浪淘尽千古风流人物24 天前
【VGGT-Ω】前馈式3D重建的规模化之路:Register Attention、自监督训练与10B参数Scaling Law深度解析
深度学习·计算机视觉·transformer·slam·vio·3d重建
大江东去浪淘尽千古风流人物24 天前
【VGGT】统一3D重建:单网络同时预测相机位姿、深度图、点云与3D轨迹的前馈Transformer架构深度解析
网络·数码相机·3d·transformer·slam·3d重建·cvpr2025
kobesdu1 个月前
【ROS2实战笔记-24】ROS2 Launch 实用技巧:条件逻辑与节点动态生成
笔记·ros·slam
大江东去浪淘尽千古风流人物1 个月前
【RADIO-ViPE】动态环境下的在线开放词汇语义SLAM:视觉-语言-几何紧耦合BA与自适应鲁棒核深度解析
slam·语义slam·vio·开放词汇·动态场景
大江东去浪淘尽千古风流人物1 个月前
【KV-Tracker】Transformer 实时位姿跟踪:KV-Cache 加速多视图几何网络达 27FPS
网络·深度学习·transformer·slam·位姿估计·kv-cache