当前位置: 首页 > news >正文

3d激光slam建图与定位(2)_aloam代码阅读

1.常用的几种loam算法
aloam 纯激光
lego_loam 纯激光 去除了地面
lio_sam imu+激光紧耦合
lvi_sam 激光+视觉
2.代码思路
2.1.特征点提取scanRegistration.cpp,这个文件的目的是为了根据曲率提取4种特征点和对当前点云进行预处理
输入是雷达点云话题
输出是 4种特征点点云和预处理后的当前点云
(1)带有点云线束id+在角度范围所处进度的全部有效点点云(因为雷达是旋转的,雷达旋转的进度)
(2)曲率比较大的特征点点云,2个点
(3)曲率一般大的特征点点云,20个点
(4)曲率比较小的面点点云
(5)一般面点点云,角点提取剩下的那些点
当输入雷达是16线雷达,去计算是哪条线id是通过计算俯仰角每2度算一根线去计算的
角度所处的进度,是根据水平角计算获得的
2.1.1选择雷达扫描半径范围内的点

 pcl::PointCloud<pcl::PointXYZ> narrowed_scan;narrowed_scan.header = scan.header;if (min_range >= max_range){ROS_ERROR_ONCE("min_range>=max_range @(%lf, %lf)", min_range, max_range);return scan;}double square_min_range = min_range * min_range;double square_max_range = max_range * max_range;for (pcl::PointCloud<pcl::PointXYZ>::const_iterator iter = scan.begin(); iter != scan.end(); ++iter){const pcl::PointXYZ &p = *iter;// 点云点到原点的位置的欧式距离double square_distance = p.x * p.x + p.y * p.y;if (square_min_range <= square_distance && square_distance <= square_max_range){narrowed_scan.points.push_back(p);}}return narrowed_scan;

2.1.2计算水平角角度范围

 int cloudSize = laserCloudIn.points.size();
// 起始点角度    atan2范围是-pi~pi
float startOri = -atan2(laserCloudIn.points[0].y, laserCloudIn.points[0].x);
// 终止点角度
float endOri = -atan2(laserCloudIn.points[cloudSize - 1].y,laserCloudIn.points[cloudSize - 1].x) +2 * M_PI;
// 总有些例外 ,比如这里大于3pi 和小于pi 就要做一些调整到合理的范围
if (endOri - startOri > 3 * M_PI)
{endOri -= 2 * M_PI;
}
else if (endOri - startOri < M_PI)
{endOri += 2 * M_PI;
}
Eigen::Vector2f anglerange;
anglerange[0] = startOri;
anglerange[1] = endOri;
return anglerange;

2.1.3每条线上点的计算范围

// 计算的范围 起始点是从第5个点开始 终止点是倒数第6个点
for (int i = 0; i < N_SCANS; i++)
{// 第一根线就是 0+5scanStartInd[i] = laserCloud->size() + 5;*laserCloud += laserCloudScans[i];// 第一根线就是第一根线的点数量-6scanEndInd[i] = laserCloud->size() - 6;
}

2.1.4计算每个点的曲率

  // 开始计算曲率for (int i = 5; i < cloudSize - 5; i++){// 每一小段计算了弧长float diffX = laserCloud->points[i - 5].x + laserCloud->points[i - 4].x + laserCloud->points[i - 3].x + laserCloud->points[i - 2].x + laserCloud->points[i - 1].x - 10 * laserCloud->points[i].x + laserCloud->points[i + 1].x + laserCloud->points[i + 2].x + laserCloud->points[i + 3].x + laserCloud->points[i + 4].x + laserCloud->points[i + 5].x;float diffY = laserCloud->points[i - 5].y + laserCloud->points[i - 4].y + laserCloud->points[i - 3].y + laserCloud->points[i - 2].y + laserCloud->points[i - 1].y - 10 * laserCloud->points[i].y + laserCloud->points[i + 1].y + laserCloud->points[i + 2].y + laserCloud->points[i + 3].y + laserCloud->points[i + 4].y + laserCloud->points[i + 5].y;float diffZ = laserCloud->points[i - 5].z + laserCloud->points[i - 4].z + laserCloud->points[i - 3].z + laserCloud->points[i - 2].z + laserCloud->points[i - 1].z - 10 * laserCloud->points[i].z + laserCloud->points[i + 1].z + laserCloud->points[i + 2].z + laserCloud->points[i + 3].z + laserCloud->points[i + 4].z + laserCloud->points[i + 5].z;// 存储起始+5到终止-6每个点对应的曲率cloudCurvature[i] = diffX * diffX + diffY * diffY + diffZ * diffZ;// 每个点的索引cloudSortInd[i] = i;// 标记cloudNeighborPicked[i] = 0;cloudLabel[i] = 0;}

2.1.4对每个点的曲率进行排序

   float t_q_sort = 0;
// 遍历每个scan
for (int i = 0; i < N_SCANS; i++)
{// 点云点数小于6个就认为 没有有效的点 就进行continueif (scanEndInd[i] - scanStartInd[i] < 6){continue;}// 用来存储不太平整的点pcl::PointCloud<PointType>::Ptr surfPointsLessFlatScan(new pcl::PointCloud<PointType>);// 将每个scan分成6等分for (int j = 0; j < 6; j++){// 每个等分的起点和结束点// 起点idint sp = scanStartInd[i] + (scanEndInd[i] - scanStartInd[i]) * j / 6;// 结束点idint ep = scanStartInd[i] + (scanEndInd[i] - scanStartInd[i]) * (j + 1) / 6 - 1;TicToc t_tmp;// 对点云曲率进行索引排序,小的在前,大的在后std::sort(cloudSortInd + sp, cloudSortInd + ep + 1, comp);t_q_sort += t_tmp.toc();int largestPickedNum = 0;// 挑选曲率比较大的部分for (int k = ep; k >= sp; k--){// 排序后顺序就乱了,这个时候索引的作用就体现出来了int ind = cloudSortInd[k];// 看看这个点是否是有效点,同时曲率是否大于阈值if (cloudNeighborPicked[ind] == 0 &&cloudCurvature[ind] > 0.1){largestPickedNum++;// //目的是为了当前帧大曲率的点和上一帧小一点曲率的点建立约束//    每段选两个曲率大的点if (largestPickedNum <= 2){cloudLabel[ind] = 2;// cornerPointsSharp存放大曲率的点cornerPointsSharp.push_back(laserCloud->points[ind]);cornerPointsLessSharp.push_back(laserCloud->points[ind]);}// 以及20个曲率稍微大一点的点else if (largestPickedNum <= 20){// label置1表示曲率稍微大一点的点cloudLabel[ind] = 1;cornerPointsLessSharp.push_back(laserCloud->points[ind]);}// 超过20个就算了else{break;}// 这个点被选中了,pick标志位置1cloudNeighborPicked[ind] = 1;// 为了保证特征点不过渡集中,将选中的点周围5个点都置为1,避免后续会选到for (int l = 1; l <= 5; l++){// 查看相邻点距离是否差异过大,如果差异过大说明点云在此不连续,是特征边缘,就会是新的特征,因此就不置位了float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l - 1].x;float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l - 1].y;float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l - 1].z;if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05){break;}cloudNeighborPicked[ind + l] = 1;}// 下面同理for (int l = -1; l >= -5; l--){float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l + 1].x;float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l + 1].y;float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l + 1].z;if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05){break;}cloudNeighborPicked[ind + l] = 1;}}}// 下面开始挑选面点int smallestPickedNum = 0;for (int k = sp; k <= ep; k++){int ind = cloudSortInd[k];// 确定这个点没有被pack 并且曲率小于阈值if (cloudNeighborPicked[ind] == 0 &&cloudCurvature[ind] < 0.1){// -1是认为平坦的点cloudLabel[ind] = -1;surfPointsFlat.push_back(laserCloud->points[ind]);smallestPickedNum++;// 每个等分取4个比较平坦的面点// 这里不区分平坦和比较平坦,因为剩下的点label默认是0就是比较平坦if (smallestPickedNum >= 4){break;}cloudNeighborPicked[ind] = 1;// 下面同理 除非曲率在0.05-0.1之间的点 否则就要标记后5个点for (int l = 1; l <= 5; l++){float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l - 1].x;float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l - 1].y;float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l - 1].z;if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05){break;}cloudNeighborPicked[ind + l] = 1;}// 标记前5个点for (int l = -1; l >= -5; l--){float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l + 1].x;float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l + 1].y;float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l + 1].z;if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05){break;}cloudNeighborPicked[ind + l] = 1;}}}for (int k = sp; k <= ep; k++){// 除了角点 其它的点都是一般平坦的点云if (cloudLabel[k] <= 0){surfPointsLessFlatScan->push_back(laserCloud->points[k]);}}}pcl::PointCloud<PointType> surfPointsLessFlatScanDS;pcl::VoxelGrid<PointType> downSizeFilter;// 一般平坦的点比较多,所以这里做一个体素滤波downSizeFilter.setInputCloud(surfPointsLessFlatScan);downSizeFilter.setLeafSize(0.2, 0.2, 0.2);downSizeFilter.filter(surfPointsLessFlatScanDS);surfPointsLessFlat += surfPointsLessFlatScanDS;
}
printf("sort q time %f \n", t_q_sort);

2.1.5 发布4种特征点及插入标志的整体点云

    // 分别将当前点云 四种特征的点云发布出去
sensor_msgs::PointCloud2 laserCloudOutMsg;
// 1.整体的点云 对每个点打了标签(哪一根线id+在角度范围所处的一个进度)
pcl::toROSMsg(*laserCloud, laserCloudOutMsg);
laserCloudOutMsg.header.stamp = laserCloudMsg->header.stamp;
laserCloudOutMsg.header.frame_id = "/map";
pubLaserCloud.publish(laserCloudOutMsg);sensor_msgs::PointCloud2 cornerPointsSharpMsg;
// 2.大曲率特征点
pcl::toROSMsg(cornerPointsSharp, cornerPointsSharpMsg);
cornerPointsSharpMsg.header.stamp = laserCloudMsg->header.stamp;
cornerPointsSharpMsg.header.frame_id = "/map";
pubCornerPointsSharp.publish(cornerPointsSharpMsg);sensor_msgs::PointCloud2 cornerPointsLessSharpMsg;
// 3.曲率稍微大一点的特征点
pcl::toROSMsg(cornerPointsLessSharp, cornerPointsLessSharpMsg);
cornerPointsLessSharpMsg.header.stamp = laserCloudMsg->header.stamp;
cornerPointsLessSharpMsg.header.frame_id = "/map";
pubCornerPointsLessSharp.publish(cornerPointsLessSharpMsg);sensor_msgs::PointCloud2 surfPointsFlat2;
// 4.平坦的点
pcl::toROSMsg(surfPointsFlat, surfPointsFlat2);
surfPointsFlat2.header.stamp = laserCloudMsg->header.stamp;
surfPointsFlat2.header.frame_id = "/map";
pubSurfPointsFlat.publish(surfPointsFlat2);
// 5.一般平坦的点
sensor_msgs::PointCloud2 surfPointsLessFlat2;
pcl::toROSMsg(surfPointsLessFlat, surfPointsLessFlat2);
surfPointsLessFlat2.header.stamp = laserCloudMsg->header.stamp;
surfPointsLessFlat2.header.frame_id = "/map";
pubSurfPointsLessFlat.publish(surfPointsLessFlat2);

2.2.激光里程计laserOdometry.cpp
(1)对特征点提取后的5个点云进行回调并存放到队列里,并同时转成pcl点云格式
(2)两次迭代,寻找角点约束和面点约束
(3)角点约束,首先进行雷达运动补偿,通过kdtee从上一帧中寻找距离当前帧角点最近的一个点p1,根据p1去找不同线上距离最近的点p2, 根据点到直线的垂线距离,构建残差方程给到ceres,可以求解出点到线的约束
(4)面点约束,首先进行雷达运动补偿,通过kdtee从上一帧中寻找距离当前帧角点最近的一个点p1,根据p1去找相同线上距离最近的点p2,根据p1去找不同线上距离最近的点p3,根据点到平面的距离,构建残差方程给到ceres,可以求解出点到面的约束
(5)通过两次迭代,进行ceres求解 得到最终的帧间约束结果,与之前的坐标约束进行求解,就得到了前端激光里程计
2.2.1运动补偿部分
激光雷达运动补偿:就是把所有的点补偿到某一时刻,这样就可以把本身过去100ms的点收集到一个时间点上去

void TransformToStart(PointType const *const pi, PointType *const po)
{// interpolation ratiodouble s;// 由于kitti数据集上的lidar已经做过了运动补偿,因此这里就不做具体补偿了if (DISTORTION)s = (pi->intensity - int(pi->intensity)) / SCAN_PERIOD;elses = 1.0; // s=1s说明全部补偿到点云结束的时刻// s = 1;//  所有点的操作方式都是一致的,相当于从结束时刻补偿到起始时刻//  这里相当于是一个匀速模型的假设Eigen::Quaterniond q_point_last = Eigen::Quaterniond::Identity().slerp(s, q_last_curr);Eigen::Vector3d t_point_last = s * t_last_curr;Eigen::Vector3d point(pi->x, pi->y, pi->z);Eigen::Vector3d un_point = q_point_last * point + t_point_last;po->x = un_point.x();po->y = un_point.y();po->z = un_point.z();po->intensity = pi->intensity;
}

根据反变换求出结束时刻的点坐标,附公式推解

void TransformToEnd(PointType const *const pi, PointType *const po)
{// undistort point firstpcl::PointXYZI un_point_tmp;// 把所有点补偿到起始时刻TransformToStart(pi, &un_point_tmp);//再 通过反变换的方式 将起始时刻坐标系下的点 转到 结束时刻坐标系下 Eigen::Vector3d un_point(un_point_tmp.x, un_point_tmp.y, un_point_tmp.z);//取出起始时刻坐标系下的点//q_last_curr  \ t_last_curr 是结束时刻坐标系转到起始时刻坐标系 的 旋转 和 平移  Eigen::Vector3d point_end = q_last_curr.inverse() * (un_point - t_last_curr);//通过反变换,求得转到 结束时刻坐标系下 的点坐标po->x = point_end.x();po->y = point_end.y();po->z = point_end.z();// Remove distortion time infopo->intensity = int(pi->intensity);
}

在这里插入图片描述
2.2.2确保5个点云都不为空

   // 首先确保5个消息都有,有一个队列为空都不行if (!cornerSharpBuf.empty() && !cornerLessSharpBuf.empty() &&!surfFlatBuf.empty() && !surfLessFlatBuf.empty() &&!fullPointsBuf.empty()){return false;}else{return true;}

2.2.3通过比较时间戳,判断是否是同一帧

 // 分别求出队列第一个时间timeCornerPointsSharp = cornerSharpBuf.front()->header.stamp.toSec();timeCornerPointsLessSharp = cornerLessSharpBuf.front()->header.stamp.toSec();timeSurfPointsFlat = surfFlatBuf.front()->header.stamp.toSec();timeSurfPointsLessFlat = surfLessFlatBuf.front()->header.stamp.toSec();timeLaserCloudFullRes = fullPointsBuf.front()->header.stamp.toSec();// 因为同一帧时间戳是相同的,因此这里比较是否是同一帧if (timeCornerPointsSharp != timeLaserCloudFullRes ||timeCornerPointsLessSharp != timeLaserCloudFullRes ||timeSurfPointsFlat != timeLaserCloudFullRes ||timeSurfPointsLessFlat != timeLaserCloudFullRes){printf("点云消息时间戳不同步!");return true;}else{return false;}

2.2.4传感器格式转换成点云格式

 // 分别将5个消息取出来,同时转成pcl的点云格式
mBuf.lock();
cornerPointsSharp->clear();
// 将第一根元素存放到cornerPointsSharp 就是当前的点云
pcl::fromROSMsg(*cornerSharpBuf.front(), *cornerPointsSharp);
// 移除前端的第一个元素 当前待处理的点云
cornerSharpBuf.pop();cornerPointsLessSharp->clear();
pcl::fromROSMsg(*cornerLessSharpBuf.front(), *cornerPointsLessSharp);
cornerLessSharpBuf.pop();surfPointsFlat->clear();
pcl::fromROSMsg(*surfFlatBuf.front(), *surfPointsFlat);
surfFlatBuf.pop();surfPointsLessFlat->clear();
pcl::fromROSMsg(*surfLessFlatBuf.front(), *surfPointsLessFlat);
surfLessFlatBuf.pop();laserCloudFullRes->clear();
pcl::fromROSMsg(*fullPointsBuf.front(), *laserCloudFullRes);
fullPointsBuf.pop();
mBuf.unlock();

2.2.5 点到线残差构建

 for (int i = 0; i < cornerPointsSharpNum; ++i)
{// 运动补偿TransformToStart(&(cornerPointsSharp->points[i]), &pointSel);// 在上一帧所有角点构成的kdtee中寻找距离当前帧最近的一个点kdtreeCornerLast->nearestKSearch(pointSel, 1, pointSearchInd, pointSearchSqDis);int closestPointInd = -1, minPointInd2 = -1;// 只有小于给定界限才认为是有效约束if (pointSearchSqDis[0] < DISTANCE_SQ_THRESHOLD){closestPointInd = pointSearchInd[0]; // 对应的最近距离约束的索引取出来// 找到其所对应的线束id 线束信息隐藏在intensity中int closestPointScanID = int(laserCloudCornerLast->points[closestPointInd].intensity);double minPointSqDis2 = DISTANCE_SQ_THRESHOLD;// 寻找角点,在刚刚角点的id上下分别继续寻找,目的是找到最近的角点,由于其按照约束进行排序,所以就是向上找for (int j = closestPointInd + 1; j < (int)laserCloudCornerLast->points.size(); ++j){// 不找同一根线束的if (int(laserCloudCornerLast->points[j].intensity) <= closestPointScanID)continue;// 要求找到的线束距离当前线束不能太远if (int(laserCloudCornerLast->points[j].intensity) > (closestPointScanID + NEARBY_SCAN))break;// 上一帧线的第2个点  到当前帧点的距离double 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 (pointSqDis < minPointSqDis2){minPointSqDis2 = pointSqDis;minPointInd2 = j;}}// 同样另外一个方向寻找角点for (int j = closestPointInd - 1; j >= 0; --j){if (int(laserCloudCornerLast->points[j].intensity) >= closestPointScanID)continue;if (int(laserCloudCornerLast->points[j].intensity) < (closestPointScanID - NEARBY_SCAN))break;double 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 (pointSqDis < minPointSqDis2){// 取出当前点和上一帧的两个角点minPointSqDis2 = pointSqDis;minPointInd2 = j;}}}// 最近点所在的线束if (minPointInd2 >= 0){// 当前点Eigen::Vector3d curr_point(cornerPointsSharp->points[i].x,cornerPointsSharp->points[i].y,cornerPointsSharp->points[i].z);// 距离当前点最近的上一帧的点Eigen::Vector3d last_point_a(laserCloudCornerLast->points[closestPointInd].x,laserCloudCornerLast->points[closestPointInd].y,laserCloudCornerLast->points[closestPointInd].z);// 距离上一帧点最近的不同线束上的第二个点 构成棱Eigen::Vector3d last_point_b(laserCloudCornerLast->points[minPointInd2].x,laserCloudCornerLast->points[minPointInd2].y,laserCloudCornerLast->points[minPointInd2].z);double s;if (DISTORTION)// 点在起始点到结束点一周中的进度s = (cornerPointsSharp->points[i].intensity - int(cornerPointsSharp->points[i].intensity)) / SCAN_PERIOD;elses = 1.0;// 残差项ceres::CostFunction *cost_function = LidarEdgeFactor::Create(curr_point, last_point_a, last_point_b, s);// 添加残差块 残差项 损失函数  待优化的变量problem.AddResidualBlock(cost_function, loss_function, para_q, para_t);corner_correspondence++;}
}

2.2.6 点到面残差构建

 for (int i = 0; i < surfPointsFlatNum; ++i)
{TransformToStart(&(surfPointsFlat->points[i]), &pointSel);// 先找上一帧距离当前帧最近的面点kdtreeSurfLast->nearestKSearch(pointSel, 1, pointSearchInd, pointSearchSqDis);int closestPointInd = -1, minPointInd2 = -1, minPointInd3 = -1;// 距离必须小于阈值if (pointSearchSqDis[0] < DISTANCE_SQ_THRESHOLD){// 取出找到的上一帧面点的索引closestPointInd = pointSearchInd[0];// 取出最近的面点在上一帧的那一条scan线上int closestPointScanID = int(laserCloudSurfLast->points[closestPointInd].intensity);double minPointSqDis2 = DISTANCE_SQ_THRESHOLD, minPointSqDis3 = DISTANCE_SQ_THRESHOLD;// 额外在寻找两个点,要求一个点和最近点同一个scan线 另一个点不同的scan// 按照增量方向寻找其它面点for (int j = closestPointInd + 1; j < (int)laserCloudSurfLast->points.size(); ++j){// 不能和当前找到的上一帧面点线束太远if (int(laserCloudSurfLast->points[j].intensity) > (closestPointScanID + NEARBY_SCAN))break;// 计算和当前帧该点距离double 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);// 如果是同一根scan且距离最近if (int(laserCloudSurfLast->points[j].intensity) <= closestPointScanID && pointSqDis < minPointSqDis2){minPointSqDis2 = pointSqDis;minPointInd2 = j;}// 如果是其它的线束点else if (int(laserCloudSurfLast->points[j].intensity) > closestPointScanID && pointSqDis < minPointSqDis3){minPointSqDis3 = pointSqDis;minPointInd3 = j;}}// 同样的方式 按照降序的方式去找这两个点for (int j = closestPointInd - 1; j >= 0; --j){if (int(laserCloudSurfLast->points[j].intensity) < (closestPointScanID - NEARBY_SCAN))break;double 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) >= closestPointScanID && pointSqDis < minPointSqDis2){minPointSqDis2 = pointSqDis;minPointInd2 = j;}else if (int(laserCloudSurfLast->points[j].intensity) < closestPointScanID && pointSqDis < minPointSqDis3){minPointSqDis3 = pointSqDis;minPointInd3 = j;}}// 如果找到的另外两个点是有效值,就取出它们的3d坐标if (minPointInd2 >= 0 && minPointInd3 >= 0){// 当前角点Eigen::Vector3d curr_point(surfPointsFlat->points[i].x,surfPointsFlat->points[i].y,surfPointsFlat->points[i].z);//    上一帧距离当前焦点最近的点Eigen::Vector3d last_point_a(laserCloudSurfLast->points[closestPointInd].x,laserCloudSurfLast->points[closestPointInd].y,laserCloudSurfLast->points[closestPointInd].z);Eigen::Vector3d last_point_b(laserCloudSurfLast->points[minPointInd2].x,laserCloudSurfLast->points[minPointInd2].y,laserCloudSurfLast->points[minPointInd2].z);Eigen::Vector3d last_point_c(laserCloudSurfLast->points[minPointInd3].x,laserCloudSurfLast->points[minPointInd3].y,laserCloudSurfLast->points[minPointInd3].z);double s;if (DISTORTION)s = (surfPointsFlat->points[i].intensity - int(surfPointsFlat->points[i].intensity)) / SCAN_PERIOD;elses = 1.0;// 构建点到面的约束ceres::CostFunction *cost_function = LidarPlaneFactor::Create(curr_point, last_point_a, last_point_b, last_point_c, s);problem.AddResidualBlock(cost_function, loss_function, para_q, para_t);plane_correspondence++;}}
}

2.2.7发布激光里程计和角点面点降频发送给后端

    // 发布雷达里程计结果
nav_msgs::Odometry laserOdometry;
laserOdometry.header.frame_id = "/map";
laserOdometry.child_frame_id = "/laser_odom";
laserOdometry.header.stamp = ros::Time().fromSec(timeSurfPointsLessFlat);
// 以四元数和平移向量发布出去
laserOdometry.pose.pose.orientation.x = q_w_curr.x();
laserOdometry.pose.pose.orientation.y = q_w_curr.y();
laserOdometry.pose.pose.orientation.z = q_w_curr.z();
laserOdometry.pose.pose.orientation.w = q_w_curr.w();
laserOdometry.pose.pose.position.x = t_w_curr.x();
laserOdometry.pose.pose.position.y = t_w_curr.y();
laserOdometry.pose.pose.position.z = t_w_curr.z();
pubLaserOdometry.publish(laserOdometry);
// 激光里程计路径
geometry_msgs::PoseStamped laserPose;
nav_msgs::Path laserPath;
laserPose.header = laserOdometry.header;
laserPose.pose = laserOdometry.pose.pose;
laserPath.header.stamp = laserOdometry.header.stamp;
laserPath.poses.push_back(laserPose);
laserPath.header.frame_id = "/map";
pubLaserPath.publish(laserPath);
// 一般角点
pcl::PointCloud<PointType>::Ptr laserCloudTemp = cornerPointsLessSharp;
// 上一帧的一般角点
cornerPointsLessSharp = laserCloudCornerLast;
laserCloudCornerLast = laserCloudTemp;//
laserCloudTemp = surfPointsLessFlat;
surfPointsLessFlat = laserCloudSurfLast;
laserCloudSurfLast = laserCloudTemp;laserCloudCornerLastNum = laserCloudCornerLast->points.size();
laserCloudSurfLastNum = laserCloudSurfLast->points.size();// kdtree设置当前帧,用来下一帧lidar odom使用
kdtreeCornerLast->setInputCloud(laserCloudCornerLast);
kdtreeSurfLast->setInputCloud(laserCloudSurfLast);
// 一定降频后给后端发送
if (frameCount % skipFrameNum == 0)
{frameCount = 0;// 一般角点sensor_msgs::PointCloud2 laserCloudCornerLast2;pcl::toROSMsg(*laserCloudCornerLast, laserCloudCornerLast2);laserCloudCornerLast2.header.stamp = ros::Time().fromSec(timeSurfPointsLessFlat);laserCloudCornerLast2.header.frame_id = "/camera";pubLaserCloudCornerLast.publish(laserCloudCornerLast2);// 面点sensor_msgs::PointCloud2 laserCloudSurfLast2;pcl::toROSMsg(*laserCloudSurfLast, laserCloudSurfLast2);laserCloudSurfLast2.header.stamp = ros::Time().fromSec(timeSurfPointsLessFlat);laserCloudSurfLast2.header.frame_id = "/camera";pubLaserCloudSurfLast.publish(laserCloudSurfLast2);// 整体点云sensor_msgs::PointCloud2 laserCloudFullRes3;pcl::toROSMsg(*laserCloudFullRes, laserCloudFullRes3);laserCloudFullRes3.header.stamp = ros::Time().fromSec(timeSurfPointsLessFlat);laserCloudFullRes3.header.frame_id = "/camera";pubLaserCloudFullRes.publish(laserCloudFullRes3);
}

2.3 scan-map 后端匹配 点云插入地图laserMapping.cpp

2.3.1 传感器数据类型转换成点云 odom转换为eigen类型

// 点云全部转换为pcl的数据格式laserCloudCornerLast->clear();pcl::fromROSMsg(*cornerLastBuf.front(), *laserCloudCornerLast);cornerLastBuf.pop();laserCloudSurfLast->clear();pcl::fromROSMsg(*surfLastBuf.front(), *laserCloudSurfLast);surfLastBuf.pop();laserCloudFullRes->clear();pcl::fromROSMsg(*fullResBuf.front(), *laserCloudFullRes);fullResBuf.pop();// lidar odom 的结果转成eigen数据格式q_wodom_curr.x() = odometryBuf.front()->pose.pose.orientation.x;q_wodom_curr.y() = odometryBuf.front()->pose.pose.orientation.y;q_wodom_curr.z() = odometryBuf.front()->pose.pose.orientation.z;q_wodom_curr.w() = odometryBuf.front()->pose.pose.orientation.w;t_wodom_curr.x() = odometryBuf.front()->pose.pose.position.x;t_wodom_curr.y() = odometryBuf.front()->pose.pose.position.y;t_wodom_curr.z() = odometryBuf.front()->pose.pose.position.z;odometryBuf.pop();// 考虑到实时性,就把队列其他的都pop出去,不然可能出现处理延时的情况while (!cornerLastBuf.empty()){cornerLastBuf.pop();printf("普通面点未清空  \n");}mBuf.unlock();

2.3.2 根据前端结果 得到后端的初始位姿

	// q_wodom_curr  t_wodom_curr 是雷达的odom// q_w_curr  t_w_curr是map坐标系下的位姿q_w_curr = q_wmap_wodom * q_wodom_curr;t_w_curr = q_wmap_wodom * t_wodom_curr + t_wmap_wodom;

2.3.3根据位置,获得全局地图的中心格子

	// 根据初始估计值计算寻找当前位姿在地图中的索引,一个各自边长是50m// 后端的地图本质上是一个以当前点为中心的一个栅格地图// 判断在全局栅格的哪一个栅格里,一个栅格是50m 栅格中心是25m// t_w_curr 是map坐标系下的位姿  centerCubeI网格中心centerCubeI = int((t_w_curr.x() + 25.0) / 50.0) + laserCloudCenWidth;centerCubeJ = int((t_w_curr.y() + 25.0) / 50.0) + laserCloudCenHeight;centerCubeK = int((t_w_curr.z() + 25.0) / 50.0) + laserCloudCenDepth;// 由于c语言的取整是向0取整 因此如-1.66 成为了-1 但-2才是正确的,因此这里自减1if (t_w_curr.x() + 25.0 < 0)centerCubeI--;if (t_w_curr.y() + 25.0 < 0)centerCubeJ--;if (t_w_curr.z() + 25.0 < 0)centerCubeK--;

2.3.4 根据机器人位置 更新全局地图范围 其它方向雷同

	// 如果当前centerCubeI栅格索引小于3,就说明当前点快接近地图边界了,需要进行调整,相当于地图整体往x正方向移动while (centerCubeI < 3){for (int j = 0; j < laserCloudHeight; j++){for (int k = 0; k < laserCloudDepth; k++){// laserCloudWidth是widtch方向栅格总大小21  laserCloudHeight   21int i = laserCloudWidth - 1;// 从x最大值开始pcl::PointCloud<PointType>::Ptr laserCloudCubeCornerPointer =// 角点laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k];pcl::PointCloud<PointType>::Ptr laserCloudCubeSurfPointer =// 面点laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k];// 整体右移for (; i >= 1; i--){laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] =laserCloudCornerArray[i - 1 + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k];laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] =laserCloudSurfArray[i - 1 + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k];}// 此时i=0,也就是最左边的格子赋值了之前最右边的格子laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] =laserCloudCubeCornerPointer;laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] =laserCloudCubeSurfPointer;// 该点云清零,由于是指针操作,相当于最左边的格子清空了laserCloudCubeCornerPointer->clear();laserCloudCubeSurfPointer->clear();}}// 索引右移centerCubeI++;laserCloudCenWidth++;}

2.3.5根据全局地图中心 ,提取局部地图每个格子在全局地图中的位置

	// 从当前格子为中心,选出地图中一定范围的点云 5*5*3 75个cube
for (int i = centerCubeI - 2; i <= centerCubeI + 2; i++) // 宽度方向
{for (int j = centerCubeJ - 2; j <= centerCubeJ + 2; j++) // 高度方向{for (int k = centerCubeK - 1; k <= centerCubeK + 1; k++) // 深度方向{if (i >= 0 && i < laserCloudWidth &&j >= 0 && j < laserCloudHeight &&k >= 0 && k < laserCloudDepth){// 把每个格子序号依次存到对应的索引//  i + laserCloudWidth * j  二维度平面位置// 每个格子在三维全局地图中的位置laserCloudValidInd[laserCloudValidNum] = i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k;// 局部地图格子数量laserCloudValidNum++;laserCloudSurroundInd[laserCloudSurroundNum] = i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k;laserCloudSurroundNum++;}}}
}

2.3.6当前帧 根据每个格子的在全局地图中的id,将局部地图的每个格子角点和面点分别叠加

	laserCloudCornerFromMap->clear();
laserCloudSurfFromMap->clear();
// 开始构建用来这一帧优化的小的局部地图 根据上面得到的索引进行叠加求和
for (int i = 0; i < laserCloudValidNum; i++)
{// 角点叠加// laserCloudValidInd[i] 每个格子的在全局地图中的位置*laserCloudCornerFromMap += *laserCloudCornerArray[laserCloudValidInd[i]];// 面点叠加*laserCloudSurfFromMap += *laserCloudSurfArray[laserCloudValidInd[i]];
}

2.3.7对当前帧角点面点下采样

	// 角点
downSizeFilterCorner.setInputCloud(laserCloudCornerLast);
downSizeFilterCorner.filter(*laserCloudCornerStack);
// 面点
downSizeFilterSurf.setInputCloud(laserCloudSurfLast);
downSizeFilterSurf.filter(*laserCloudSurfStack);

2.3.8点线残差构建 ,这里和前端有区别 ,通过最邻近的5个地图点进行构建协方差矩阵,通过协方差矩阵最大特征值与次大特征值判断是否存在直线

int corner_num = 0;// 构建角点相关约束for (int i = 0; i < laserCloudCornerStackNum; i++){// 实时角点 点坐标pointOri = laserCloudCornerStack->points[i];//  把雷达点转换到map坐标系pointAssociateToMap(&pointOri, &pointSel);// 局部地图中寻找和该点最近的5个点// pointSearchInd 5个点在局部地图中的索引kdtreeCornerFromMap->nearestKSearch(pointSel, 5, pointSearchInd, pointSearchSqDis);// 判断最远的点距离不能超过1m,否则就是无效约束if (pointSearchSqDis[4] < 1.0){std::vector<Eigen::Vector3d> nearCorners;Eigen::Vector3d center(0, 0, 0);for (int j = 0; j < 5; j++){Eigen::Vector3d tmp(laserCloudCornerFromMap->points[pointSearchInd[j]].x,laserCloudCornerFromMap->points[pointSearchInd[j]].y,laserCloudCornerFromMap->points[pointSearchInd[j]].z);// 5个点坐标的叠加center = center + tmp;// 转存这5个点给nearCornersnearCorners.push_back(tmp);}// 计算这5个点的均值center = center / 5.0;Eigen::Matrix3d covMat = Eigen::Matrix3d::Zero();// 构建协方差矩阵,5个变量的变化趋势for (int j = 0; j < 5; j++){// 每个点与均值之间的偏移量Eigen::Matrix<double, 3, 1> tmpZeroMean = nearCorners[j] - center;// 该点与该点转置的外积 当前矩阵与当前矩阵的转置 得到3*3的矩阵,当前点的协方差矩阵covMat = covMat + tmpZeroMean * tmpZeroMean.transpose();}// 进行特征值分解Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> saes(covMat);// 根据特征值分解情况看看是不是真正的线特征// 特征向量就是线特征的方向 Eigen::Vector3d unit_direction = saes.eigenvectors().col(2);Eigen::Vector3d curr_point(pointOri.x, pointOri.y, pointOri.z);// 最大特征值大于次大特征值的3倍认为是线特征if (saes.eigenvalues()[2] > 3 * saes.eigenvalues()[1]){Eigen::Vector3d point_on_line = center;Eigen::Vector3d point_a, point_b;// 根据拟合出来的线特征方向,以平均点为中心构建两个虚拟点//从中心点沿着方向向量向两端移动0.1m,使用两个点代替一条直线,//这样计算点到直线的距离的形式就跟laserOdometry相似point_a = 0.1 * unit_direction + point_on_line;point_b = -0.1 * unit_direction + point_on_line;// 构建约束 和lidar odom 约束一致ceres::CostFunction *cost_function = LidarEdgeFactor::Create(curr_point, point_a, point_b, 1.0);problem.AddResidualBlock(cost_function, loss_function, parameters, parameters + 4);corner_num++;}}}

2.3.9点面残差构建 这里与前端有区别 面的构建通过 最临近当前角点的5个点 通过构建超定方程 qr分解获得的 法向量与点之间的关系

int surf_num = 0;
// 构建面点的约束
for (int i = 0; i < laserCloudSurfStackNum; i++)
{// 实时面点坐标pointOri = laserCloudSurfStack->points[i];// 把雷达点坐标转到map坐标系pointAssociateToMap(&pointOri, &pointSel);// 局部地图中搜索距离该点最近的5个点kdtreeSurfFromMap->nearestKSearch(pointSel, 5, pointSearchInd, pointSearchSqDis);Eigen::Matrix<double, 5, 3> matA0;Eigen::Matrix<double, 5, 1> matB0 = -1 * Eigen::Matrix<double, 5, 1>::Ones();// 构建面点方程ax+by+cz+d=0// 通过构建一个超定方程求解这个平面方程// 判断最远的点距离不能超过1m,否则就是无效约束if (pointSearchSqDis[4] < 1.0){for (int j = 0; j < 5; j++){matA0(j, 0) = laserCloudSurfFromMap->points[pointSearchInd[j]].x;matA0(j, 1) = laserCloudSurfFromMap->points[pointSearchInd[j]].y;matA0(j, 2) = laserCloudSurfFromMap->points[pointSearchInd[j]].z;}// 通过eigen接口求解该方程,解就是这个平面的法向量// 豪斯霍尔德变换Eigen::Vector3d norm = matA0.colPivHouseholderQr().solve(matB0);double negative_OA_dot_norm = 1 / norm.norm();// 法向量归一化norm.normalize();bool planeValid = true;// 根据求出来的平面方程进行校验 看看是不是符合平面约束for (int j = 0; j < 5; j++){// 这里相当于求解点到平面的距离if (fabs(norm(0) * laserCloudSurfFromMap->points[pointSearchInd[j]].x +norm(1) * laserCloudSurfFromMap->points[pointSearchInd[j]].y +norm(2) * laserCloudSurfFromMap->points[pointSearchInd[j]].z + negative_OA_dot_norm) > 0.2){planeValid = false; // 点如果距离平面过远,就认为这是一个拟合的不好的平面break;}}Eigen::Vector3d curr_point(pointOri.x, pointOri.y, pointOri.z);// 如果平面有效就构建平面约束if (planeValid){// 利用平面方程构建约束 和前端构建形式稍有不同ceres::CostFunction *cost_function = LidarPlaneNormFactor::Create(curr_point, norm, negative_OA_dot_norm);problem.AddResidualBlock(cost_function, loss_function, parameters, parameters + 4);surf_num++;}

2.2.10通过反变换更新odom-》map的tf关系

// q_wmap_wodom t_wmap_wodom是map到odom之间的关系
q_wmap_wodom = q_w_curr * q_wodom_curr.inverse();
t_wmap_wodom = t_w_curr - q_wmap_wodom * t_wodom_curr;

2.2.11将优化后的当前帧角点加入到局部地图,面点雷同

	for (int i = 0; i < laserCloudCornerStackNum; i++)
{// 该点根据位姿投到地图坐标系pointAssociateToMap(&laserCloudCornerStack->points[i], &pointSel);// 算出这个点所在的格子在全局地图中的索引int cubeI = int((pointSel.x + 25.0) / 50.0) + laserCloudCenWidth;int cubeJ = int((pointSel.y + 25.0) / 50.0) + laserCloudCenHeight;int cubeK = int((pointSel.z + 25.0) / 50.0) + laserCloudCenDepth;// 同样负数做对应的操作if (pointSel.x + 25.0 < 0)cubeI--;if (pointSel.y + 25.0 < 0)cubeJ--;if (pointSel.z + 25.0 < 0)cubeK--;// 如果超过边界的话就算了if (cubeI >= 0 && cubeI < laserCloudWidth &&cubeJ >= 0 && cubeJ < laserCloudHeight &&cubeK >= 0 && cubeK < laserCloudDepth){// 当前格子在全局地图中的索引int cubeInd = cubeI + laserCloudWidth * cubeJ + laserCloudWidth * laserCloudHeight * cubeK;// 将当前帧点云角点插入到角点格子中laserCloudCornerArray[cubeInd]->push_back(pointSel);}
}

2.2.12把当前帧涉及到的局部地图下采样

	for (int i = 0; i < laserCloudValidNum; i++){int ind = laserCloudValidInd[i];pcl::PointCloud<PointType>::Ptr tmpCorner(new pcl::PointCloud<PointType>());downSizeFilterCorner.setInputCloud(laserCloudCornerArray[ind]);downSizeFilterCorner.filter(*tmpCorner);laserCloudCornerArray[ind] = tmpCorner;pcl::PointCloud<PointType>::Ptr tmpSurf(new pcl::PointCloud<PointType>());downSizeFilterSurf.setInputCloud(laserCloudSurfArray[ind]);downSizeFilterSurf.filter(*tmpSurf);laserCloudSurfArray[ind] = tmpSurf;}

2.2.13局部地图发布

	//  每隔5帧对外发布一下
if (frameCount % 5 == 0)
{laserCloudSurround->clear();// 把当前帧相关的局部地图发布出去  laserCloudSurroundNum 坐标点的索引数目for (int i = 0; i < laserCloudSurroundNum; i++){int ind = laserCloudSurroundInd[i];*laserCloudSurround += *laserCloudCornerArray[ind];*laserCloudSurround += *laserCloudSurfArray[ind];}sensor_msgs::PointCloud2 laserCloudSurround3;pcl::toROSMsg(*laserCloudSurround, laserCloudSurround3);laserCloudSurround3.header.stamp = ros::Time().fromSec(timeLaserOdometry);laserCloudSurround3.header.frame_id = "/map";pubLaserCloudSurround.publish(laserCloudSurround3);
}

2.2.14全局地图发布

// 每隔20帧发布一次全局地图
if (frameCount % 20 == 0)
{// 21*21*11=4851pcl::PointCloud<PointType> laserCloudMap;for (int i = 0; i < 4851; i++){laserCloudMap += *laserCloudCornerArray[i];laserCloudMap += *laserCloudSurfArray[i];}sensor_msgs::PointCloud2 laserCloudMsg;pcl::toROSMsg(laserCloudMap, laserCloudMsg);laserCloudMsg.header.stamp = ros::Time().fromSec(timeLaserOdometry);laserCloudMsg.header.frame_id = "/map";pubLaserCloudMap.publish(laserCloudMsg);
}

2.2.15全局位姿,轨迹 tf 发布

	// 发布当前位姿
nav_msgs::Odometry odomAftMapped;
odomAftMapped.header.frame_id = "/map";
odomAftMapped.child_frame_id = "/laser_link";
odomAftMapped.header.stamp = ros::Time().fromSec(timeLaserOdometry);
odomAftMapped.pose.pose.orientation.x = q_w_curr.x();
odomAftMapped.pose.pose.orientation.y = q_w_curr.y();
odomAftMapped.pose.pose.orientation.z = q_w_curr.z();
odomAftMapped.pose.pose.orientation.w = q_w_curr.w();
odomAftMapped.pose.pose.position.x = t_w_curr.x();
odomAftMapped.pose.pose.position.y = t_w_curr.y();
odomAftMapped.pose.pose.position.z = t_w_curr.z();
pubOdomAftMapped.publish(odomAftMapped);
// 发布当前轨迹
geometry_msgs::PoseStamped laserAfterMappedPose;
laserAfterMappedPose.header = odomAftMapped.header;
laserAfterMappedPose.pose = odomAftMapped.pose.pose;
laserAfterMappedPath.header.stamp = odomAftMapped.header.stamp;
laserAfterMappedPath.header.frame_id = "/map";
laserAfterMappedPath.poses.push_back(laserAfterMappedPose);
pubLaserAfterMappedPath.publish(laserAfterMappedPath);// 发布tf
static tf::TransformBroadcaster br;
tf::Transform transform;
tf::Quaternion q;
transform.setOrigin(tf::Vector3(t_w_curr(0),t_w_curr(1),t_w_curr(2)));
q.setW(q_w_curr.w());
q.setX(q_w_curr.x());
q.setY(q_w_curr.y());
q.setZ(q_w_curr.z());
transform.setRotation(q);
br.sendTransform(tf::StampedTransform(transform, odomAftMapped.header.stamp, "/map", "/laser_link"));

相关文章:

3d激光slam建图与定位(2)_aloam代码阅读

1.常用的几种loam算法 aloam 纯激光 lego_loam 纯激光 去除了地面 lio_sam imu激光紧耦合 lvi_sam 激光视觉 2.代码思路 2.1.特征点提取scanRegistration.cpp&#xff0c;这个文件的目的是为了根据曲率提取4种特征点和对当前点云进行预处理 输入是雷达点云话题 输出是 4种特征点…...

Java 8 新特性——Lambda 表达式(2)

一、Java Stream API Java Stream函数式编程接口最初在Java 8中引入&#xff0c;并且与 lambda 一起成为Java开发里程碑式的功能特性&#xff0c;它极大的方便了开放人员处理集合类数据的效率。 Java Stream就是一个数据流经的管道&#xff0c;并且在管道中对数据进行操作&…...

MES管理系统中常用的数据模型有哪些

在MES管理系统项目中&#xff0c;数据建模对于生产过程的监控、分析和管理具有至关重要的作用。本文将介绍一些常见的MES管理系统数据建模方面&#xff0c;并阐述它们在生产过程中的重要性和应用。 1、产品数据模型是MES系统中的基础模块之一。它涵盖了产品的基本信息、规格和属…...

ARM DIY(三)板载串口和 LCD 调试

前言 今天焊接两大关键输入输出设备&#xff1a;串口和屏幕。 串口 串口部分使用 CP2102N 芯片&#xff08;USB 转 TTL&#xff09;&#xff0c;这样用一根数据线连接板子和 PC 就可以直接调试了。 焊接 CP2102 和 Type C 上电调试&#xff0c;串口可以正常输入输出。 看来…...

计算机网络-笔记-第一章-计算机网络概述

目录 一、第一章——计算机网络概述 1、因特网概述 &#xff08;1&#xff09;网络、互联网、因特网 &#xff08;2&#xff09;因特网发展的三个阶段 &#xff08;3&#xff09;因特网服务的提供者&#xff08;ISP&#xff09; &#xff08;4&#xff09;因特网标准化工…...

Oracle-day4:分组查询(带条件)、DDL建表、约束、主从表

一、内容回顾 /*------------------内容回顾------------------------上周内容回顾--1、单表的基础查询--A、select * from emp;--B、列的运算 --数字类型运算 - * /--函数运算 mod ceil floor round upper lower--C、取别名--列、表达书取别名--*表示所有的列和列同时存在时…...

(详解)数据结构-----------栈与队列 c语言实现

本章将会详细讲解以下知识点&#xff1a; 目录 一&#xff1a;栈 1&#xff1a;栈的定义&#xff0c;栈的特点 2&#xff1a;用什么结构来实现栈与原因的分析? 3: (超详解)栈的常用接口并且附上测试用例 二:队列 1:队列的定义&#xff0c;队列的特点 2&#xff1a;用什么结…...

前端文件、图片直传OOS、分片上传、el-upload上传(vue+elementUI)

前言&#xff1a;基于天翼云的面相对象存储(Object-Oriented Storage&#xff0c;OOS),实现小文件的直接上传&#xff0c;大文件的分片上传。 开发文档地址&#xff1a;网址 上传之前的相关操作&#xff1a;注册账户&#xff0c;创建 AccessKeyId 和 AccessSecretKey之后&…...

java自动登录 selenium 自动登录并获取cookie

选择操作网页 我用的edge&#xff0c;谷歌我的版本太高没有对应的驱动… 下载Edge的驱动程序&#xff0c;直接解压就好里面只有一个.exe文件 https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ 复制即用&#xff0c;看注释 import com.alibaba.fastjs…...

vue中 computed()方法详解

在Vue中&#xff0c;computed是一种计算属性&#xff0c;它用于定义一个属性&#xff0c;该属性的值是根据其他属性的值计算而来的。computed属性的值会被缓存&#xff0c;只有当依赖的属性发生变化时&#xff0c;才会重新计算。 computed属性可以在Vue实例的computed选项中定…...

在服务器上搭建Jenkins

目录 1.服务器要求 2.官方文档 3.在服务器上下载Jenkins 3.1 下载war包 3.2 将war包上传到服务器的一个目录下 3.3 启动jenkins 3.3.1 jdk版本升级 1&#xff09;下载jdk17 2&#xff09;解压到当前文件夹 3&#xff09;配置路径 4.jenkins配置 4.1 填写初始密码&a…...

全面解析MES系统中的报工操作

一、报工操作的定义&#xff1a; 报工操作是指在生产过程中&#xff0c;操作员通过MES系统记录和提交生产工序的相关信息&#xff0c;如工时、产量、质量等。报工操作将生产过程中的实际情况反馈给MES系统&#xff0c;实现生产数据的实时采集和记录。 二、报工操作的流程&…...

Harbor 私有仓库迁移

文章目录 一.私有仓库迁移的介绍1.为何要对Harbor 私有仓库的迁移2.Harbor 私有仓库的迁移特点3. Harbor 私有仓库的迁移注意要点 二.私有仓库迁移配置1.源Harbor配置&#xff08;192.168.198.11&#xff09;&#xff08;1&#xff09;接着以下操作查看容器状况及是否可以登录 …...

制造业物联网革命:智慧工厂数据采集与远程监控管理

智慧工厂是指运用现代信息技术和物联网技术&#xff0c;实现制造业生产过程的智能数字化。智慧工厂的工业设备不仅能够自动化运行&#xff0c;还可以通过网络技术帮助企业实现数据采集、远程监控与管理。4G工业网关便成为了智慧工厂通讯的重要组成部分&#xff0c;起到了连接工…...

软考A计划-网络工程师-复习背熟-网络管理和计算机基础知识

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…...

springBoot打印精美logo

文章目录 &#x1f412;个人主页&#x1f3c5;JavaEE系列专栏&#x1f4d6;前言&#xff1a;&#x1f380;文本logo &#x1f412;个人主页 &#x1f3c5;JavaEE系列专栏 &#x1f4d6;前言&#xff1a; 本篇博客主要以提供springBoot打印精美logo &#x1f380;文本logo ??…...

kali开启SSH服务(简单无比)

我会一直陪着你 1.切换到管理员用户&#xff1a;2.启动SSH服务3.要在Kali Linux上启用SSH服务并修改配置文件&#xff0c;你可以按照以下步骤进行操作&#xff1a;4.查看SSH服务状态是否正常运行&#xff0c;命令为&#xff1a;注意&#xff1a;配置文件有些地方不同&#xff0…...

Ubuntu20.04如何更换国内源-阿里云源

1.备份源文件 cp /etc/apt/sources.list /etc/apt/sources.list.bak 2.打开源文件&#xff0c;注释默认的源 vim /etc/apt/sources.list ## 注释原本内容 # deb http://mirrors.ivolces.com/ubuntu/ focal main restricted universe multiverse # deb-src http://mirrors.ivolc…...

goland设置

1、go file设置 file->setting->Editor->File and Code Templates->Go File package ${GO_PACKAGE_NAME} /** * description: * author:${USER} * date:${YEAR}/${MONTH}/${DAY} ${HOUR}:${MINUTE} * Versio…...

2023年Java核心技术第十篇(篇篇万字精讲)

目录 十九 . 一个线程两次调用start&#xff08;&#xff09;方法会出现什么情况&#xff1f;线程的生命周期和状态转移。 19.1 典型回答 19.1.1 线程生命周期&#xff1a; 19.1.2 计时等待详细解释&#xff1a; 19.2 深入扩展考察 19.2.1 线程是什么&#xff1f; 19.2.2 Green…...

分享一篇关于如何使用BootstrapVue的入门指南

你想轻松地创建令人惊叹且响应式的在线应用程序吗&#xff1f;使用BootstrapVue&#xff0c;您可以快速创建美观且用户友好的界面。这个开源工具包是基于Vue.js和Bootstrap构建的&#xff0c;非常适合开发现代Web应用程序。本文将介绍其基础知识&#xff0c;让您可以开始使用这…...

【1day】复现Cellular Router命令执行漏洞

目录 一、漏洞描述 二、影响版本 三、资产测绘 四、漏洞复现 一、漏洞描述 移动路由器(Cellular Router)是一种利用移动网络提供无线互联网连接的设备。它们类似于传统路由器,但不同于使用有线连接(如以太网或DSL)...

【Torch API】pytorch 中repeat_interleave函数详解

torch.repeat_interleave(input, repeats, dimNone) → Tensor Repeat elements of a tensor. Parameters input (Tensor) – the input tensor. repeats (Tensor or int) – The number of repetitions for each element. repeats is broadcasted to fit the shape of the …...

TDesign表单rules通过函数 实现复杂逻辑验证输入内容

Element ui 中 我们可以通过validator 绑定函数来验证一些不在表单model中的值 又或者处理一下比较复杂的判断逻辑 TDesign也有validator 但比较直观的说 没有Element那么好用 这里 我们给validator绑定了我们自己的checkAge函数 这个函数中 只有一个参数 value 而且 如果你的…...

springgateway网关修改响应后,部分中文乱码问题

原因 是因为响应体过大&#xff0c;开启了压缩&#xff0c;数据分段进行响应得&#xff0c;导致处理返回体得时候乱码 解决方式 Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request exchange.getR…...

微信开发之一键发布群公告的技术实现

简要描述&#xff1a; 设置群公告 请求URL&#xff1a; http://域名地址/setChatRoomAnnouncement 请求方式&#xff1a; POST 请求头Headers&#xff1a; Content-Type&#xff1a;application/jsonAuthorization&#xff1a;login接口返回 参数&#xff1a; 参数名必…...

R语言和Python用泊松过程扩展:霍克斯过程Hawkes Processes分析比特币交易数据订单到达自激过程时间序列...

全文下载链接&#xff1a;http://tecdat.cn/?p25880 本文描述了一个模型&#xff0c;该模型解释了交易的聚集到达&#xff0c;并展示了如何将其应用于比特币交易数据。这是很有趣的&#xff0c;原因很多。例如&#xff0c;对于交易来说&#xff0c;能够预测在短期内是否有更多…...

自动化运维:Ansible脚本之playbook剧本

目录 一、理论 1.playbooks 2.YAML 3.使用ansible批量安装apache服务 4.定义、引用变量 5.指定远程主机sudo切换用户 6.when条件判断 7.迭代 8.Templates 模块 9.tags 模块 10.Roles 模块 二、实验 1.使用ansible批量安装apache服务 2.定义、引用变量…...

基于角色访问控制-RBAC(Role-Based Access Control)

1.RBAC简介 RBAC&#xff08;Role-Based Access Control&#xff09;是一种基于角色的访问控制模型&#xff0c;它是一种安全策略&#xff0c;用于限制系统中用户对资源的访问权限。RBAC模型的核心概念是用户角色和资源访问权限。 在角色访问控制中&#xff0c;首先需要定义系…...

springboot项目实现断点续传

java代码 package com.ruoyi.web.upload.controller; import com.ruoyi.web.upload.dto.FileChunkDTO; import com.ruoyi.web.upload.dto.FileChunkResultDTO; import com.ruoyi.web.upload.result.Result; import com.ruoyi.web.upload.service.IUploadService; import org.s…...

解析经典面试题:for 循环中的 let var

更多文章可以看看我的博客&#xff1a;https://icheng.github.io/ 题目 for循环中&#xff0c;使用 var 或 let 声明 i 变量&#xff0c;会得到不同的结果 var arr []; for (var i 0; i < 2; i) {arr[i] function () {console.log(i);} } arr[0](); arr[1]();输出&…...

CSS按钮-跑马灯边框

思路很简单&#xff0c;实现方法有很多很多。但是大体思路与实现方法都类似&#xff1a;渐变色 动画&#xff0c;主要区别在动画的具体实现 0、HTML 结构 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><titl…...

【PCIE系统学习】Gen1/2Gen3/4 symobl与OrderSet概念对比

这个专栏要干的事:学习PCIE理论知识,学会PCIE IP/VIP的使用。可以达到上手做项目,而不是空有理论 适合谁看:正在学习PCIE 设计验证,但学的过于零散。想有对比的系统的学习。 低价收费的目的:抵制胡乱传播转载现象。 版本控制:便于增加内容以及勘误 版本说明v20230829 …...

C++ Qt 中QMimeDatabase类详细介绍以及应用场景

C Qt 中QMimeDatabase类详细介绍以及应用场景 文章目录 C Qt 中QMimeDatabase类详细介绍以及应用场景一、QMimeDatabase类是什么&#xff1f;二、QMimeDatabase类中的关键功能和特点三、QMimeDatabase的用法四、QMimeDatabase的应用场景 一、QMimeDatabase类是什么&#xff1f;…...

深度学习7:生成对抗网络 – Generative Adversarial Networks | GAN

生成对抗网络 – GAN 是最近2年很热门的一种无监督算法&#xff0c;他能生成出非常逼真的照片&#xff0c;图像甚至视频。我们手机里的照片处理软件中就会使用到它。 目录 生成对抗网络 GAN 的基本原理 大白话版本 非大白话版本 第一阶段&#xff1a;固定「判别器D」&#x…...

R语言空气污染数据的地理空间可视化和分析:颗粒物2.5(PM2.5)和空气质量指数(AQI)...

原文链接&#xff1a;http://tecdat.cn/?p23800 由于空气污染对公众健康的不利影响&#xff0c;人们一直非常关注。世界各国的环境部门都通过各种方法&#xff08;例如地面观测网络&#xff09;来监测和评估空气污染问题&#xff08;点击文末“阅读原文”获取完整代码数据&…...

实现excel导出最简单方式

今天来记录一下导出excel的实现方式&#xff0c;导出的格式是xlsx的文件。 这里用到的是hutool的工具包&#xff0c;依赖如下&#xff1a; <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.3.5&…...

【每日一题Day310】LC1654到家的最少跳跃次数 | BFS

到家的最少跳跃次数【LC1654】 有一只跳蚤的家在数轴上的位置 x 处。请你帮助它从位置 0 出发&#xff0c;到达它的家。 跳蚤跳跃的规则如下&#xff1a; 它可以 往前 跳恰好 a 个位置&#xff08;即往右跳&#xff09;。它可以 往后 跳恰好 b 个位置&#xff08;即往左跳&…...

[Android AIDL] --- AIDL原理简析

上一篇文章已经讲述了如何在Android studio中搭建基于aidl的cs模型框架&#xff0c;只是用起来了&#xff0c;这次对aidl及cs端如何调用的原理进行简单分析 1 创建AIDL文件 AIDL 文件可以分为两类。 一类是用来定义接口方法&#xff0c;声明要暴露哪些接口给客户端调用&#…...

企业的固定资产管理怎么操作

一家拥有多台大型设备的工厂&#xff0c;这些设备需要定期进行保养和维护&#xff0c;以确保其正常运转。而企业内部员工由于专业知识和技能的不同&#xff0c;需要分工协作才能更好地完成各项工作任务。因此&#xff0c;在设备资产管理方面&#xff0c;如何实现高效、便捷、透…...

Rust 进阶学习

Rust 进阶学习 文章目录 Rust 进阶学习所有权作用域移动和克隆涉及函数的所有权机制涉及参数的所有权涉及返回值的所有权 引用和租借可变引用 枚举类枚举成员的属性枚举匹配 结构体结构体方法结构体关联函数 错误处理不可恢复错误可恢复错误 Rust代码组织管理Module默认的Modul…...

保护网站安全:学习蓝莲花的安装和使用,复现跨站脚本攻击漏洞及XSS接收平台

这篇文章旨在用于网络安全学习&#xff0c;请勿进行任何非法行为&#xff0c;否则后果自负。 环境准备 一、XSS基础 1、反射型XSS 攻击介绍 原理 攻击者通过向目标网站提交包含恶意脚本的请求&#xff0c;然后将该恶意脚本注入到响应页面中&#xff0c;使其他用户在查看…...

Redis——如何解决redis穿透、雪崩、击穿问题

目录 一、查询商品信息的常规代码示例二、缓存击穿2.1、缓存击穿的理解2.2、缓存击穿的解决方案2.3、解决缓存击穿的代码示例 三、缓存雪崩3.1、缓存雪崩的理解3.2、缓存雪崩的解决方案3.2.1、缓存集中过期的情况3.2.2、缓存服务器宕机的情况3.2.3、缓存服务器断电的情况 3.3、…...

MySQL一行记录是如何存储的?

目录 MySQL的数据存放在哪个文件&#xff1f; 表空间文件的结构是怎么样的&#xff1f; 1、行&#xff08;row&#xff09; 2、页&#xff08;page&#xff09; 3、区&#xff08;extent&#xff09; 4、段&#xff08;segment&#xff09; InnoDB 行格式有哪些&#xf…...

[element-ui] el-tree全部展开与收回

shrinkTreeNode () {// 改变一个全局变量this.treeStatus !this.treeStatus;// 改变每个节点的状态this.changeTreeNodeStatus(this.$refs.attrList.store.root); },// 改变节点的状态 changeTreeNodeStatus (node) {node.expanded this.treeStatus;for (let i 0; i < no…...

git 统计(命令)

查询某人某个时刻提交了多少代码 added 添加代码 removed 删除代码 total 总代码 git log --author刘俊秦 --since2023-08-01 00:00:00 --until2023-08-23 23:00:00 --prettytformat: --numstat | awk { add $1; subs $2; loc $1 - $2 } END { printf "added lines: %s…...

斐波那契1(矩阵快速幂加速递推,斐波那契前n项平方和)

链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 来源&#xff1a;牛客网 Keven 特别喜欢斐波那契数列&#xff0c;已知 fib11fib_11fib1​1&#xff0c;fib21fib_21fib2​1&#xff0c;对于 n>3n>3n>3&#xff0c;fibnfibn−2fibn−1fib_{n}fib_{n-2}fib_{n…...

minikube mac 启动

系统信息如下 最开始使用的minikube是1.22.0版本&#xff0c;按照如下命令启动&#xff1a; minikube start --memory7851 --cpus4 --image-mirror-countrycn遇到了下面一些问题&#xff1a; 1、拉取coredns:v1.8.0镜像失败 Error response from daemon: manifest for regis…...

从零开始学习 Java:简单易懂的入门指南之查找算法及排序算法(二十)

查找算法及排序算法 常见的七种查找算法&#xff1a;1. 基本查找2. 二分查找3. 插值查找4. 斐波那契查找5. 分块查找6. 哈希查找7. 树表查找 四种排序算法&#xff1a;1. 冒泡排序1.1 算法步骤1.2 动图演示1.3 代码示例 2. 选择排序2.1 算法步骤2.2 动图演示 3. 插入排序3.1 算…...

非煤矿山风险监测预警算法 yolov8

非煤矿山风险监测预警算法通过yolov8网络模型深度学习算法框架&#xff0c;非煤矿山风险监测预警算法在煤矿关键地点安装摄像机等设备利用智能化视频识别技术&#xff0c;能够实时分析人员出入井口的情况&#xff0c;人数变化并检测作业状态。YOLO的结构非常简单&#xff0c;就…...