• slam 点云退化


    Lidar Slam退化问题分析报告

    摘要:激光雷达在空旷区域存在点云采集数据较少,特征无法对机器人的位置起到约束的作用,考虑LIW融合对最终定位的影响。当错误的LIW即发生退化,位姿输出不准存在较大误差,如果未检测到退化,协方差没有及时调整,会导致融合模块位置发生异常。退化检测精度对融合位姿输出影响较大。

    • 点云PCA分析(Principle Component Analysis主成分分析

    PCA(Principal Components Analysis)主成分分析,应用于点云预处理,平面检测,法向量求解,降维、分类,解压(升维),用PCA对点云中的点分类,地面点,墙面点,物体上的点等,是一种数据降维技术,用于数据预处理。PCA是将三维投影到某个面上,用于发现其主要方向。面的选择依据是选择尽量使得点的分布方差最大,分布较散的面,方差越大,它的信息量越大。

    1.1点云特征形式

    点云特征可以分为一维线特征(b)、二维面特征(c)、三维空间特征(a)三种情况,如下图所示。

    1.2 点云特征对定位的影响

    定位一般包含位置(x,y,z)与姿态(roll,pitch,yaw)的估计,激光雷达采集到的点云数据后,与上一帧数据进行匹配,计算出雷达的相对位姿变化。线特征无法对旋转进行约束,面特征无法对平行与该平面的位置进行约束,若想满足对车的位姿进行全方位的约束就需要为空间特征。

    1.3空间特征评估标准

    对于直线特征为直线的斜率及截距;对于平面特征,表征平面向量的方法为坐标原点的及法向量;对于三维空间特征相应的以质心和三个法向量表征,即原点和空间的三个基坐标。点云的法向量用特征向量表示。特征向量最直观的解释之一是它总是指向数据方差最大的方向。更准确地说,第一特征向量是数据方差最大的方向,第二特征向量是与第一特征向量垂直的方向上数据方差最大的方向,第三特征向量是与第一和第二特征向量垂直的方向上数据方差最大的方向,类似于空间坐标系下的基坐标系,以此类推。

    假设在平面坐标系中,每个数据样本都是可以用坐标x、y表示的二维点。这些数据样本的协方差矩阵的特征向量是u和v。较长的u是第一特征向量,较短的v是第二特征向量。特征值的大小用箭头的长度表示。我们可以看到,第一个特征向量(从数据的平均值)指向欧几里德空间中数据方差最大的方向,第二个特征向量跟第一特征向量是垂直的。

    下图是二维空间的一个例子:

    三维空间中的特征向量就比较复杂,如图所示:

    数据在某个基上的投影值(也是在这个基上的坐标值)越分散,方差越大,这个基保留的信息也就越多,信息量保存能力最大的基向量一定是的协方差矩阵的特征向量,并且这个特征向量保存的信息量就是它对应的特征值。

    • 实际测试

    为验证退化检测的有效性,运行了两组数据并进行了分析。

    2.1非退化场景测试

    场景一特征比较丰富,特征较多,场景如下图所示:

    采用此方法检测结果如下:

    根据特征值与特征向量的结果,将特征值按从大到小排序特征值大的表示特征较多。其中橙色与黄色的为第一与第二特征值(图中为了显示缩小了1000倍),蓝色为特征值最小的取值范围,设置退化的阈值为sort_degen(2)<500且sort_degen(1)/sort_degen(2)>200,当最小方向的特征值过小且第二方向的特征值大于其200倍时认为发散(主要区分只有一个平面特征的情况,如单独无人车的地面特征无法进行位置估计).

    此时的Z方向最高为3米,三个方向特征值为57046、31874、1584,点云分布及特征向量如上图所示。

    以RTK为真值,计算相邻两帧的运动的距离l,与融合相邻两帧的距离为l1,计算l-l1的插值,如下图所示:

    从图中可以看出,rtk与fusion的结果,两帧之间的变化范围在0.3-0.4m,两个结果匹配较高。

    2.2退化场景测试

    退化场景二在场景的右侧点云较少,退化严重。

    matlab显示的结果如下图所示,圆圈标注部分为检测到退化部分。

    退化时点云情况如下:

    三个方向的特征值分别为:43307、12635、52.退化较轻,z方向点较少,最高1m。

    场景三:场景中的特征值分别为:3787、2399、2.95,不满足约束条件。

    下图为添加了退化检测的融合方案,从rtk与融合轨迹对比,未发现由退化引起的位姿输出异常的情况。

    参考:https://github.com/TixiaoShan/LIO-SAM.git

    https://www.cnblogs.com/chenlinchong/p/14907439.html

    https://frc.ri.cmu.edu/~zhangji/publications/ICRA_2016.pdf

    《PCL点云库学习&VS2010(X64)》Part 43 协方差矩阵的特征向量_点云协方差矩阵_梁Rio的博客-CSDN博客

    点云的基本特征和描述,PCA主成分分析_点云特征值_一抹烟霞的博客-CSDN博客

    SLAM中的退化问题_slam退化_Kinetis60的博客-CSDN博客

    lio-sam退化检测:

    1. for (int i = 0; i < laserCloudSelNum; i++) {
    2. // lidar -> camera
    3. pointOri.x = laserCloudOri->points[i].y;
    4. pointOri.y = laserCloudOri->points[i].z;
    5. pointOri.z = laserCloudOri->points[i].x;
    6. // lidar -> camera
    7. coeff.x = coeffSel->points[i].y;
    8. coeff.y = coeffSel->points[i].z;
    9. coeff.z = coeffSel->points[i].x;
    10. coeff.intensity = coeffSel->points[i].intensity;
    11. // in camera
    12. float arx = (crx * sry * srz * pointOri.x + crx * crz * sry * pointOri.y - srx * sry * pointOri.z) * coeff.x
    13. + (-srx * srz * pointOri.x - crz * srx * pointOri.y - crx * pointOri.z) * coeff.y
    14. + (crx * cry * srz * pointOri.x + crx * cry * crz * pointOri.y - cry * srx * pointOri.z) * coeff.z;
    15. float ary = ((cry * srx * srz - crz * sry) * pointOri.x
    16. + (sry * srz + cry * crz * srx) * pointOri.y + crx * cry * pointOri.z)
    17. * coeff.x
    18. + ((-cry * crz - srx * sry * srz) * pointOri.x
    19. + (cry * srz - crz * srx * sry) * pointOri.y - crx * sry * pointOri.z)
    20. * coeff.z;
    21. float arz = ((crz * srx * sry - cry * srz) * pointOri.x + (-cry * crz - srx * sry * srz) * pointOri.y) * coeff.x
    22. + (crx * crz * pointOri.x - crx * srz * pointOri.y) * coeff.y
    23. + ((sry * srz + cry * crz * srx) * pointOri.x + (crz * sry - cry * srx * srz) * pointOri.y) * coeff.z;
    24. // camera -> lidar
    25. matA.at<float>(i, 0) = arz;
    26. matA.at<float>(i, 1) = arx;
    27. matA.at<float>(i, 2) = ary;
    28. matA.at<float>(i, 3) = coeff.z;
    29. matA.at<float>(i, 4) = coeff.x;
    30. matA.at<float>(i, 5) = coeff.y;
    31. matB.at<float>(i, 0) = -coeff.intensity;
    32. }
    33. cv::transpose(matA, matAt);
    34. matAtA = matAt * matA;
    35. matAtB = matAt * matB;
    36. cv::solve(matAtA, matAtB, matX, cv::DECOMP_QR);
    37. if (iterCount == 0) {
    38. cv::Mat matE(1, 6, CV_32F, cv::Scalar::all(0));
    39. cv::Mat matV(6, 6, CV_32F, cv::Scalar::all(0));
    40. cv::Mat matV2(6, 6, CV_32F, cv::Scalar::all(0));
    41. cv::eigen(matAtA, matE, matV);
    42. matV.copyTo(matV2);
    43. isDegenerate = false;
    44. float eignThre[6] = {100, 100, 100, 100, 100, 100};
    45. for (int i = 5; i >= 0; i--) {
    46. if (matE.at<float>(0, i) < eignThre[i]) {
    47. for (int j = 0; j < 6; j++) {
    48. matV2.at<float>(i, j) = 0;
    49. }
    50. isDegenerate = true;
    51. } else {
    52. break;
    53. }
    54. }
    55. matP = matV.inv() * matV2;
    56. }

     fast-lio2打滑检测:

    1. pcl::compute3DCentroid(*feats_undistort, xyz_centroid);
    2. pcl::computeCovarianceMatrix(*feats_undistort, xyz_centroid, all_covariance);
    3. Eigen::EigenSolver<Eigen::Matrix3f> es(all_covariance);
    4. Eigen::VectorXf eigenvalues = es.eigenvalues().real();
    5. Eigen::VectorXf sorted_eigen;
    6. Eigen::VectorXi index;
    7. sort_vec(eigenvalues, sorted_eigen, index);
    8. fout_degen << sorted_eigen(0) << " "
    9. << sorted_eigen(1) << " "
    10. << sorted_eigen(2) << std::endl;
    11. if (sorted_eigen(2) < 1000 && log10(abs(sorted_eigen(1)) / abs(sorted_eigen(2))) > ls_map.eskf_stt_.eigen_degen) {
    12. ls_map.car_stt_.flag_degen = true;
    13. p_imu->flag_degen_ = true;
    14. ls_map.lidar_stt_.savepcd++;
    15. if (feats_undistort->points.size() > 0) {
    16. std::cerr << "feats_undistort:" << feats_undistort->points.size() << std::endl;
    17. savepcl(string(root_dir + "/PCD/" + to_string(ls_map.lidar_stt_.savepcd) + ".pcd"), *feats_undistort);
    18. }
    19. flag_degen_num = 0;
    20. }

    matlab显示特征值:

    1. X = load('nodegene/pose_degen.txt');
    2. b1=X(:,1);
    3. b2=X(:,2);
    4. b3=X(:,3);
    5. Y=load('nodegene/pose_out.txt');
    6. ax = Y(:,2);
    7. ay = Y(:,3);
    8. b4=b3;
    9. figure(1)
    10. for i=1:length(b1)
    11. if(b3(i)<1500&&log10(b2(i) /b3(i) ) > 2.3)%200
    12. plot(ax(i),ay(i),'o');
    13. b4(i)=0;
    14. hold on;
    15. else
    16. b4(i)=1000;
    17. end
    18. end
    19. plot(ax,ay)
    20. figure(2)
    21. plot(b3)
    22. hold on;
    23. plot(b1/200)
    24. hold on;
    25. plot(b2/200)
    26. hold on;
    27. plot(b4)

     c++特征值特征向量显示

    1. #include <Eigen/Core>
    2. #include <Eigen/Dense>
    3. #include <boost/thread/thread.hpp>
    4. #include <iostream>
    5. #include <pcl/common/centroid.h>
    6. #include <pcl/common/common_headers.h>
    7. #include <pcl/console/parse.h>
    8. #include <pcl/features/feature.h>
    9. #include <pcl/features/normal_3d.h>
    10. #include <pcl/filters/voxel_grid.h>
    11. #include <pcl/io/pcd_io.h>
    12. #include <pcl/point_cloud.h>
    13. #include <pcl/point_types.h>
    14. #include <pcl/visualization/cloud_viewer.h>
    15. #include <pcl/visualization/pcl_visualizer.h>
    16. #include <string>
    17. #include <unistd.h>
    18. using namespace pcl;
    19. using namespace pcl::io;
    20. using namespace Eigen;
    21. using namespace std;
    22. #define PRITF 5
    23. struct VecFeat {
    24. float val;
    25. float vec[3];
    26. };
    27. int point_size = 0;
    28. /************************************************************************************************************
    29. /*****************************可视化单个点云:应用PCL
    30. Visualizer可视化类显示单个具有XYZ信息的点云****************/
    31. /************************************************************************************************************/
    32. // simpleVis函数实现最基本的点云可视化操作,
    33. boost::shared_ptr<pcl::visualization::PCLVisualizer>
    34. simpleVis(pcl::PointCloud<pcl::PointXYZ>::ConstPtr cloud) {
    35. // --------------------------------------------
    36. // -----Open 3D viewer and add point cloud-----
    37. // --------------------------------------------
    38. //创建视窗对象并给标题栏设置一个名称“3D
    39. // Viewer”并将它设置为boost::shared_ptr智能共享指针,这样可以保证指针在程序中全局使用,而不引起内存错误
    40. boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer(
    41. new pcl::visualization::PCLVisualizer("3D Viewer"));
    42. //设置视窗的背景色,可以任意设置RGB的颜色,这里是设置为黑色
    43. viewer->setBackgroundColor(0, 0, 0);
    44. /*这是最重要的一行,我们将点云添加到视窗对象中,并定一个唯一的字符串作为ID
    45. 号,利用此字符串保证在其他成员中也能
    46. 标志引用该点云,多次调用addPointCloud可以实现多个点云的添加,,每调用一次就会创建一个新的ID号,如果想更新一个
    47. 已经显示的点云,必须先调用removePointCloud(),并提供需要更新的点云ID 号,
    48. *******************************************************************************************/
    49. viewer->addPointCloud<pcl::PointXYZ>(cloud, "sample cloud");
    50. //用于改变显示点云的尺寸,可以利用该方法控制点云在视窗中的显示方法,
    51. viewer->setPointCloudRenderingProperties(
    52. pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "sample cloud");
    53. /*******************************************************************************************************
    54. 查看复杂的点云,经常让人感到没有方向感,为了保持正确的坐标判断,需要显示坐标系统方向,可以通过使用X(红色)
    55. Y(绿色 )Z
    56. (蓝色)圆柱体代表坐标轴的显示方式来解决,圆柱体的大小可以通过scale参数来控制,本例中scale设置为1.0
    57. ******************************************************************************************************/
    58. viewer->addCoordinateSystem(1.0);
    59. //通过设置照相机参数使得从默认的角度和方向观察点云
    60. viewer->initCameraParameters();
    61. return (viewer);
    62. }
    63. /*****************************可视化点云颜色特征******************************************************/
    64. /**************************************************************************************************
    65. 多数情况下点云显示不采用简单的XYZ类型,常用的点云类型是XYZRGB点,包含颜色数据,除此之外,还可以给指定的点云定制颜色
    66. 以示得点云在视窗中比较容易区分。点赋予不同的颜色表征其对应的Z轴值不同,PCL
    67. Visualizer可根据所存储的颜色数据为点云 赋色,
    68. 比如许多设备kinect可以获取带有RGB数据的点云,PCL
    69. Vizualizer可视化类可使用这种颜色数据为点云着色,rgbVis函数中的代码
    70. 用于完成这种操作。
    71. ***************************************************************************************************/
    72. /**************************************************************************
    73. 与前面的示例相比点云的类型发生了变化,这里使用的点云带有RGB数据的属性字段,
    74. ****************************************************************************/
    75. boost::shared_ptr<pcl::visualization::PCLVisualizer>
    76. rgbVis(pcl::PointCloud<pcl::PointXYZRGB>::ConstPtr cloud) {
    77. // --------------------------------------------
    78. // -----Open 3D viewer and add point cloud-----
    79. // --------------------------------------------
    80. boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer(
    81. new pcl::visualization::PCLVisualizer("3D Viewer"));
    82. viewer->setBackgroundColor(0, 0, 0);
    83. /***************************************************************************************************************
    84. 设置窗口的背景颜色后,创建一个颜色处理对象,PointCloudColorHandlerRGBField利用这样的对象显示自定义颜色数据,PointCloudColorHandlerRGBField
    85. 对象得到每个点云的RGB颜色字段,
    86. **************************************************************************************************************/
    87. pcl::visualization::PointCloudColorHandlerRGBField<pcl::PointXYZRGB> rgb(
    88. cloud);
    89. viewer->addPointCloud<pcl::PointXYZRGB>(cloud, rgb, "sample cloud");
    90. viewer->setPointCloudRenderingProperties(
    91. pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3, "sample cloud");
    92. viewer->addCoordinateSystem(1.0);
    93. viewer->initCameraParameters();
    94. return (viewer);
    95. }
    96. /******************可视化点云自定义颜色特征**********************************************************/
    97. /****************************************************************************************************
    98. 演示怎样给点云着上单独的一种颜色,可以利用该技术给指定的点云着色,以区别其他的点云,
    99. *****************************************************************************************************/
    100. //点云类型为XYZ类型,customColourVis函数将点云赋值为绿色,
    101. boost::shared_ptr<pcl::visualization::PCLVisualizer>
    102. customColourVis(pcl::PointCloud<pcl::PointXYZ>::ConstPtr cloud) {
    103. // --------------------------------------------
    104. // -----Open 3D viewer and add point cloud-----
    105. // --------------------------------------------
    106. boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer(
    107. new pcl::visualization::PCLVisualizer("3D Viewer"));
    108. viewer->setBackgroundColor(0, 0, 0);
    109. //创建一个自定义的颜色处理器PointCloudColorHandlerCustom对象,并设置颜色为纯绿色
    110. pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> single_color(
    111. cloud, 0, 255, 0);
    112. // addPointCloud<>()完成对颜色处理器对象的传递
    113. viewer->addPointCloud<pcl::PointXYZ>(cloud, single_color, "sample cloud");
    114. viewer->setPointCloudRenderingProperties(
    115. pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3, "sample cloud");
    116. viewer->addCoordinateSystem(1.0);
    117. viewer->initCameraParameters();
    118. return (viewer);
    119. }
    120. //*******************可视化点云法线和其他特征*************************************************/
    121. /*********************************************************************************************
    122. 显示法线是理解点云的一个重要步骤,点云法线特征是非常重要的基础特征,PCL
    123. visualizer可视化类可用于绘制法线,
    124. 也可以绘制表征点云的其他特征,比如主曲率和几何特征,normalsVis函数中演示了如何实现点云的法线,
    125. ***********************************************************************************************/
    126. boost::shared_ptr<pcl::visualization::PCLVisualizer>
    127. normalsVis(pcl::PointCloud<pcl::PointXYZRGB>::ConstPtr cloud,
    128. pcl::PointCloud<pcl::Normal>::ConstPtr normals) {
    129. // --------------------------------------------------------
    130. // -----Open 3D viewer and add point cloud and normals-----
    131. // --------------------------------------------------------
    132. boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer(
    133. new pcl::visualization::PCLVisualizer("3D Viewer"));
    134. viewer->setBackgroundColor(0, 0, 0);
    135. pcl::visualization::PointCloudColorHandlerRGBField<pcl::PointXYZRGB> rgb(
    136. cloud);
    137. viewer->addPointCloud<pcl::PointXYZRGB>(cloud, rgb, "sample cloud");
    138. viewer->setPointCloudRenderingProperties(
    139. pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3, "sample cloud");
    140. //实现对点云法线的显示
    141. viewer->addPointCloudNormals<pcl::PointXYZRGB, pcl::Normal>(
    142. cloud, normals, 10, 0.05, "normals");
    143. viewer->addCoordinateSystem(1.0);
    144. viewer->initCameraParameters();
    145. return (viewer);
    146. }
    147. //*****************绘制普通形状************************************************//
    148. /**************************************************************************************************************
    149. PCL
    150. visualizer可视化类允许用户在视窗中绘制一般图元,这个类常用于显示点云处理算法的可视化结果,例如
    151. 通过可视化球体
    152. 包围聚类得到的点云集以显示聚类结果,shapesVis函数用于实现添加形状到视窗中,添加了四种形状:从点云中的一个点到最后一个点
    153. 之间的连线,原点所在的平面,以点云中第一个点为中心的球体,沿Y轴的椎体
    154. *************************************************************************************************************/
    155. boost::shared_ptr<pcl::visualization::PCLVisualizer>
    156. shapesVis(pcl::PointCloud<pcl::PointXYZRGB>::ConstPtr cloud) {
    157. // --------------------------------------------
    158. // -----Open 3D viewer and add point cloud添加点云到视窗实例代码-----
    159. // --------------------------------------------
    160. boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer(
    161. new pcl::visualization::PCLVisualizer("3D Viewer"));
    162. viewer->setBackgroundColor(0, 0, 0);
    163. pcl::visualization::PointCloudColorHandlerRGBField<pcl::PointXYZRGB> rgb(
    164. cloud);
    165. viewer->addPointCloud<pcl::PointXYZRGB>(cloud, rgb, "sample cloud");
    166. viewer->setPointCloudRenderingProperties(
    167. pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3, "sample cloud");
    168. viewer->addCoordinateSystem(1.0);
    169. viewer->initCameraParameters();
    170. /************************************************************************************************
    171. 绘制形状的实例代码,绘制点之间的连线,
    172. *************************************************************************************************/
    173. viewer->addLine<pcl::PointXYZRGB>(cloud->points[0],
    174. cloud->points[cloud->size() - 1], "line");
    175. //添加点云中第一个点为中心,半径为0.2的球体,同时可以自定义颜色
    176. viewer->addSphere(cloud->points[0], 0.2, 0.5, 0.5, 0.0, "sphere");
    177. //---------------------------------------
    178. //-----Add shapes at other
    179. // locations添加绘制平面使用标准平面方程ax+by+cz+d=0来定义平面,这个平面以原点为中心,方向沿着Z方向-----
    180. //---------------------------------------
    181. pcl::ModelCoefficients coeffs;
    182. coeffs.values.push_back(0.0);
    183. coeffs.values.push_back(0.0);
    184. coeffs.values.push_back(1.0);
    185. coeffs.values.push_back(0.0);
    186. viewer->addPlane(coeffs, "plane");
    187. //添加锥形的参数
    188. coeffs.values.clear();
    189. coeffs.values.push_back(0.3);
    190. coeffs.values.push_back(0.3);
    191. coeffs.values.push_back(0.0);
    192. coeffs.values.push_back(0.0);
    193. coeffs.values.push_back(1.0);
    194. coeffs.values.push_back(0.0);
    195. coeffs.values.push_back(5.0);
    196. viewer->addCone(coeffs, "cone");
    197. return (viewer);
    198. }
    199. /***************
    200. * cov show
    201. *************************************************************************************************************/
    202. boost::shared_ptr<pcl::visualization::PCLVisualizer>
    203. CovshapesVis(pcl::PointCloud<pcl::PointXYZRGB>::ConstPtr cloud,
    204. const Eigen::Vector4f xyz_centroid, VecFeat *vecfeat) {
    205. // --------------------------------------------
    206. // -----Open 3D viewer and add point cloud添加点云到视窗实例代码-----
    207. // --------------------------------------------
    208. boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer(
    209. new pcl::visualization::PCLVisualizer("3D Viewer"));
    210. viewer->setBackgroundColor(1, 1, 1);
    211. pcl::visualization::PointCloudColorHandlerRGBField<pcl::PointXYZRGB> rgb(
    212. cloud);
    213. viewer->addPointCloud<pcl::PointXYZRGB>(cloud, rgb, "sample cloud");
    214. viewer->setPointCloudRenderingProperties(
    215. pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3, "sample cloud");
    216. // viewer->addCoordinateSystem(1.0);
    217. // viewer->initCameraParameters();
    218. pcl::PointXYZRGB center_point, cord_xyz[3];
    219. center_point.x = xyz_centroid[0];
    220. center_point.y = xyz_centroid[1];
    221. center_point.z = xyz_centroid[2];
    222. double rgb_show[3];
    223. for (int i = 0; i < 3; i++) {
    224. rgb_show[i] =
    225. vecfeat[i].val / (vecfeat[0].val + vecfeat[1].val + vecfeat[2].val);
    226. cord_xyz[i].x =
    227. point_size * rgb_show[i] * vecfeat[i].vec[0] + center_point.x;
    228. cord_xyz[i].y =
    229. point_size * rgb_show[i] * vecfeat[i].vec[1] + center_point.y;
    230. cord_xyz[i].z =
    231. point_size * rgb_show[i] * vecfeat[i].vec[2] + center_point.z;
    232. string lin_name = to_string(i);
    233. viewer->addLine<pcl::PointXYZRGB>(center_point, cord_xyz[i], rgb_show[i],
    234. rgb_show[i], rgb_show[i], lin_name);
    235. }
    236. // viewer->addLine<pcl::PointXYZRGB>(center_point,
    237. // cloud->points[cloud->size() - 1], "line");
    238. //添加点云中第一个点为中心,半径为0.2的球体,同时可以自定义颜色
    239. // viewer->addSphere(cloud->points[0], 0.2, 0.5, 0.5, 0.0, "sphere");
    240. //---------------------------------------
    241. //-----Add shapes at other
    242. // locations添加绘制平面使用标准平面方程ax+by+cz+d=0来定义平面,这个平面以原点为中心,方向沿着Z方向-----
    243. //---------------------------------------
    244. // pcl::ModelCoefficients coeffs;
    245. // coeffs.values.push_back(0.0);
    246. // coeffs.values.push_back(0.0);
    247. // coeffs.values.push_back(1.0);
    248. // coeffs.values.push_back(0.0);
    249. // viewer->addPlane(coeffs, "plane");
    250. //添加锥形的参数
    251. // coeffs.values.clear();
    252. // coeffs.values.push_back(0.3);
    253. // coeffs.values.push_back(0.3);
    254. // coeffs.values.push_back(0.0);
    255. // coeffs.values.push_back(0.0);
    256. // coeffs.values.push_back(1.0);
    257. // coeffs.values.push_back(0.0);
    258. // coeffs.values.push_back(5.0);
    259. // viewer->addCone(coeffs, "cone");
    260. return (viewer);
    261. }
    262. /******************************************************************************************
    263. 多视角显示:PCL
    264. visealizer可视化类允许用户通过不同的窗口(Viewport)绘制多个点云这样方便对点云比较
    265. viewportsVis函数演示如何用多视角来显示点云计算法线的方法结果对比
    266. ******************************************************************************************/
    267. boost::shared_ptr<pcl::visualization::PCLVisualizer>
    268. viewportsVis(pcl::PointCloud<pcl::PointXYZRGB>::ConstPtr cloud,
    269. pcl::PointCloud<pcl::Normal>::ConstPtr normals1,
    270. pcl::PointCloud<pcl::Normal>::ConstPtr normals2) {
    271. // --------------------------------------------------------
    272. // -----Open 3D viewer and add point cloud and normals-----
    273. // --------------------------------------------------------
    274. boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer(
    275. new pcl::visualization::PCLVisualizer("3D Viewer"));
    276. viewer->initCameraParameters();
    277. //以上是创建视图的标准代码
    278. int v1(0); //创建新的视口
    279. viewer->createViewPort(
    280. 0.0, 0.0, 0.5, 1.0,
    281. v1); // 4个参数分别是X轴的最小值,最大值,Y轴的最小值,最大值,取值0-1,v1是标识
    282. viewer->setBackgroundColor(0, 0, 0, v1); //设置视口的背景颜色
    283. viewer->addText(
    284. "Radius: 0.01", 10, 10, "v1 text",
    285. v1); //添加一个标签区别其他窗口 利用RGB颜色着色器并添加点云到视口中
    286. pcl::visualization::PointCloudColorHandlerRGBField<pcl::PointXYZRGB> rgb(
    287. cloud);
    288. viewer->addPointCloud<pcl::PointXYZRGB>(cloud, rgb, "sample cloud1", v1);
    289. //对第二视口做同样的操作,使得做创建的点云分布于右半窗口,将该视口背景赋值于灰色,以便明显区别,虽然添加同样的点云,给点云自定义颜色着色
    290. int v2(0);
    291. viewer->createViewPort(0.5, 0.0, 1.0, 1.0, v2);
    292. viewer->setBackgroundColor(0.3, 0.3, 0.3, v2);
    293. viewer->addText("Radius: 0.1", 10, 10, "v2 text", v2);
    294. pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZRGB>
    295. single_color(cloud, 0, 255, 0);
    296. viewer->addPointCloud<pcl::PointXYZRGB>(cloud, single_color, "sample cloud2",
    297. v2);
    298. //为所有视口设置属性,
    299. viewer->setPointCloudRenderingProperties(
    300. pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3, "sample cloud1");
    301. viewer->setPointCloudRenderingProperties(
    302. pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 3, "sample cloud2");
    303. viewer->addCoordinateSystem(1.0);
    304. //添加法线 每个视图都有一组对应的法线
    305. viewer->addPointCloudNormals<pcl::PointXYZRGB, pcl::Normal>(
    306. cloud, normals1, 10, 0.05, "normals1", v1);
    307. viewer->addPointCloudNormals<pcl::PointXYZRGB, pcl::Normal>(
    308. cloud, normals2, 10, 0.05, "normals2", v2);
    309. return (viewer);
    310. }
    311. /*******************************************************************************************************
    312. 这里是处理鼠标事件的函数,每次相应鼠标时间都会回电函数,需要从event实例提取事件信息,本例中查找鼠标左键的释放事件
    313. 每次响应这种事件都会在鼠标按下的位置上生成一个文本标签。
    314. *********************************************************************************************************/
    315. unsigned int text_id = 0;
    316. void keyboardEventOccurred(const pcl::visualization::KeyboardEvent &event,
    317. void *viewer_void) {
    318. pcl::visualization::PCLVisualizer *viewer =
    319. static_cast<pcl::visualization::PCLVisualizer *>(viewer_void);
    320. if (event.getKeySym() == "r" && event.keyDown()) {
    321. std::cout << "r was pressed => removing all text" << std::endl;
    322. char str[512];
    323. for (unsigned int i = 0; i < text_id; ++i) {
    324. sprintf(str, "text#%03d", i);
    325. viewer->removeShape(str);
    326. }
    327. text_id = 0;
    328. }
    329. }
    330. /********************************************************************************************
    331. 键盘事件 我们按下哪个按键 如果按下r健
    332. 则删除前面鼠标所产生的文本标签,需要注意的是,当按下R键时 3D相机仍然会重置
    333. 所以在PCL中视窗中注册事件响应回调函数,不会覆盖其他成员对同一事件的响应
    334. **************************************************************************************************/
    335. void mouseEventOccurred(const pcl::visualization::MouseEvent &event,
    336. void *viewer_void) {
    337. pcl::visualization::PCLVisualizer *viewer =
    338. static_cast<pcl::visualization::PCLVisualizer *>(viewer_void);
    339. if (event.getButton() == pcl::visualization::MouseEvent::LeftButton &&
    340. event.getType() == pcl::visualization::MouseEvent::MouseButtonRelease) {
    341. std::cout << "Left mouse button released at position (" << event.getX()
    342. << ", " << event.getY() << ")" << std::endl;
    343. char str[512];
    344. sprintf(str, "text#%03d", text_id++);
    345. viewer->addText("clicked here", event.getX(), event.getY(), str);
    346. }
    347. }
    348. /******************自定义交互*****************************************************************************/
    349. /******************************************************************************************************
    350. 多数情况下,默认的鼠标和键盘交互设置不能满足用户的需求,用户想扩展函数的某一些功能,
    351. 比如按下键盘时保存点云的信息, 或者通过鼠标确定点云的位置
    352. interactionCustomizationVis函数进行演示如何捕捉鼠标和键盘事件,在窗口点击,将会显示
    353. 一个2D的文本标签,按下r健出去文本
    354. ******************************************************************************************************/
    355. boost::shared_ptr<pcl::visualization::PCLVisualizer>
    356. interactionCustomizationVis() {
    357. boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer(
    358. new pcl::visualization::PCLVisualizer("3D Viewer"));
    359. viewer->setBackgroundColor(0, 0, 0);
    360. //以上是实例化视窗的标准代码
    361. viewer->addCoordinateSystem(1.0);
    362. //分别注册响应键盘和鼠标事件,keyboardEventOccurred
    363. // mouseEventOccurred回调函数,需要将boost::shared_ptr强制转换为void*
    364. viewer->registerKeyboardCallback(keyboardEventOccurred, (void *)viewer.get());
    365. viewer->registerMouseCallback(mouseEventOccurred, (void *)viewer.get());
    366. return (viewer);
    367. }
    368. /********************************************************
    369. * 计算点云中点坐标
    370. * 功能与函数相同
    371. * pcl::compute3DCentroid(*feats_undistort, xyz_centroid);
    372. */
    373. Eigen::Vector4f ComputeCent(const pcl::PointCloud<pcl::PointXYZ>::Ptr cloud) {
    374. //计算中心点坐标
    375. Eigen::Vector4f pcl_center = Eigen::Vector4f::Zero(4);
    376. for (int i = 0; i < cloud->points.size(); i++) {
    377. pcl_center[0] += cloud->points[i].x;
    378. pcl_center[1] += cloud->points[i].y;
    379. pcl_center[2] += cloud->points[i].z;
    380. }
    381. pcl_center = pcl_center / cloud->points.size();
    382. pcl_center[3] = 1.0;
    383. return pcl_center;
    384. }
    385. /****************************
    386. * 根据点云的中点和点云计算点云的方差
    387. * pcl::computeCovarianceMatrix(*feats_undistort, xyz_centroid,
    388. all_covariance);
    389. */
    390. Eigen::Matrix3f ComputeCov(Eigen::Vector4f &center,
    391. const pcl::PointCloud<pcl::PointXYZ>::Ptr cloud) {
    392. int cld_sz = 1.0;
    393. double xx = 0, xy = 0, xz = 0, yy = 0, yz = 0, zz = 0;
    394. for (int i = 0; i < cloud->points.size(); i++) {
    395. xx += (cloud->points[i].x - center[0]) * (cloud->points[i].x - center[0]);
    396. xy += (cloud->points[i].x - center[0]) * (cloud->points[i].y - center[1]);
    397. xz += (cloud->points[i].x - center[0]) * (cloud->points[i].z - center[2]);
    398. yy += (cloud->points[i].y - center[1]) * (cloud->points[i].y - center[1]);
    399. yz += (cloud->points[i].y - center[1]) * (cloud->points[i].z - center[2]);
    400. zz += (cloud->points[i].z - center[2]) * (cloud->points[i].z - center[2]);
    401. }
    402. //大小为3*3的协方差矩阵
    403. Eigen::Matrix3f covMat(3, 3);
    404. covMat(0, 0) = xx / cld_sz;
    405. covMat(0, 1) = covMat(1, 0) = xy / cld_sz;
    406. covMat(0, 2) = covMat(2, 0) = xz / cld_sz;
    407. covMat(1, 1) = yy / cld_sz;
    408. covMat(1, 2) = covMat(2, 1) = yz / cld_sz;
    409. covMat(2, 2) = zz / cld_sz;
    410. return covMat;
    411. }
    412. /*************************************
    413. *input: vec特征值
    414. *output:sorted_vec 排序后的特征值
    415. * :ind 排序后的特征值对应的特征向量编号
    416. *
    417. ***************************************/
    418. void sort_vec(const Vector3f &vec, Vector3f &sorted_vec, Vector3i &ind) {
    419. ind = Vector3i::LinSpaced(vec.size(), 0, vec.size() - 1);
    420. auto rule = [vec](int i, int j) -> bool { return abs(vec(i)) > abs(vec(j)); };
    421. std::sort(ind.data(), ind.data() + ind.size(), rule);
    422. sorted_vec.resize(vec.size());
    423. for (int i = 0; i < vec.size(); i++) {
    424. sorted_vec(i) = vec(ind(i));
    425. }
    426. }
    427. /******************************************
    428. * 计算特征值和特征向量
    429. *input:cloud-点云
    430. *output:xyz_centroid-点云中心坐标
    431. * 特征值与特征向量vecfeat为3维数组
    432. *
    433. *****************************************/
    434. void ComputePclVec(const pcl::PointCloud<pcl::PointXYZ>::Ptr cloud,
    435. Eigen::Vector4f &xyz_centroid, VecFeat *vecfeat) {
    436. Eigen::Matrix3f all_covariance;
    437. pcl::compute3DCentroid(*cloud, xyz_centroid);
    438. pcl::computeCovarianceMatrix(*cloud, xyz_centroid, all_covariance);
    439. if (PRITF == 1)
    440. std::cerr << "begin:" << xyz_centroid << ";" << ComputeCent(cloud) << ";"
    441. << ComputeCov(xyz_centroid, cloud) << ";" << all_covariance
    442. << std::endl;
    443. //求特征值与特征向量
    444. Eigen::EigenSolver<Eigen::Matrix3f> es(all_covariance);
    445. Eigen::Matrix3f valMat = es.pseudoEigenvalueMatrix(); //特征值矩阵
    446. Eigen::Vector3f val = es.eigenvalues().real(); //特征值
    447. Eigen::Matrix3f vec = es.pseudoEigenvectors(); //特征向量
    448. //找到最小特征值t1
    449. // val(0,0) val(1,1) val(2,2)协方差矩阵中,就是
    450. // 只有对角线元素不为零,两个变量是独立的
    451. //对应三个特征向量
    452. if (PRITF == 2)
    453. std::cerr << "特征值" << val << "value 2" << es.eigenvalues().real()
    454. << " 特征向量" << vec << std::endl;
    455. Eigen::Vector3f sorted_eigen; //排序后的特征值
    456. Eigen::Vector3i index; //按特征值大小排序后的特征向量编号
    457. sort_vec(val, sorted_eigen, index);
    458. for (int i = 0; i < 3; i++) {
    459. vecfeat[i].val = sorted_eigen[i];
    460. std::cerr << "num " << i << " :"
    461. << "val is :" << vecfeat[i].val << std::endl;
    462. for (int j = 0; j < 3; j++)
    463. vecfeat[i].vec[j] = vec(j, i);
    464. std::cerr << "vec is:" << vecfeat[0].vec[i] << "," << vecfeat[1].vec[i]
    465. << "," << vecfeat[2].vec[i] << std::endl;
    466. }
    467. std::cerr << vecfeat << std::endl;
    468. }
    469. /**
    470. * 保存点云
    471. * name:a.pcd
    472. * @return int
    473. */
    474. template <typename T> int savepcl(string name, T &incloud) {
    475. pcl::PointCloud<pcl::PointXYZ> cloud;
    476. // 创建点云
    477. cloud.width = 1;
    478. cloud.height = incloud.points.size();
    479. cloud.is_dense = false;
    480. cloud.points.resize(incloud.points.size());
    481. for (size_t i = 0; i < incloud.points.size(); ++i) {
    482. // 坐标范围在[0,1024),正方体内随机取点
    483. cloud.points[i].x = incloud.points[i].x;
    484. cloud.points[i].y = incloud.points[i].y;
    485. cloud.points[i].z = incloud.points[i].z;
    486. }
    487. pcl::io::savePCDFileASCII(name, cloud);
    488. return (0);
    489. }
    490. /*****
    491. * 生成点云
    492. * 形式
    493. */
    494. template <typename T> int genpcl(T &cloud, int mode) {
    495. // pcl::PointCloud<pcl::PointXYZ> cloud;
    496. // 创建点云
    497. if (mode == 0) {
    498. cloud.width = 10000;
    499. cloud.height = 1;
    500. cloud.is_dense = false;
    501. cloud.points.resize(cloud.width * cloud.height);
    502. for (size_t i = 0; i < cloud.points.size(); ++i) {
    503. // 坐标范围在[0,1024),正方体内随机取点
    504. cloud.points[i].x = 1024 * (rand() / (RAND_MAX + 1.0f));
    505. cloud.points[i].y = 1024 * (rand() / (RAND_MAX + 1.0f));
    506. cloud.points[i].z = 1024 * (rand() / (RAND_MAX + 1.0f));
    507. }
    508. }
    509. return (0);
    510. }
    511. /***
    512. * 读取点云
    513. *
    514. */
    515. int readpcl(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, string name) {
    516. if (pcl::io::loadPCDFile<pcl::PointXYZ>(name, *cloud) == -1) //*打开点云文件
    517. {
    518. PCL_ERROR("Couldn't read file test_pcd.pcd\n");
    519. return (-1);
    520. }
    521. } /***
    522. *删除过远点
    523. */
    524. void removefar(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, float distance,
    525. float dismin) {
    526. pcl::PointCloud<pcl::PointXYZ> cloudtmp;
    527. double maxz = 0;
    528. for (int i = 0; i < cloud->points.size(); i++) {
    529. double dis = sqrt(cloud->points[i].x * cloud->points[i].x +
    530. cloud->points[i].y * cloud->points[i].y);
    531. if (cloud->points[i].z > maxz)
    532. maxz = cloud->points[i].z;
    533. if ((abs(cloud->points[i].x) < distance) &&
    534. (abs(cloud->points[i].y) < distance) && dis > dismin) {
    535. cloudtmp.points.push_back(cloud->points[i]);
    536. }
    537. }
    538. std::cerr << "max z:" << maxz << std::endl;
    539. *cloud = cloudtmp;
    540. }
    541. int main(int argc, char **argv) {
    542. int choose = atoi(argv[1]);
    543. int points_cloud_mod = atoi(argv[2]);
    544. if (1) {
    545. // ------------------------------------
    546. // -----Create example point cloud-----
    547. // ------------------------------------
    548. pcl::PointCloud<pcl::PointXYZ>::Ptr basic_cloud_ptr(
    549. new pcl::PointCloud<pcl::PointXYZ>);
    550. pcl::PointCloud<pcl::PointXYZRGB>::Ptr point_cloud_ptr(
    551. new pcl::PointCloud<pcl::PointXYZRGB>);
    552. std::cout << "Genarating example point clouds.\n\n";
    553. // We're going to make an ellipse extruded along the z-axis. The colour for
    554. // the XYZRGB cloud will gradually go from red to green to blue.
    555. uint8_t r(255), g(15), b(15);
    556. /*点云模式选择:
    557. ***0:圆 45*num
    558. **1:矩形
    559. */
    560. if (points_cloud_mod == 0) {
    561. int choose_angle = 2.0;
    562. point_size = 1.0;
    563. for (float z(-1.0); z <= 1.0; z += 0.05) {
    564. for (float angle(0.0); angle <= choose_angle * 45; angle += 5.0) {
    565. pcl::PointXYZ basic_point;
    566. basic_point.x = 0.5 * cosf(pcl::deg2rad(angle));
    567. basic_point.y = sinf(pcl::deg2rad(angle));
    568. basic_point.z = z;
    569. basic_cloud_ptr->points.push_back(basic_point);
    570. pcl::PointXYZRGB point;
    571. point.x = basic_point.x;
    572. point.y = basic_point.y;
    573. point.z = basic_point.z;
    574. uint32_t rgb =
    575. (static_cast(r) << 16 | static_cast(g) << 8 |
    576. static_cast(b));
    577. point.rgb = *reinterpret_cast(&rgb);
    578. point_cloud_ptr->points.push_back(point);
    579. }
    580. if (z < 0.0) {
    581. r -= 12;
    582. g += 12;
    583. } else {
    584. g -= 12;
    585. b += 12;
    586. }
    587. }
    588. } else if (points_cloud_mod == 1) {
    589. point_size = 1024;
    590. for (int i = 0; i < 1024; i++) {
    591. pcl::PointXYZ basic_point;
    592. basic_point.x = 1024 * (rand() / (RAND_MAX + 1.0f));
    593. basic_point.y = 1024 * (rand() / (RAND_MAX + 1.0f));
    594. basic_point.z = 1 * (rand() / (RAND_MAX + 1.0f));
    595. basic_cloud_ptr->points.push_back(basic_point);
    596. pcl::PointXYZRGB point;
    597. point.x = basic_point.x;
    598. point.y = basic_point.y;
    599. point.z = basic_point.z;
    600. uint32_t rgb =
    601. (static_cast(r) << 16 | static_cast(g) << 8 |
    602. static_cast(b));
    603. point.rgb = *reinterpret_cast(&rgb);
    604. point_cloud_ptr->points.push_back(point);
    605. if (basic_point.z < 1024.0) {
    606. r -= 12;
    607. g += 12;
    608. } else {
    609. g -= 12;
    610. b += 12;
    611. }
    612. }
    613. } else if (points_cloud_mod == 2) {
    614. point_size = 1024;
    615. for (int i = 0; i < 1024; i++) {
    616. pcl::PointXYZ basic_point;
    617. basic_point.x = 1024 * (rand() / (RAND_MAX + 1.0f));
    618. basic_point.y = 1024 * (rand() / (RAND_MAX + 1.0f));
    619. basic_point.z = 1;
    620. basic_cloud_ptr->points.push_back(basic_point);
    621. pcl::PointXYZRGB point;
    622. point.x = basic_point.x;
    623. point.y = basic_point.y;
    624. point.z = basic_point.z;
    625. uint32_t rgb =
    626. (static_cast(r) << 16 | static_cast(g) << 8 |
    627. static_cast(b));
    628. point.rgb = *reinterpret_cast(&rgb);
    629. point_cloud_ptr->points.push_back(point);
    630. if (basic_point.z < 1024.0) {
    631. r -= 12;
    632. g += 12;
    633. } else {
    634. g -= 12;
    635. b += 12;
    636. }
    637. }
    638. } else if (points_cloud_mod == 3) {
    639. point_size = 50;
    640. string file_name = "../pcd/" + (string)argv[3];
    641. readpcl(basic_cloud_ptr, file_name);
    642. removefar(basic_cloud_ptr, 70, 0);
    643. pcl::copyPointCloud(*basic_cloud_ptr, *point_cloud_ptr);
    644. } else if (points_cloud_mod == 4) {
    645. point_size = 50;
    646. genpcl(*basic_cloud_ptr, 0);
    647. savepcl("../pcd/pcl.pcd", *basic_cloud_ptr);
    648. pcl::copyPointCloud(*basic_cloud_ptr, *point_cloud_ptr);
    649. }
    650. basic_cloud_ptr->width = (int)basic_cloud_ptr->points.size();
    651. basic_cloud_ptr->height = 1;
    652. point_cloud_ptr->width = (int)point_cloud_ptr->points.size();
    653. point_cloud_ptr->height = 1;
    654. // ----------------------------------------------------------------
    655. // -----Calculate surface normals with a search radius of 0.05-----
    656. // ----------------------------------------------------------------
    657. pcl::NormalEstimation ne;
    658. ne.setInputCloud(point_cloud_ptr);
    659. pcl::search::KdTree::Ptr tree(
    660. new pcl::search::KdTree());
    661. ne.setSearchMethod(tree);
    662. pcl::PointCloud::Ptr cloud_normals1(
    663. new pcl::PointCloud);
    664. ne.setRadiusSearch(0.05);
    665. ne.compute(*cloud_normals1);
    666. // ---------------------------------------------------------------
    667. // -----Calculate surface normals with a search radius of 0.1-----
    668. // ---------------------------------------------------------------
    669. pcl::PointCloud::Ptr cloud_normals2(
    670. new pcl::PointCloud);
    671. ne.setRadiusSearch(0.1);
    672. ne.compute(*cloud_normals2);
    673. boost::shared_ptr viewer;
    674. if (choose == 1) {
    675. viewer = simpleVis(basic_cloud_ptr);
    676. } else if (choose == 2) {
    677. viewer = rgbVis(point_cloud_ptr);
    678. } else if (choose == 3) {
    679. viewer = customColourVis(basic_cloud_ptr);
    680. } else if (choose == 4) {
    681. viewer = normalsVis(point_cloud_ptr, cloud_normals2);
    682. } else if (choose == 5) {
    683. viewer = shapesVis(point_cloud_ptr);
    684. } else if (choose == 6) {
    685. viewer = viewportsVis(point_cloud_ptr, cloud_normals1, cloud_normals2);
    686. } else if (choose == 7) {
    687. viewer = interactionCustomizationVis();
    688. } else if (choose == 8) {
    689. Eigen::Vector4f xyz_centroid;
    690. VecFeat vecfeat[3];
    691. ComputePclVec(basic_cloud_ptr, xyz_centroid, &vecfeat[0]);
    692. viewer = CovshapesVis(point_cloud_ptr, xyz_centroid, &vecfeat[0]);
    693. }
    694. //--------------------
    695. // -----Main loop-----
    696. //--------------------
    697. while (!viewer->wasStopped()) {
    698. viewer->spinOnce(100);
    699. boost::this_thread::sleep(boost::posix_time::microseconds(100000));
    700. }
    701. }
    702. return 0;
    703. }

     特征值求解solver

     

  • 相关阅读:
    山与路远程控制 一个基于electron和golang实现的远控软件
    通达信交易接口
    python的反射机制
    大模型培训老师叶梓 AI编程的未来:GitHub Copilot的创新之旅与实践智慧
    c++11 智能指针-辅助类 (std::enable_shared_from_this)
    ComfyUI中实现反推提示词的多种方案
    Spring Cloud Alibaba(Nacos+Open Feign)微服务项目如何在本地调试,每次都调用本地的服务?
    PMBOK史上最大的改版,你知道到底有什么精华嘛?
    制造企业有了ERP,是否需要MES系统?
    2023/9/15 -- C++/QT
  • 原文地址:https://blog.csdn.net/JanKin_BY/article/details/132851723