LIO-SAM代码解析:imageProjection.cpp


1. cloudHandler

cachePointCloud 函数用于缓存点云数据并对其进行格式转换和检查,以确保点云数据符合后续处理的要求。主要功能包括:

  1. 缓存接收到的点云消息,保证点云处理的连续性。
  2. 根据不同传感器类型转换点云格式。
  3. 提取点云时间戳信息并检查点云的密集性。
  4. 验证点云是否包含环号(ring channel)和时间戳字段,为去畸变(deskew)和其他算法提供基础支持。
// 函数:cachePointCloud
// 功能:缓存点云数据,转换格式并检查有效性
bool cachePointCloud(const sensor_msgs::PointCloud2ConstPtr& laserCloudMsg)
{
    // 将接收到的点云数据存入队列
    cloudQueue.push_back(*laserCloudMsg);
    if (cloudQueue.size() <= 2)
        return false;

    // 从队列中取出当前点云消息
    currentCloudMsg = std::move(cloudQueue.front());
    cloudQueue.pop_front();

    // 根据传感器类型转换点云格式
    if (sensor == SensorType::VELODYNE || sensor == SensorType::LIVOX)
    {
        pcl::moveFromROSMsg(currentCloudMsg, *laserCloudIn);
    }
    else if (sensor == SensorType::OUSTER)
    {
        // 转换为Velodyne格式
        pcl::moveFromROSMsg(currentCloudMsg, *tmpOusterCloudIn);
        laserCloudIn->points.resize(tmpOusterCloudIn->size());
        laserCloudIn->is_dense = tmpOusterCloudIn->is_dense;
        for (size_t i = 0; i < tmpOusterCloudIn->size(); i++)
        {
            auto &src = tmpOusterCloudIn->points[i];
            auto &dst = laserCloudIn->points[i];
            dst.x = src.x;
            dst.y = src.y;
            dst.z = src.z;
            dst.intensity = src.intensity;
            dst.ring = src.ring;
            dst.time = src.t * 1e-9f;
        }
    }
    else
    {
        ROS_ERROR_STREAM("Unknown sensor type: " << int(sensor));
        ros::shutdown();
    }

    // 提取时间戳信息
    cloudHeader = currentCloudMsg.header;
    timeScanCur = cloudHeader.stamp.toSec();
    timeScanEnd = timeScanCur + laserCloudIn->points.back().time;

    // 检查点云是否为密集格式
    if (laserCloudIn->is_dense == false)
    {
        ROS_ERROR("Point cloud is not in dense format, please remove NaN points first!");
        ros::shutdown();
    }

    // 检查点云是否包含环号(ring channel)
    static int ringFlag = 0;
    if (ringFlag == 0)
    {
        ringFlag = -1;
        for (int i = 0; i < (int)currentCloudMsg.fields.size(); ++i)
        {
            if (currentCloudMsg.fields[i].name == "ring")
            {
                ringFlag = 1;
                break;
            }
        }
        if (ringFlag == -1)
        {
            ROS_ERROR("Point cloud ring channel not available, please configure your point cloud data!");
            ros::shutdown();
        }
    }

    // 检查点云是否包含时间戳字段
    if (deskewFlag == 0)
    {
        deskewFlag = -1;
        for (auto &field : currentCloudMsg.fields)
        {
            if (field.name == "time" || field.name == "t")
            {
                deskewFlag = 1;
                break;
            }
        }
        if (deskewFlag == -1)
            ROS_WARN("Point cloud timestamp not available, deskew function disabled, system will drift significantly!");
    }

    return true;
}

2. deskewInfo

cloudHandler代码实现了点云去畸变(deskew)的信息准备过程,包括 IMU 数据和里程计(odometry)数据的处理。其核心功能包括:

  1. IMU 数据处理 (imuDeskewInfo)
    • 校准并缓存扫描时间内的 IMU 数据。
    • 通过角速度积分,计算每个时间点的姿态变化(滚转、俯仰、偏航)。
    • 初始化扫描开始时的姿态信息。
  2. 里程计数据处理 (odomDeskewInfo)
    • 校准并缓存扫描时间内的里程计数据。
    • 提取扫描开始和结束的位姿信息。
    • 计算两时刻之间的位姿增量(位移和欧拉角增量)。
bool deskewInfo()
{
    // 使用锁来确保对 imuQueue 和 odoQueue 的访问是线程安全的
    std::lock_guard<std::mutex> lock1(imuLock);
    std::lock_guard<std::mutex> lock2(odoLock);

    // 检查 IMU 数据是否可用
    // 确保 IMU 数据队列不为空,并且数据的时间戳在扫描时间范围内
    if (imuQueue.empty() || imuQueue.front().header.stamp.toSec() > timeScanCur || imuQueue.back().header.stamp.toSec() < timeScanEnd)
    {
        ROS_DEBUG("Waiting for IMU data ...");
        return false; // 如果 IMU 数据不可用,返回 false
    }

    // 处理 IMU 数据
    imuDeskewInfo();
    // 处理里程计数据
    odomDeskewInfo();

    return true; // 如果数据处理成功,返回 true
}
void imuDeskewInfo()
{
    // 初始化标志,表示 IMU 数据是否可用
    cloudInfo.imuAvailable = false;

    // 删除过早的 IMU 数据
    // 保留时间戳在扫描时间范围内的 IMU 数据
    while (!imuQueue.empty())
    {
        if (imuQueue.front().header.stamp.toSec() < timeScanCur - 0.01)
            imuQueue.pop_front();
        else
            break;
    }

    // 如果 IMU 数据队列为空,直接返回
    if (imuQueue.empty()) return;

    // 初始化指针,用于遍历 IMU 数据
    imuPointerCur = 0;

    // 遍历 IMU 数据
    for (int i = 0; i < (int)imuQueue.size(); ++i)
    {
        sensor_msgs::Imu thisImuMsg = imuQueue[i];
        double currentImuTime = thisImuMsg.header.stamp.toSec();

        // 初始化扫描起始姿态
        if (currentImuTime <= timeScanCur)
            imuRPY2rosRPY(&thisImuMsg, &cloudInfo.imuRollInit, &cloudInfo.imuPitchInit, &cloudInfo.imuYawInit);

        // 如果当前 IMU 数据的时间戳超过扫描结束时间,停止处理
        if (currentImuTime > timeScanEnd + 0.01)
            break;

        // 处理第一个 IMU 数据
        if (imuPointerCur == 0)
        {
            imuRotX[0] = imuRotY[0] = imuRotZ[0] = 0;
            imuTime[0] = currentImuTime;
            ++imuPointerCur;
            continue;
        }

        // 获取角速度并积分计算姿态变化
        double angular_x, angular_y, angular_z;
        imuAngular2rosAngular(&thisImuMsg, &angular_x, &angular_y, &angular_z);
        double timeDiff = currentImuTime - imuTime[imuPointerCur - 1];
        imuRotX[imuPointerCur] = imuRotX[imuPointerCur - 1] + angular_x * timeDiff;
        imuRotY[imuPointerCur] = imuRotY[imuPointerCur - 1] + angular_y * timeDiff;
        imuRotZ[imuPointerCur] = imuRotZ[imuPointerCur - 1] + angular_z * timeDiff;
        imuTime[imuPointerCur] = currentImuTime;
        ++imuPointerCur;
    }

    // 修正指针值
    --imuPointerCur;

    // 如果处理了有效的 IMU 数据,设置标志为 true
    if (imuPointerCur > 0)
        cloudInfo.imuAvailable = true;
}
  1. IMU 数据积分计算
    根据角速度积分计算姿态变化:
    θ x ( t ) = θ x ( t 0 ) + ∫ t 0 t ω x ( τ )   d τ \theta_x(t) = \theta_x(t_0) + \int_{t_0}^{t} \omega_x(\tau) \, d\tau θx(t)=θx(t0)+t0tωx(τ)dτ
    θ y ( t ) = θ y ( t 0 ) + ∫ t 0 t ω y ( τ )   d τ \theta_y(t) = \theta_y(t_0) + \int_{t_0}^{t} \omega_y(\tau) \, d\tau θy(t)=θy(t0)+t0tωy(τ)dτ
    θ z ( t ) = θ z ( t 0 ) + ∫ t 0 t ω z ( τ )   d τ \theta_z(t) = \theta_z(t_0) + \int_{t_0}^{t} \omega_z(\tau) \, d\tau θz(t)=θz(t0)+t0tωz(τ)dτ
        // 获取角速度并积分计算姿态变化
        double angular_x, angular_y, angular_z;
        imuAngular2rosAngular(&thisImuMsg, &angular_x, &angular_y, &angular_z);
        double timeDiff = currentImuTime - imuTime[imuPointerCur - 1];
        imuRotX[imuPointerCur] = imuRotX[imuPointerCur - 1] + angular_x * timeDiff;
        imuRotY[imuPointerCur] = imuRotY[imuPointerCur - 1] + angular_y * timeDiff;
        imuRotZ[imuPointerCur] = imuRotZ[imuPointerCur - 1] + angular_z * timeDiff;
        imuTime[imuPointerCur] = currentImuTime;
  1. 里程计位姿增量计算
void odomDeskewInfo()
{
    // 初始化标志,表示里程计数据是否可用
    cloudInfo.odomAvailable = false;

    // 删除过早的里程计数据
    // 保留时间戳在扫描时间范围内的里程计数据
    while (!odomQueue.empty())
    {
        if (odomQueue.front().header.stamp.toSec() < timeScanCur - 0.01)
            odomQueue.pop_front();
        else
            break;
    }

    // 如果里程计数据队列为空,直接返回
    if (odomQueue.empty()) return;

    // 如果第一个里程计数据的时间戳超过扫描起始时间,直接返回
    if (odomQueue.front().header.stamp.toSec() > timeScanCur)
        return;

    // 获取扫描开始时的位姿
    nav_msgs::Odometry startOdomMsg;
    for (int i = 0; i < (int)odomQueue.size(); ++i)
    {
        startOdomMsg = odomQueue[i];
        if (ROS_TIME(&startOdomMsg) < timeScanCur)
            continue;
        else
            break;
    }

    // 将四元数转换为欧拉角
    tf::Quaternion orientation;
    tf::quaternionMsgToTF(startOdomMsg.pose.pose.orientation, orientation);
    double roll, pitch, yaw;
    tf::Matrix3x3(orientation).getRPY(roll, pitch, yaw);

    // 记录扫描开始时的位姿
    cloudInfo.initialGuessX = startOdomMsg.pose.pose.position.x;
    cloudInfo.initialGuessY = startOdomMsg.pose.pose.position.y;
    cloudInfo.initialGuessZ = startOdomMsg.pose.pose.position.z;
    cloudInfo.initialGuessRoll = roll;
    cloudInfo.initialGuessPitch = pitch;
    cloudInfo.initialGuessYaw = yaw;

    // 设置里程计数据可用标志
    cloudInfo.odomAvailable = true;

    // 获取扫描结束时的位姿
    if (odomQueue.back().header.stamp.toSec() < timeScanEnd)
        return;

    nav_msgs::Odometry endOdomMsg;
    for (int i = 0; i < (int)odomQueue.size(); ++i)
    {
        endOdomMsg = odomQueue[i];
        if (ROS_TIME(&endOdomMsg) < timeScanEnd)
            continue;
        else
            break;
    }

    // 检查位姿协方差是否一致
    if (int(round(startOdomMsg.pose.covariance[0])) != int(round(endOdomMsg.pose.covariance[0])))
        return;

    // 计算位姿增量
    Eigen::Affine3f transBegin = pcl::getTransformation(startOdomMsg.pose.pose.position.x, startOdomMsg.pose.pose.position.y, startOdomMsg.pose.pose.position.z, roll, pitch, yaw);
    tf::quaternionMsgToTF(endOdomMsg.pose.pose.orientation, orientation);
    tf::Matrix3x3(orientation).getRPY(roll, pitch, yaw);
    Eigen::Affine3f transEnd = pcl::getTransformation(endOdomMsg.pose.pose.position.x, endOdomMsg.pose.pose.position.y, endOdomMsg.pose.pose.position.z, roll, pitch, yaw);

    Eigen::Affine3f transBt = transBegin.inverse() * transEnd;
    pcl::getTranslationAndEulerAngles(transBt, odomIncreX, odomIncreY, odomIncreZ, rollIncre, pitchIncre, yawIncre);

    // 设置位姿增量计算完成标志
    odomDeskewFlag = true;
}

位姿增量计算
起始和结束位姿变换矩阵分别为 T begin T_{\text{begin}} Tbegin T end T_{\text{end}} Tend,位姿增量为:
T bt = T begin − 1 ⋅ T end T_{\text{bt}} = T_{\text{begin}}^{-1} \cdot T_{\text{end}} Tbt=Tbegin1Tend
Δ x , Δ y , Δ z , Δ θ x , Δ θ y , Δ θ z = translation_and_euler ( T bt ) \Delta x, \Delta y, \Delta z, \Delta \theta_x, \Delta \theta_y, \Delta \theta_z = \text{translation\_and\_euler}(T_{\text{bt}}) Δx,Δy,Δz,Δθx,Δθy,Δθz=translation_and_euler(Tbt)

    // 计算位姿增量
    Eigen::Affine3f transBegin = pcl::getTransformation(startOdomMsg.pose.pose.position.x, startOdomMsg.pose.pose.position.y, startOdomMsg.pose.pose.position.z, roll, pitch, yaw);
    tf::quaternionMsgToTF(endOdomMsg.pose.pose.orientation, orientation);
    tf::Matrix3x3(orientation).getRPY(roll, pitch, yaw);
    Eigen::Affine3f transEnd = pcl::getTransformation(endOdomMsg.pose.pose.position.x, endOdomMsg.pose.pose.position.y, endOdomMsg.pose.pose.position.z, roll, pitch, yaw);

    Eigen::Affine3f transBt = transBegin.inverse() * transEnd;

四元数到欧拉角转换
利用四元数 q = ( x , y , z , w ) q = (x, y, z, w) q=(x,y,z,w),将其转换为欧拉角 ( ϕ , θ , ψ ) (\phi, \theta, \psi) (ϕ,θ,ψ)
ϕ = arctan ⁡ ( 2 ( q w q x + q y q z ) 1 − 2 ( q x 2 + q y 2 ) ) \phi = \arctan\left(\frac{2(q_w q_x + q_y q_z)}{1 - 2(q_x^2 + q_y^2)}\right) ϕ=arctan(12(qx2+qy2)2(qwqx+qyqz))
θ = arcsin ⁡ ( 2 ( q w q y − q z q x ) ) \theta = \arcsin(2(q_w q_y - q_z q_x)) θ=arcsin(2(qwqyqzqx))
ψ = arctan ⁡ ( 2 ( q w q z + q x q y ) 1 − 2 ( q y 2 + q z 2 ) ) \psi = \arctan\left(\frac{2(q_w q_z + q_x q_y)}{1 - 2(q_y^2 + q_z^2)}\right) ψ=arctan(12(qy2+qz2)2(qwqz+qxqy))

  pcl::getTranslationAndEulerAngles(transBt, odomIncreX, odomIncreY, odomIncreZ, rollIncre, pitchIncre, yawIncre);

3. projectPointCloud

projectPointCloud 函数将输入的点云数据投影到二维范围图(range image)中,用于后续点云处理。其主要功能包括:

  1. 点云过滤:剔除范围超出传感器有效范围的点以及非目标扫描线的点。
  2. 角度计算:根据传感器类型计算水平角度或列索引。
  3. 范围图构建:将点云按照行列索引投影到范围图矩阵,同时处理点去畸变(deskew)。
void projectPointCloud()
{
    // 获取输入点云的大小
    int cloudSize = laserCloudIn->points.size();

    // 遍历每个点
    for (int i = 0; i < cloudSize; ++i)
    {
        // 创建一个点类型变量,用于存储当前点的信息
        PointType thisPoint;
        thisPoint.x = laserCloudIn->points[i].x;
        thisPoint.y = laserCloudIn->points[i].y;
        thisPoint.z = laserCloudIn->points[i].z;
        thisPoint.intensity = laserCloudIn->points[i].intensity;

        // 计算点到激光雷达的距离
        float range = pointDistance(thisPoint);
        // 如果距离不在有效范围内,跳过该点
        if (range < lidarMinRange || range > lidarMaxRange)
            continue;

        // 获取当前点的扫描行号
        int rowIdn = laserCloudIn->points[i].ring;
        // 如果行号不在有效范围内,跳过该点
        if (rowIdn < 0 || rowIdn >= N_SCAN)
            continue;

        // 如果行号不符合下采样率,跳过该点
        if (rowIdn % downsampleRate != 0)
            continue;

        // 初始化列号
        int columnIdn = -1;

        // 根据激光雷达类型计算列号
        if (sensor == SensorType::VELODYNE || sensor == SensorType::OUSTER)
        {
            // 计算水平角度
            float horizonAngle = atan2(thisPoint.x, thisPoint.y) * 180 / M_PI;
            // 计算列号
            static float ang_res_x = 360.0 / float(Horizon_SCAN);
            columnIdn = -round((horizonAngle - 90.0) / ang_res_x) + Horizon_SCAN / 2;
            // 修正列号,确保其在有效范围内
            if (columnIdn >= Horizon_SCAN)
                columnIdn -= Horizon_SCAN;
        }
        else if (sensor == SensorType::LIVOX)
        {
            // 对于 Livox 激光雷达,使用预先计算的列号
            columnIdn = columnIdnCountVec[rowIdn];
            columnIdnCountVec[rowIdn] += 1;
        }

        // 如果列号不在有效范围内,跳过该点
        if (columnIdn < 0 || columnIdn >= Horizon_SCAN)
            continue;

        // 如果该位置已经有有效距离值,跳过该点
        if (rangeMat.at<float>(rowIdn, columnIdn) != FLT_MAX)
            continue;

        // 对当前点进行去畸变处理
        thisPoint = deskewPoint(&thisPoint, laserCloudIn->points[i].time);

        // 更新范围图像中的距离值
        rangeMat.at<float>(rowIdn, columnIdn) = range;

        // 计算点在全点云中的索引
        int index = columnIdn + rowIdn * Horizon_SCAN;
        // 更新全点云中的点
        fullCloud->points[index] = thisPoint;
    }
}
  1. 点的距离计算
    P = ( x , y , z ) P = (x, y, z) P=(x,y,z) 的距离(range)为:
    r = x 2 + y 2 + z 2 r = \sqrt{x^2 + y^2 + z^2} r=x2+y2+z2
        // 创建一个点类型变量,用于存储当前点的信息
        PointType thisPoint;
        thisPoint.x = laserCloudIn->points[i].x;
        thisPoint.y = laserCloudIn->points[i].y;
        thisPoint.z = laserCloudIn->points[i].z;
        thisPoint.intensity = laserCloudIn->points[i].intensity;

        // 计算点到激光雷达的距离
        float range = pointDistance(thisPoint);
  1. 水平角度计算(Velodyne/Ouster)
    对于点 P = ( x , y , z ) P = (x, y, z) P=(x,y,z),水平角度为:
    θ = arctan ⁡ ( x y ) ⋅ 180 π \theta = \arctan\left(\frac{x}{y}\right) \cdot \frac{180}{\pi} θ=arctan(yx)π180
    列索引为:
    col = − ⌈ θ − 90 ang_res_x ⌉ + Horizon_SCAN 2 \text{col} = -\left\lceil\frac{\theta - 90}{\text{ang\_res\_x}}\right\rceil + \frac{\text{Horizon\_SCAN}}{2} col=ang_res_xθ90+2Horizon_SCAN
            // 计算水平角度
            float horizonAngle = atan2(thisPoint.x, thisPoint.y) * 180 / M_PI;
            // 计算列号
            static float ang_res_x = 360.0 / float(Horizon_SCAN);
            columnIdn = -round((horizonAngle - 90.0) / ang_res_x) + Horizon_SCAN / 2;
  1. 投影矩阵更新
    对于点 ( r , θ ) (r, \theta) (r,θ),如果矩阵位置 ( row , col ) (\text{row}, \text{col}) (row,col) 为空,则更新为:
    rangeMat [ row , col ] = r \text{rangeMat}[\text{row}, \text{col}] = r rangeMat[row,col]=r
        // 更新范围图像中的距离值
        rangeMat.at<float>(rowIdn, columnIdn) = range;

        // 计算点在全点云中的索引
        int index = columnIdn + rowIdn * Horizon_SCAN;
        // 更新全点云中的点
        fullCloud->points[index] = thisPoint;

4. cloudExtraction

cloudExtraction 函数从范围图中提取有效点云数据,并存储在 extractedCloud 中。提取的点云数据用于激光里程计计算。主要功能包括:

  1. 记录起止索引:为每条激光扫描线记录点云在提取后的点云中的起始和结束索引。
  2. 点云提取:遍历范围图,将有效点(非 FLT_MAX)提取到新的点云中。
  3. 信息存储:保存点的列索引和距离信息,用于后续处理。
void cloudExtraction()
{
    int count = 0; // 初始化计数器,用于记录提取的点云大小

    // 提取分段点云用于激光雷达里程计
    for (int i = 0; i < N_SCAN; ++i)
    {
        // 记录当前扫描行的起始索引
        cloudInfo.startRingIndex[i] = count - 1 + 5;

        for (int j = 0; j < Horizon_SCAN; ++j)
        {
            // 检查当前点是否有效(距离值不为 FLT_MAX)
            if (rangeMat.at<float>(i, j) != FLT_MAX)
            {
                cloudInfo.pointColInd[count] = j;
                // 保存点的范围信息
                cloudInfo.pointRange[count] = rangeMat.at<float>(i, j);
                // 保存提取的点云
                extractedCloud->push_back(fullCloud->points[j + i * Horizon_SCAN]);
                // 增加计数器,记录提取的点云大小
                ++count;
            }
        }
        // 记录当前扫描行的结束索引
        cloudInfo.endRingIndex[i] = count - 1 - 5;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值