LeGO-Loam代码解析(二)--- Lego-LOAM的地面点分离、聚类、两步优化方法

1 地面点分离剔除方法

1.1 数学推导

        LeGO-LOAM 中前端改进中很重要的一点就是充分利用了地面点,那首先自然是提取
对地面点的提取。

        如上图,相邻的两个扫描线束的同一列打在地面上如 AB 点所示,他们的垂直高度差
h =|z_0 - z_1| ,水平距离差d = \sqrt{(x_0-x_1)^2 + (y_0-y_1)^2} ,计算垂直高度差和水平高度差的角度\theta= atan2(h, d) 。(这里\theta为AB到水平面的夹角)理想情况下,\theta应该接近 0,考虑到一方面激光雷达安装也无法做到绝对水平。另一方面,地面也不是绝对水平。因此,这个角度会略微大于 0,考虑到作者实际在草坪之类的场景下运动,因此这个值被设置成 10 度。
        实际上此种地面分离算法有一些简单,我们可以结合激光雷达安装高度等其他先验信息进行优化。

1.2 Lego-LOAM代码实现

        版本1:LegoLOAM改版

void ImageProjection::groundRemoval() {// _ground_mat// -1, no valid info to check if ground of not//  0, initial value, after validation, means not ground//  1, ground// 同一列由下到上 _horizontal_scans = 1800  _ground_scan_index=7(地面点的上限)16线设置为7  因为水平地面点不可能在天上面对把for (size_t j = 0; j < _horizontal_scans; ++j){for (size_t i = 0; i < _ground_scan_index; ++i){// 取出同一列相邻点的索引size_t lowerInd = j + (i)*_horizontal_scans;size_t upperInd = j + (i + 1) * _horizontal_scans;// 非有效点if (_full_cloud->points[lowerInd].intensity == -1 ||_full_cloud->points[upperInd].intensity == -1) {// no info to check, invalid points_ground_mat(i, j) = -1;continue;}float dX =_full_cloud->points[upperInd].x - _full_cloud->points[lowerInd].x;float dY =_full_cloud->points[upperInd].y - _full_cloud->points[lowerInd].y;float dZ =_full_cloud->points[upperInd].z - _full_cloud->points[lowerInd].z;float vertical_angle = std::atan2(dZ , sqrt(dX * dX + dY * dY + dZ * dZ));// TODO: review this change// 和上文的公式相同 _sensor_mount_angle设置为(_sensor_mount_angle *= DEG_TO_RAD) (DEG_TO_RAD = M_PI / 180.0)if ( (vertical_angle - _sensor_mount_angle) <= 10 * DEG_TO_RAD){_ground_mat(i, j) = 1;_ground_mat(i + 1, j) = 1;}}}// extract ground cloud (_ground_mat == 1)// mark entry that doesn't need to label (ground and invalid point) for// segmentation note that ground remove is from 0~_N_scan-1, need _range_mat// for mark label matrix for the 16th scan// 遍历特征矩阵 1800 * 16 如果是地面点/无效点的话 设置为-1 不参与线特征面特征的提取for (size_t i = 0; i < _vertical_scans; ++i){for (size_t j = 0; j < _horizontal_scans; ++j){if (_ground_mat(i, j) == 1 ||_range_mat(i, j) == FLT_MAX){_label_mat(i, j) = -1;}}}// RVIZ可视化的操作for (size_t i = 0; i <= _ground_scan_index; ++i){for (size_t j = 0; j < _horizontal_scans; ++j){if (_ground_mat(i, j) == 1)_ground_cloud->push_back(_full_cloud->points[j + i * _horizontal_scans]);}}
}

        版本2:Lego-LOAM原生

    void groundRemoval(){size_t lowerInd, upperInd;float diffX, diffY, diffZ, angle;// groundMat// -1, no valid info to check if ground of not//  0, initial value, after validation, means not ground//  1, groundfor (size_t j = 0; j < Horizon_SCAN; ++j){for (size_t i = 0; i < groundScanInd; ++i){lowerInd = j + ( i )*Horizon_SCAN;upperInd = j + (i+1)*Horizon_SCAN;if (fullCloud->points[lowerInd].intensity == -1 ||fullCloud->points[upperInd].intensity == -1){// no info to check, invalid pointsgroundMat.at<int8_t>(i,j) = -1;continue;}diffX = fullCloud->points[upperInd].x - fullCloud->points[lowerInd].x;diffY = fullCloud->points[upperInd].y - fullCloud->points[lowerInd].y;diffZ = fullCloud->points[upperInd].z - fullCloud->points[lowerInd].z;angle = atan2(diffZ, sqrt(diffX*diffX + diffY*diffY) ) * 180 / M_PI;if (abs(angle - sensorMountAngle) <= 10){groundMat.at<int8_t>(i,j) = 1;groundMat.at<int8_t>(i+1,j) = 1;}}}// extract ground cloud (groundMat == 1)// mark entry that doesn't need to label (ground and invalid point) for segmentation// note that ground remove is from 0~N_SCAN-1, need rangeMat for mark label matrix for the 16th scanfor (size_t i = 0; i < N_SCAN; ++i){for (size_t j = 0; j < Horizon_SCAN; ++j){if (groundMat.at<int8_t>(i,j) == 1 || rangeMat.at<float>(i,j) == FLT_MAX){labelMat.at<int>(i,j) = -1;}}}if (pubGroundCloud.getNumSubscribers() != 0){for (size_t i = 0; i <= groundScanInd; ++i){for (size_t j = 0; j < Horizon_SCAN; ++j){if (groundMat.at<int8_t>(i,j) == 1)groundCloud->push_back(fullCloud->points[j + i*Horizon_SCAN]);}}}}

2 基于BFS的点云聚类和外点剔除、树干点云提取算法

2.1 数学推导

        广度优先遍历(BFS)和深度优先遍历(DFS)同属于两种经典的图遍历的算法。
        具体到广度优先遍历算法来说,首先从某个节点出发,一层一层地遍历,下一层必须等到上一层节点全部遍历完成之后才会开始遍历。

        比如在上面这个无向图中,如果我们从 A 节点开始遍历,那么首先访问和 A 节点相邻
的节点,这里就是 S、B、D,然后在访问和 S、B、D 相邻的其他节点,这里就是 C。
        因此,遍历的顺序是 A->S->B->D->C;如果我们从 S 开始遍历,则顺序就是 S->A->B-
>C->D;可以看到,不同的起始点对应的遍历顺序是不同的。
        通常我们使用 BFS 遍历图结构的时候,会借助一个队列 std::queue 来帮助我们实现,
        其基本步骤如下:

1. 将起始顶点 S 加入队列
2. 访问 S,同时把 S 标记为已访问
3. 循环从队列中取出节
        1.如果队列为空,则访问结束
        2.否则类似 S,将该节点标记为已访问,同时将其子节点加入队列。
        在 LeGO-LOAM 中,BFS 算法用于实现一个简单的点云聚类功能。

        BFS 算法适用于图数据结构,为了把单帧 lidar 点云运用上 BFS 算法,首先需要将其建模成一个图模型,一个很简单有效的办法就是将其投影到一个平面图上,以velodyne-16 为例,我们将其投影到一个 16×1800 大小的图上(这里 16 是一共有 16跟线束,1800 是因为水平分辨率是 0.2 度,一个扫描周期有 1800 个点)如图:

        对于任何一个珊格点,其上下左右四个相邻点视为图结构中的邻接节点,这里要注意的是,左右边界的点和边界另一侧也构成邻接,因为水平方向是同一个扫描周期,具有物理意义上的连续性。我们可以以任意一个点开始执行 BFS 搜索,直到遍历完这部分近邻点,聚类点数过少的就认为是 outlier,可以被剔除。

具体实现
        1. 遍历每个点,如果该点已经被处理过了就不再处理
        2. 如果没有被处理就说明这是一个新的聚类,然后执行 BFS 的步骤
                1.将队列里的首元素弹出,然后将该元素近邻塞入队列末尾(这里没有使用std::queue,使用的普通数组,所以就使用双指针来替代)

        这里的近邻就是上下左右:

         2.分别判断近邻和自身距离是否足够近

         angle 越大则认为两点越可能是同一个聚类物体上的点,则打上同样的 label。

        3.如此循环

2.2 代码实现

在嘈杂环境中的扫描的特征提取过程。原始点云如图(a)所示。在图(b)中,红色点被标记为地面点。其余的点是分割后保留的点。只保留树干..树叶点都被剔除掉了在图(c)中,蓝色和黄色点表示F e 和 F p 中的边缘和平面特征。在图(d)中,绿色和粉色点分别代表F e 和 F p 中的边缘和平面特征。

        广度优先遍历代码:

// 广度优先遍历void labelComponents(int row, int col){// use std::queue std::vector std::deque will slow the program down greatlyfloat d1, d2, alpha, angle;int fromIndX, fromIndY, thisIndX, thisIndY; bool lineCountFlag[N_SCAN] = {false};// 用一个数组保存X坐标,一个数组保存Y坐标queueIndX[0] = row;queueIndY[0] = col;// 队列里面有多少个点int queueSize = 1;// 指向前端int queueStartInd = 0;// 指向即将要加入的位置int queueEndInd = 1;// 将这个点的(X,Y)坐标赋予给allPushedIndX[0]allPushedIndX[0] = row;allPushedIndY[0] = col;int allPushedIndSize = 1;while(queueSize > 0){// 将队列的首元素去除,完成广度优先的操作fromIndX = queueIndX[queueStartInd];fromIndY = queueIndY[queueStartInd];// 队列中一共有多少点--queueSize;// 将指向队列首部的指针向前移动1++queueStartInd;// 标记这个点云属于哪一类,当前聚类的ID,当前物体属于同一个ID在确定当前可以聚类的情况下更新 labelCount,labelCount++labelMat.at<int>(fromIndX, fromIndY) = labelCount;// 遍历这个结点的邻接顶点,其实就是上下左右四个元素// std::vector<std::pair<int8_t, int8_t> > neighborIterator;// std::pair<int8_t, int8_t> neighbor;// neighbor.first = -1; neighbor.second =  0; neighborIterator.push_back(neighbor);// neighbor.first =  0; neighbor.second =  1; neighborIterator.push_back(neighbor);// neighbor.first =  0; neighbor.second = -1; neighborIterator.push_back(neighbor);// neighbor.first =  1; neighbor.second =  0; neighborIterator.push_back(neighbor);for (auto iter = neighborIterator.begin(); iter != neighborIterator.end(); ++iter){// 四个可能的邻接顶点的(X,Y)值thisIndX = fromIndX + (*iter).first;thisIndY = fromIndY + (*iter).second;// 索引必须在(N_SCAN * HORIZON_SCAN = 16 * 1600中)// 行数不在 0 - 16 间(最上面最下面的点 第1根线或者第8根线的点)if (thisIndX < 0 || thisIndX >= N_SCAN)continue;// 同一根线相距360度且左右相邻if (thisIndY < 0)thisIndY = Horizon_SCAN - 1;if (thisIndY >= Horizon_SCAN)thisIndY = 0;// 这个顶点是否被访问过if (labelMat.at<int>(thisIndX, thisIndY) != 0)continue;// 将两个点距离雷达较远的点的距离赋值为d1,较近的点赋值为d2d1 = std::max(rangeMat.at<float>(fromIndX, fromIndY), rangeMat.at<float>(thisIndX, thisIndY));d2 = std::min(rangeMat.at<float>(fromIndX, fromIndY), rangeMat.at<float>(thisIndX, thisIndY));// (0,1) (0,-1) 角分辨率为0.2度--左右糯if ((*iter).first == 0)alpha = segmentAlphaX;// (1,0) (-1,0) 角分辨率为2度--上下挪动elsealpha = segmentAlphaY;// d2*sin(alpha)为垂线距离  d2*cos(alpha))为垂线的对边 d1 -d2*cos(alpha))为小边 求tan值为angle角度angle = atan2(d2*sin(alpha), (d1 -d2*cos(alpha)));// 60度(弧度制) 60.0/180.0*M_PI;   ----减少这个值会增大精度if (angle > segmentTheta){// 丁真 添加到队列中   queueEndInd指向即将要插入的位置中queueIndX[queueEndInd] = thisIndX;queueIndY[queueEndInd] = thisIndY;++queueSize;++queueEndInd;// 标记和前面一种的类型labelMat.at<int>(thisIndX, thisIndY) = labelCount;// 标志位 : 是否这个点被遍历过设置为truelineCountFlag[thisIndX] = true;// 保存聚类结果allPushedIndX[allPushedIndSize] = thisIndX;allPushedIndY[allPushedIndSize] = thisIndY;++allPushedIndSize;}}}// check if this segment is validbool feasibleSegment = false;// 如果聚类的点大于30,小于30认为是叶子点/噪声点if (allPushedIndSize >= 30)feasibleSegment = true;// 可能是树杆(垂直于LIDAR扫描方向的)else if (allPushedIndSize >= segmentValidPointNum){int lineCount = 0;for (size_t i = 0; i < N_SCAN; ++i)if (lineCountFlag[i] == true)++lineCount;if (lineCount >= segmentValidLineNum)feasibleSegment = true;            }// segment is valid, mark these pointsif (feasibleSegment == true){// 更新标签值,表示下次是新的一陀东西++labelCount;}else{// 聚类无效for (size_t i = 0; i < allPushedIndSize; ++i){labelMat.at<int>(allPushedIndX[i], allPushedIndY[i]) = 999999;}}}

        特征提取前预处理代码:

    // 对非地面点聚类void cloudSegmentation(){// 对所有点进行遍历 N_SCAN=16线 Horizon_SCAN=1800(每根线有1800个点)for (size_t i = 0; i < N_SCAN; ++i)for (size_t j = 0; j < Horizon_SCAN; ++j)if (labelMat.at<int>(i,j) == 0)// 地面点会是-1我们不处理(在上面的代码讲解中已经说过了)// 以这个点为顶点进行广度优先遍历labelComponents(i, j);int sizeOfSegCloud = 0;// 遍历本帧所有激光点for (size_t i = 0; i < N_SCAN; ++i){segMsg.startRingIndex[i] = sizeOfSegCloud-1 + 5;for (size_t j = 0; j < Horizon_SCAN; ++j){// 要不是有效的距离点 要不是有效的地面点if (labelMat.at<int>(i,j) > 0 || groundMat.at<int8_t>(i,j) == 1){// 聚类无效的点if (labelMat.at<int>(i,j) == 999999){if (i > groundScanInd && j % 5 == 0){outlierCloud->push_back(fullCloud->points[j + i*Horizon_SCAN]);continue;}else{continue;}}// 1800列 做一个下采样 点云加密的时候可以用// groundMat// -1, no valid info to check if ground of not//  0, initial value, after validation, means not ground//  1, groundif (groundMat.at<int8_t>(i,j) == 1){if (j%5!=0 && j>5 && j<Horizon_SCAN-5)continue;}// 标记地面点,这样在后续提取特征的时候不会被当作角点提取segMsg.segmentedCloudGroundFlag[sizeOfSegCloud] = (groundMat.at<int8_t>(i,j) == 1);// mark the points' column index for marking occlusion latersegMsg.segmentedCloudColInd[sizeOfSegCloud] = j;// 保存距离激光雷达的距离segMsg.segmentedCloudRange[sizeOfSegCloud]  = rangeMat.at<float>(i,j);// 保存点云segmentedCloud->push_back(fullCloud->points[j + i*Horizon_SCAN]);// 更新下一帧的点的索引++sizeOfSegCloud;}}// 同LIO-SAM特征提取的边界segMsg.endRingIndex[i] = sizeOfSegCloud-1 - 5;}// 发布给RVIZif (pubSegmentedCloudPure.getNumSubscribers() != 0){for (size_t i = 0; i < N_SCAN; ++i){for (size_t j = 0; j < Horizon_SCAN; ++j){if (labelMat.at<int>(i,j) > 0 && labelMat.at<int>(i,j) != 999999){segmentedCloudPure->push_back(fullCloud->points[j + i*Horizon_SCAN]);segmentedCloudPure->points.back().intensity = labelMat.at<int>(i,j);}}}}}

3  两步优化的帧间里程计

3.1 理论推导

        和原始 LOAM (或者 A-LOAM )一样,通过前后两帧点云来估计两帧之间的运动,从而累加得到前端里程记的输出,和上述方法使用线面约束同时优化六自由度帧间位姿不同,LeGO-LOAM 的前端分成两个步骤,每个步骤估计三自由度的变量:
        第一步 利用地面点优化
        地面点更符合面特征的性质。因此,地面点的优化问题就使用点到面的约束来构建,同时我们注意到,地面点之间的约束对 x , y 和 yaw 这三个自由度是不能观的。换句话说,当这三个自由度的值发生变化时,点到面的残差不会发生显著变化。所以,地面点之间的优化只会对 pitch , roll 以及 z 进行约束和优化
        第二步 利用角点优化
        第一步优化完 pitch 、 roll 以及 z 之后,我们仍需对另外三个自由度的变量进行估计,此时,我们选用提取的角点进行优化,由于多线激光雷达提取的角点通常是垂直的边缘特征。

        因此,这些特征对 x 、 y 以及yaw 有着比较好的能观性,通过角点的优化结合上地面点的结果可以得到六自由度的帧间优化结果。

激光里程计模块的两步优化。首先通过匹配从地面点提取的平面特征获得 [t z , θ roll , θ pitch ]。然后,使用从分割点提取的边缘特征来估计 [t x , t y , θ yaw ],同时将 [t z , θ roll , θ pitch ] 作为约束。

3.2 代码实现

    void updateTransformation(){if (laserCloudCornerLastNum < 10 || laserCloudSurfLastNum < 100)return;for (int iterCount1 = 0; iterCount1 < 25; iterCount1++) {laserCloudOri->clear();coeffSel->clear();// 平面点优化findCorrespondingSurfFeatures(iterCount1);if (laserCloudOri->points.size() < 10)continue;if (calculateTransformationSurf(iterCount1) == false)break;}for (int iterCount2 = 0; iterCount2 < 25; iterCount2++) {laserCloudOri->clear();coeffSel->clear();// 角点优化findCorrespondingCornerFeatures(iterCount2);if (laserCloudOri->points.size() < 10)continue;if (calculateTransformationCorner(iterCount2) == false)break;}}

        每一个部分和LOAM一致,请移步我的多传感器融合的博客。

    void findCorrespondingSurfFeatures(int iterCount){int surfPointsFlatNum = surfPointsFlat->points.size();for (int i = 0; i < surfPointsFlatNum; i++) {TransformToStart(&surfPointsFlat->points[i], &pointSel);if (iterCount % 5 == 0) {kdtreeSurfLast->nearestKSearch(pointSel, 1, pointSearchInd, pointSearchSqDis);int closestPointInd = -1, minPointInd2 = -1, minPointInd3 = -1;if (pointSearchSqDis[0] < nearestFeatureSearchSqDist) {closestPointInd = pointSearchInd[0];int closestPointScan = int(laserCloudSurfLast->points[closestPointInd].intensity);float pointSqDis, minPointSqDis2 = nearestFeatureSearchSqDist, minPointSqDis3 = nearestFeatureSearchSqDist;for (int j = closestPointInd + 1; j < surfPointsFlatNum; j++) {if (int(laserCloudSurfLast->points[j].intensity) > closestPointScan + 2.5) {break;}pointSqDis = (laserCloudSurfLast->points[j].x - pointSel.x) * (laserCloudSurfLast->points[j].x - pointSel.x) + (laserCloudSurfLast->points[j].y - pointSel.y) * (laserCloudSurfLast->points[j].y - pointSel.y) + (laserCloudSurfLast->points[j].z - pointSel.z) * (laserCloudSurfLast->points[j].z - pointSel.z);if (int(laserCloudSurfLast->points[j].intensity) <= closestPointScan) {if (pointSqDis < minPointSqDis2) {minPointSqDis2 = pointSqDis;minPointInd2 = j;}} else {if (pointSqDis < minPointSqDis3) {minPointSqDis3 = pointSqDis;minPointInd3 = j;}}}for (int j = closestPointInd - 1; j >= 0; j--) {if (int(laserCloudSurfLast->points[j].intensity) < closestPointScan - 2.5) {break;}pointSqDis = (laserCloudSurfLast->points[j].x - pointSel.x) * (laserCloudSurfLast->points[j].x - pointSel.x) + (laserCloudSurfLast->points[j].y - pointSel.y) * (laserCloudSurfLast->points[j].y - pointSel.y) + (laserCloudSurfLast->points[j].z - pointSel.z) * (laserCloudSurfLast->points[j].z - pointSel.z);if (int(laserCloudSurfLast->points[j].intensity) >= closestPointScan) {if (pointSqDis < minPointSqDis2) {minPointSqDis2 = pointSqDis;minPointInd2 = j;}} else {if (pointSqDis < minPointSqDis3) {minPointSqDis3 = pointSqDis;minPointInd3 = j;}}}}pointSearchSurfInd1[i] = closestPointInd;pointSearchSurfInd2[i] = minPointInd2;pointSearchSurfInd3[i] = minPointInd3;}if (pointSearchSurfInd2[i] >= 0 && pointSearchSurfInd3[i] >= 0) {tripod1 = laserCloudSurfLast->points[pointSearchSurfInd1[i]];tripod2 = laserCloudSurfLast->points[pointSearchSurfInd2[i]];tripod3 = laserCloudSurfLast->points[pointSearchSurfInd3[i]];float pa = (tripod2.y - tripod1.y) * (tripod3.z - tripod1.z) - (tripod3.y - tripod1.y) * (tripod2.z - tripod1.z);float pb = (tripod2.z - tripod1.z) * (tripod3.x - tripod1.x) - (tripod3.z - tripod1.z) * (tripod2.x - tripod1.x);float pc = (tripod2.x - tripod1.x) * (tripod3.y - tripod1.y) - (tripod3.x - tripod1.x) * (tripod2.y - tripod1.y);float pd = -(pa * tripod1.x + pb * tripod1.y + pc * tripod1.z);float ps = sqrt(pa * pa + pb * pb + pc * pc);pa /= ps;pb /= ps;pc /= ps;pd /= ps;float pd2 = pa * pointSel.x + pb * pointSel.y + pc * pointSel.z + pd;float s = 1;if (iterCount >= 5) {s = 1 - 1.8 * fabs(pd2) / sqrt(sqrt(pointSel.x * pointSel.x+ pointSel.y * pointSel.y + pointSel.z * pointSel.z));}if (s > 0.1 && pd2 != 0) {coeff.x = s * pa;coeff.y = s * pb;coeff.z = s * pc;coeff.intensity = s * pd2;laserCloudOri->push_back(surfPointsFlat->points[i]);coeffSel->push_back(coeff);}}}}
   void findCorrespondingCornerFeatures(int iterCount){int cornerPointsSharpNum = cornerPointsSharp->points.size();for (int i = 0; i < cornerPointsSharpNum; i++) {TransformToStart(&cornerPointsSharp->points[i], &pointSel);if (iterCount % 5 == 0) {kdtreeCornerLast->nearestKSearch(pointSel, 1, pointSearchInd, pointSearchSqDis);int closestPointInd = -1, minPointInd2 = -1;if (pointSearchSqDis[0] < nearestFeatureSearchSqDist) {closestPointInd = pointSearchInd[0];int closestPointScan = int(laserCloudCornerLast->points[closestPointInd].intensity);float pointSqDis, minPointSqDis2 = nearestFeatureSearchSqDist;for (int j = closestPointInd + 1; j < cornerPointsSharpNum; j++) {if (int(laserCloudCornerLast->points[j].intensity) > closestPointScan + 2.5) {break;}pointSqDis = (laserCloudCornerLast->points[j].x - pointSel.x) * (laserCloudCornerLast->points[j].x - pointSel.x) + (laserCloudCornerLast->points[j].y - pointSel.y) * (laserCloudCornerLast->points[j].y - pointSel.y) + (laserCloudCornerLast->points[j].z - pointSel.z) * (laserCloudCornerLast->points[j].z - pointSel.z);if (int(laserCloudCornerLast->points[j].intensity) > closestPointScan) {if (pointSqDis < minPointSqDis2) {minPointSqDis2 = pointSqDis;minPointInd2 = j;}}}for (int j = closestPointInd - 1; j >= 0; j--) {if (int(laserCloudCornerLast->points[j].intensity) < closestPointScan - 2.5) {break;}pointSqDis = (laserCloudCornerLast->points[j].x - pointSel.x) * (laserCloudCornerLast->points[j].x - pointSel.x) + (laserCloudCornerLast->points[j].y - pointSel.y) * (laserCloudCornerLast->points[j].y - pointSel.y) + (laserCloudCornerLast->points[j].z - pointSel.z) * (laserCloudCornerLast->points[j].z - pointSel.z);if (int(laserCloudCornerLast->points[j].intensity) < closestPointScan) {if (pointSqDis < minPointSqDis2) {minPointSqDis2 = pointSqDis;minPointInd2 = j;}}}}pointSearchCornerInd1[i] = closestPointInd;pointSearchCornerInd2[i] = minPointInd2;}if (pointSearchCornerInd2[i] >= 0) {tripod1 = laserCloudCornerLast->points[pointSearchCornerInd1[i]];tripod2 = laserCloudCornerLast->points[pointSearchCornerInd2[i]];float x0 = pointSel.x;float y0 = pointSel.y;float z0 = pointSel.z;float x1 = tripod1.x;float y1 = tripod1.y;float z1 = tripod1.z;float x2 = tripod2.x;float y2 = tripod2.y;float z2 = tripod2.z;float m11 = ((x0 - x1)*(y0 - y2) - (x0 - x2)*(y0 - y1));float m22 = ((x0 - x1)*(z0 - z2) - (x0 - x2)*(z0 - z1));float m33 = ((y0 - y1)*(z0 - z2) - (y0 - y2)*(z0 - z1));float a012 = sqrt(m11 * m11  + m22 * m22 + m33 * m33);float l12 = sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2) + (z1 - z2)*(z1 - z2));float la =  ((y1 - y2)*m11 + (z1 - z2)*m22) / a012 / l12;float lb = -((x1 - x2)*m11 - (z1 - z2)*m33) / a012 / l12;float lc = -((x1 - x2)*m22 + (y1 - y2)*m33) / a012 / l12;float ld2 = a012 / l12;float s = 1;if (iterCount >= 5) {s = 1 - 1.8 * fabs(ld2);}if (s > 0.1 && ld2 != 0) {coeff.x = s * la; coeff.y = s * lb;coeff.z = s * lc;coeff.intensity = s * ld2;laserCloudOri->push_back(cornerPointsSharp->points[i]);coeffSel->push_back(coeff);}}}}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/52624.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

n5173b是德科技keysight N5173B信号发生器

产品概述 是德科技/安捷伦N5173B EXG模拟信号发生器 当您需要平衡预算和性能时&#xff0c;是德科技N5173B EXG微波模拟信号发生器是经济高效的选择。它提供解决宽带滤波器、放大器、接收机等参数测试的基本信号。执行基本LO上变频或CW阻塞&#xff0c;低成本覆盖13、20、31.…

Dynamic CRM开发 - 实体字段(三)计算字段

在 Dynamic CRM开发 - 实体字段(一)中提到了实体字段的属性字段类型:有简单、计算、汇总三种,本篇幅通过一个示例讲解计算字段。 有这样一个需求:根据用户填写的出生日期,计算年龄。 1、创建一个“出生日期”字段,时间类型即可。 2、创建一个计算字段“年龄”,如下图…

数据库(DQL,多表设计,事务,索引)

目录 查询数据库表中数据 where 条件列表 group by 分组查询 having 分组后条件列表 order by 排序字段列表 limit 分页参数 多表设计 一对多 多对多 一对一 多表查询 事物 索引 查询数据库表中数据 关键字&#xff1a;SELECT 中间有空格&#xff0c;加引…

【C++】—— C++11新特性之 “右值引用和移动语义”

前言&#xff1a; 本期&#xff0c;我们将要的介绍有关 C右值引用 的相关知识。对于本期知识内容&#xff0c;大家是必须要能够掌握的&#xff0c;在面试中是属于重点考察对象。 目录 &#xff08;一&#xff09;左值引用和右值引用 1、什么是左值&#xff1f;什么是左值引用…

PID直观感受简述

0、仿真控制框图 1、增加p的作用&#xff08;增加响应&#xff09;P 2、增加I的作用&#xff08;消除稳差&#xff09;PI 3、增加D的作用&#xff08;抑制波动&#xff09;PID 加入对噪声很敏 4、综合比对

linux中定时器的使用

在Linux中&#xff0c;可以使用timer_create、timer_settime和timer_delete等函数来创建和管理定时器。下面是一个简单的示例程序&#xff0c;演示如何在Linux中使用定时器&#xff1a; #include <stdio.h> #include <stdlib.h> #include <signal.h> #inclu…

STL---list

目录 1. list的介绍及使用 1.1 list的介绍 1.2 list的使用注意事项 2.list接口介绍及模拟实现 2.1构造​编辑 2.2容量 2.3修改 3.list迭代器 4.迭代器失效 5.模拟实现 6.vector和list的区别 1. list的介绍及使用 1.1 list的介绍 list的文档介绍 1. list是可以在常…

数据库第十七课-------ETL任务调度系统的安装和使用

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

Jenkins配置远程服务器SSH Server流程

说明&#xff1a;以阿里云轻量应用服务器&#xff0c;本文介绍如何在Jenkins中配置远程服务器&#xff0c;Jenkins安装参考这篇文章&#xff1b; 第一步&#xff1a;启动服务 首先&#xff0c;启动Jenkins容器&#xff0c;进入Jenkins管理后台&#xff0c;点击系统配置&#…

echarts 的dataZoom滑块两端文字被遮挡

问题&#xff1a; 期望&#xff1a; 解决方案&#xff1a; 1&#xff1a;调整宽度&#xff08;4版本的没有width属性&#xff09; 2. 参考&#xff1a;echarts图标设置dataZoom拖拽时间轴时自动调整两侧文字的位置_datazoom 位置_乌栖曲的博客-CSDN博客 设置文字的定位 cons…

物联网(IoT)安全挑战与解决方案: 分析物联网设备面临的安全威胁,以及如何设计和管理安全的IoT生态系统

第一章&#xff1a;引言 随着科技的飞速发展&#xff0c;物联网&#xff08;IoT&#xff09;作为连接世界的桥梁&#xff0c;已经成为现代社会不可或缺的一部分。然而&#xff0c;随着IoT设备数量的不断增加&#xff0c;其安全问题也日益显著。本文将深入探讨IoT领域面临的安全…

暄桐展览| 我们桐学有自己的习作展(1)

林曦老师《从书法之美到生活之美》的第五阶课程《静定的滋养2021》已告一段落。570天的用功&#xff0c;桐学们的技艺都有了水涨船高的进益。      无论书法课&#xff08;全阶和五阶&#xff09;还是国画课&#xff0c;暄桐都有一套完整系统的教学体系&#xff0c;也会在桐…

Java | IDEA中Netty运行多个client的方法

想要运行多个client但出现这种提示&#xff1a; 解决方法 1、打开IDEA&#xff0c;右上角找到下图&#xff0c;并点击 2、勾选

微信支付

文档地址&#xff1a;https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter9_1 封装的工具类 package com.qf.fmall.utils;import cn.hutool.core.util.XmlUtil; import cn.hutool.http.HttpRequest; import org.apache.shiro.crypto.hash.Md5Hash;import java.util.…

【keepalived双机热备与 lvs(DR)】

目录 一、概述 1.简介 2.原理 3.作用 二、安装 1.配置文件 2.配置项 三、功能模块 1.core 2.vrrp 3.check 四、配置双机热备 1.master 2.backup 五、验证 1.ping验证 2.服务验证 六、双机热备的脑裂现象 七、keepalivedlvs&#xff08;DR&#xff09; 1.作…

性能测试告诉你 mysql 数据库存储引擎该如何选?

简介 数据库存储引擎&#xff1a;是数据库底层软件组织&#xff0c;数据库管理系统&#xff08;DBMS&#xff09;使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能&#xff0c;使用不同的存储引擎&#xff0c;还可以…

Wireshark数据抓包分析之互联网控制报文协议_ICMP

一、实验目的: 通过使用wireshark抓取的ICMP数据包对这个ICMP控制报文进行分析 二、预备知识&#xff1a; 1.ICMP协议概述&#xff1a;ICMP是Internet Control Message Protocol的缩写&#xff0c;即互联网控制报文协议。它是TCP/IP协议族的一个子协议&#xff0c;用于IP主机、…

HDLBits-Verilog学习记录 | Verilog Language-Vectors

文章目录 11.vectors | vector012.vectors in more detail | vector113.Vector part select | Vector214.Bitwise operators | Vectorgates15.Four-input gates | Gates416.Vector concatenation operator | Vector317.Vector reversal 1 | Vectorr18. Replication operator | …

SD-WebUI和ComfyUI的局域网访问设置!

如何通过局域网访问AI绘画软件&#xff0c;这是星球成员提的一个问题&#xff0c;而且两个软件都问到了&#xff0c;我也回答过了。现在把内容整理一下发出来&#xff0c;大家可能用得着。 SD-WebUI和ComfyUI这两个AI绘画工具都是通过浏览器来使用&#xff0c;但是默认情况下并…

Request对象和response对象

一、概念 request对象和response对象是通过Servlet容器&#xff08;如Tomcat&#xff09;自动创建并传递给Servlet的。 Servlet容器负责接收客户端的请求&#xff0c;并将请求信息封装到request对象中&#xff0c;然后将request对象传 递给相应的Servlet进行处理。类似地&…