RDP算法

这个算法很简单,可以说是对路径的拉直。可以用于处理A*。也可以说是无约束的降采样,可以消除小的抖动,但无法保证路径仍然安全。

有时路径规划获得的点太多,或者录制机器人路径获得的点太多,可以使用RDP算法,对得到的点进行缩减,用少量目标点表示路径。

首先,将起始点和终点连成一条线,找到剩下的点中距离这条线垂直距离最大的点P,记住点P和最大距离max,如果max小于设定的距离epsilon,则直接返回起始点和终止点就可以了。由于最大距离离这条直线也不远,因此可以把所有的点都看做在这条直线上,相当于把整个路径按照首尾拉直了。

如果max大于设定的距离epsilon,那么开始递归,以点P为中心将线段分为两部分,每一部分都重复上述过程,直到递归结束,相当于对两部分各自拉直。


opencv处理轮廓

void cv::circle(InputOutputArray img, Point center, int radius, const Scalar& color, int thickness = 1, LineTypes lineType = LINE_8, int shift = 0);

  • img: 输入输出参数,表示待绘制的目标图像。
  • center: 输入参数,表示圆心坐标,是一个 cv::Point 类型的对象。
  • radius: 输入参数,表示圆的半径。
  • color: 输入参数,表示绘制圆的颜色以及透明度,是一个 cv::Scalar 类型的对象。
  • thickness: 可选参数,表示圆线条的宽度。默认值为 1 表示绘制一个像素宽度的圆,如果设置为负值,则表示绘制一条填充的圆。
  • lineType
    : 可选参数,表示圆边界的类型,可以取以下几个值:
    cv::LINE_4: 表示绘制四个相邻的点的圆边界,默认值。
    cv::LINE_8: 表示绘制八个相邻的点的圆边界。
    cv::LINE_AA: 表示绘制抗锯齿的圆边界。

  • shift: 可选参数,表示坐标点像素值所占用的位数,默认值为 0。

矩形

void cv::rectangle(InputOutputArray img, Rect rect, const Scalar& color, int thickness = 1, LineTypes lineType = LINE_8, int shift = 0)

  • img: 输入输出参数,表示待绘制的目标图像。
  • rect: 输入参数,表示矩形,是一个 cv::Rect 类型的对象,可以通过传递左上角和右下角坐标的方式来定义一个矩形。
  • color: 输入参数,表示绘制矩形的颜色以及透明度,是一个 cv::Scalar 类型的对象。
  • thickness: 可选参数,表示矩形边框的宽度。默认值为 1 表示绘制一个像素宽度的矩形,如果设置为负值,则表示绘制一条填充的矩形。
  • lineType: 可选参数,表示矩形边框的类型,可以取以下几个值:
    cv::LINE_4: 表示绘制四个相邻的点的矩形边框,默认值。
    cv::LINE_8: 表示绘制八个相邻的点的矩形边框。
    cv::LINE_AA: 表示绘制抗锯齿的矩形边框。
  • shift: 可选参数,表示坐标点像素值所占用的位数,默认值为 0。

Rect函数的基本形式是Rect(x, y, width, height),其中x和y代表矩形左上角的坐标,widthheight分别代表矩形的宽度和高度。

如果创建一个Rect对象rect(100, 50, 50, 100),有以下常用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
rect.area();     //返回rect的面积 5000

rect.size(); //返回rect的尺寸 [50 × 100]

rect.tl(); //返回rect的左上顶点的坐标 [100, 50]

rect.br(); //返回rect的右下顶点的坐标 [150, 150]

rect.width(); //返回rect的宽度 50

rect.height(); //返回rect的高度 100

rect.contains(Point(x, y)); //返回布尔变量,判断rect是否包含Point(x, y)点


用opencv处理轮廓

Rect boundingRect(InputArray points)

寻找包裹轮廓的最小正矩形。矩形的边界与图像边界平行。 唯一一个参数是输入的二维点集,可以是 vector 或 Mat 类型。只需要 #include "opencv2/opencv.hpp"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<opencv.hpp>
#include<iostream>
using namespace cv;
using namespace std;
int main(){
Mat src = imread("/home/user/test.jpg");
imshow("src", src);

Mat gray, bin_img;
cvtColor(src, gray, COLOR_BGR2GRAY); //将原图转换为灰度图
imshow("gray", gray);

//二值化
threshold(gray, bin_img, 150, 255, THRESH_BINARY_INV);
imshow("bin_img", bin_img);

//寻找最外围轮廓
vector<vector<Point> >contours;
findContours(bin_img, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);

//绘制边界矩阵
RNG rngs = { 12345 };
Mat dst = Mat::zeros(src.size(), src.type());
for (int i = 0; i < contours.size(); i++)
{
Scalar colors = Scalar(rngs.uniform(0, 255), rngs.uniform(0, 255), rngs.uniform(0, 255));
drawContours(dst, contours, i, colors, 1);
Rect rects = boundingRect(contours[i]);
rectangle(dst, rects, colors, 2);
}
imshow("dst", dst);

waitKey(0);
}

RotatedRect minAreaRect(InputArray points)

寻找包裹轮廓的最小斜矩形。唯一一个参数是输入的二维点集,可以是 vector 或 Mat 类型。只需要 #include "opencv2/opencv.hpp"

boundingRect 返回结果的区别是:矩形的边界不必与图像边界平行

RotatedRect旋转矩形结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    Mat gray, bin_img;
cvtColor(src, gray, COLOR_BGR2GRAY); //将原图转换为灰度图
imshow("gray", gray);

//二值化
threshold(gray, bin_img, 150, 255, THRESH_BINARY_INV);
imshow("bin_img", bin_img);

//寻找最外围轮廓
vector<vector<Point> >contours;
findContours(bin_img, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);

//绘制最小边界矩阵
RNG rngs = { 12345 };
Mat dst = Mat::zeros(src.size(), src.type());
Point2f pts[4];
for (int i = 0; i < contours.size(); i++)
{
Scalar colors = Scalar(rngs.uniform(0, 255), rngs.uniform(0, 255), rngs.uniform(0, 255));
drawContours(dst, contours, i, colors, 1);
RotatedRect rects = minAreaRect(contours[i]);
// 确定旋转矩阵的四个顶点
rects.points(pts);
for (int i = 0; i < 4; i++)
line(dst, pts[i], pts[(i + 1) % 4], colors, 2);

}
}

double pointPolygonTest(InputArray contour, Point2f pt, bool measureDist)

计算点与轮廓的距离及位置关系,只需要 #include "opencv2/opencv.hpp"

  • contour: 所需检测的轮廓对象

  • pt: Point2f 类型的pt, 待判定位置的点

  • measureDist: 是否计算距离的标志, 当其为true时, 计算点到轮廓的最短距离, 当其为false时, 只判定轮廓与点的位置关系, 具体关系如下:

① 返回值为-1, 表示点在轮廓外部

② 返回值为0, 表示点在轮廓上

③ 返回值为1, 表示点在轮廓内部

如果不需要知道具体的距离,建议将第三个参数设为false,这样速度会提高2到3倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//计算点到轮廓的距离与位置关系
Mat srcImg = imread("1.png");
imshow("src", srcImg);

Mat dstImg = srcImg.clone();
cvtColor(srcImg, srcImg, CV_BGR2GRAY);
threshold(srcImg, srcImg, 100, 255, CV_THRESH_BINARY);
imshow("threshold", srcImg);

vector<vector<Point>> contours;
vector<Vec4i> hierarcy;
findContours(srcImg, contours, hierarcy, CV_RETR_TREE, CV_CHAIN_APPROX_NONE);//TREE:提取所有轮廓 NONE:画出轮廓所有点
cout << "contours.size()=" << contours.size() << endl;
for (int i = 0; i < contours.size(); i++)//遍历每个轮廓
{
for (int j = 0; j < contours[i].size(); j++)//遍历轮廓每个点
{
cout << "(" << contours[i][j].x << "," << contours[i][j].y << ")" << endl;
}
}
double a0 = pointPolygonTest(contours[0], Point(3, 3), true);//点到轮廓的最短距离
double b0 = pointPolygonTest(contours[0], Point(212, 184), false);//点与轮廓的位置关系:-1表示外部;0表示在轮廓上;1表示轮廓内部

求轮廓面积 cv::contourArea

double contourArea( InputArray contour, bool oriented = false );

  • InputArray类型的contour,输入的向量,二维点(轮廓顶点),可以为std::vector或Mat类型。
  • bool类型的oriented,面向区域标识符。若其为true,会返回一个带符号的面积值,正负取决于轮廓的方向。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;

int main(void)
{
Mat A = Mat::zeros(500, 500, CV_8UC1);
circle(A, Point2i(100, 100), 3, 255, -1);
circle(A, Point2i(300, 400), 50, 255, -1);
circle(A, Point2i(250, 100), 100, 255, -1);
circle(A, Point2i(400, 300), 60, 255, -1);

std::vector<std::vector<cv::Point> > contours; // 创建轮廓容器
std::vector<cv::Vec4i> hierarchy;

cv::findContours(A, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE, cv::Point());
if (!contours.empty() && !hierarchy.empty())
{
std::vector<std::vector<cv::Point> >::const_iterator itc = contours.begin();
// 遍历所有轮廓
int i = 1;
while (itc != contours.end())
{
double area = cv::contourArea(*itc);
cout << "第" << i << "个轮廓的面积为:" << area << endl;
i++;
itc++;
}
}
imshow("A", A);
waitKey(0);
system("pause");
return 0;
}

第一个圆的面积并不是9π,而是20。面积值是按照轮廓的内部面积进行计算的,会损失一些。

参考: cv::contourArea


深入探究虚函数 (二) 虚函数表vtbl和虚表指针vptr

虚函数表是通过一块内存来存储虚函数的地址,它实际是一个函数指针数组,每一个元素都是虚函数的指针,虚函数表的最后一个元素是一个空指针。假如虚函数类型为int,函数指针就是int*类型.虚函数表将被该类的所有对象共享,每个对象内部包含了一个虚表指针,指向虚函数表

有虚函数的类的最开始部分就是虚指针,它指向虚函数表起始地址,类型为int**(如果虚函数类型int),表中存放虚函数的地址。通过这个指针可以获取到该类对象的所有虚函数,包括父类的。

在编译期,编译器完成了虚表的创建,而虚指针在构造函数期间被初始化

因为派生类会继承基类的虚函数表,所以通过虚函数表,我们就可以知道该类对象的父类,在转换的时候就可以用来判断对象有无继承关系。派生类中增加的虚函数,如果覆盖了基类的虚函数,虚函数表中会替换相应的基类虚函数,地址换成派生类的;如果没有覆盖基类的虚函数,就添加到原虚函数表后面,以空指针结尾. 所以说派生类的虚函数表中的函数地址不是连续的,基类的是连续的。

类Base的虚表如下图:
Base.png
如果派生类Derived没有覆盖基类的虚函数,它的虚函数表如下图:

如果覆盖vFunc1,则替换Base的vFunc1;如果还定义了一个虚函数vFunc3,那么继续往虚函数表之后填,最后一个数组成员还是空指针

更详细的说明

当类有虚函数时候,类的第一个成员变量是一个虚函数指针,而this指针的值和第一个成员变量的地址相同(this指针指向第一个成员变量)。因此当有虚函数时,this指针的值等于虚函数指针的地址 this==&_vptr;

参考:
C++ 虚函数表
深入C++对象模型&虚函数表


Boost教程(四)atomic无锁编程

atomic 无锁编程

多个线程之间共享地址空间,所以多个线程共享进程中的全局变量和堆,都可以对全局变量和堆上的数据进行读写,但是如果两个线程同时修改同一个数据,可能造成某线程的修改丢失;如果一个线程写的同时,另一个线程去读该数据时可能会读到写了一半的数据。这些行为都是线程不安全的行为,会造成程序运行逻辑出现错误。下面的程序很常见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// boost::atomic<int> i(0);
int i=0;
boost::mutex mut;
void thread_1(int n);

int main()
{
int n=100000; // n不够大时,i不容易出现不同的情况
boost::thread th1 = boost::thread(boost::bind(&thread_1,n));
boost::thread th2 = boost::thread(boost::bind(&thread_1,n));
th1.join();
th2.join();
cout<< i<<endl;
return 0;
}

void thread_1(int n)
{
while(n--)
{
mut.lock();
i++;
mut.unlock();
}
}

如果不加lock,i最终值是不确定的,因为两个线程同时对i进行了写操作。一般地,我们用互斥锁mutex保护临界区,保证同一时间只能有一个线程可以获取锁,持有锁的线程可以对共享变量进行修改,修改完毕后释放锁,而不持有锁的线程阻塞等待直到获取到锁,然后才能对共享变量进行修改,最后i必定为200000

Boost提供了原子类型atomic,通过使用原子类型可摆脱每次对共享变量进行操作都进行的加锁解锁动作,节省了系统开销,同时避免了线程因阻塞而频繁的切换。atomic封装了不同计算机硬件的底层操作原语,提供了跨平台的原子操作功能,解决并发竞争读写变量的困扰,只需要包含文件<boost/atomic.hpp>,在上面的代码中使用boost::atomic<int> i(0);,然后去掉函数中的互斥锁,运行效果是一样的,而且节省了系统开销。

atomic可以把对类型T的操作原子化,T的要求:

  1. 标量类型(算数,枚举,指针)
  2. POD类型,可以使用memcmp,memset等函数

两种方式创建atomic对象:

1
2
3
4
5
6
atomic<int> a(10);
assert(a==10); //安全函数,若表达式不成立结束程序


atomic<long> L;
cout << L <<endl; //初始值不确定

最重要的两个成员函数: store() (operator=) 和 load() (operator T() )以原子方式存取,不会因为并发访问导致数据不一致。

1
2
3
4
5
6
7
8
9
boost::atomic<bool> b(1);
assert(b != 0);
std::cout << b << std::endl;
b.store(0);//存值
std::cout << b << std::endl;

boost::atomic<int> n1(100);
std::cout << n1.exchange(200) << std::endl; //交换两个值,并且返回原值100
std::cout << n1 << std::endl;

测试代码中临界区非常短,只有一个语句,所以显得加锁解锁操作对程序性能影响很大,但在实际应用中,我们的临界区一般不会这么短,临界区越长,加锁和解锁操作的性能损耗越微小,无锁编程和有锁编程之间的性能差距也就越微小。

无锁编程最大的优势在于两点:

  • 避免了死锁的产生。由于无锁编程避免了使用锁,所以也就不会出现并发编程中最让人头疼的死锁问题,对于提高程序健壮性有很大积极意义
  • 代码更加清晰与简洁

参考:C++11多线程编程


ROS2的概述和安装

ros2.0是一个跨平台的机器人开发框架,它在ros1.0的基础上进行了重构和改进,以适应更多的应用场景和需求。ros2.0的架构可以分为以下几个层次:

  • OS层: ros2.0支持多种操作系统,包括Linux、Windows、macOS、RTOS等,也支持没有操作系统的裸机.。

  • 中间层: ros2.0采用基于RTSP协议的DDS (Data-Distribution Service) 作为中间层,DDS是一种用于实时和嵌入式系统发布-订阅式通信的工业标准,它提供了点对点的通信模式,不需要像ros1.0那样借由master节点来完成两个节点间通信,这使得系统更加容错和灵活。

  • 接口层: ros2.0提供了两个主要的接口层,分别是rmw (ros middleware interface) 和rd (ros dient libraries) 。rmw是相对底层的接口层,直接和DDS交互,C语言实现:rc是对mw相对高层的抽象,C/C++实现。此外,还有一个ros to dds组件,主要为用户直接访问DDS层提供接口。

  • 应用层: ros2.0支持用C++或者Python来编写应用程序,也支持其他语言的绑定。应用程序可以通过r或者ros to dds来调用中间层的功能。

如果项目需要实时性和分布通信支持,ROS 2.0提供了更好的解决方案

安装

安装参考 安装ROS2的过程,唯一不同地方是我没有遇到 2.3 中的报错。

截至目前有两个ROS 2的版本还没有到项目终止日期 (EOL end-of-life),一个是最新的发布版本为Iron Irwini,发布时间是2023年5月23日,EOL date是2024年11月,另一个是Humble Hawksbil,发布时间是2022年5月23日,EOL date是到2027年5月,比较后我选择了Humble这个版本来学习。

ament_cmake是cmake的增强版

ROS2安装指定包,和ROS1一样,例如 sudo apt-get install ros-humble-irobot-create-msgs


ubuntu配置CUDA
  • Ensure there is enough space in /tmp and that the installation package is not corrupt

刚开始安装./cuda_12.1.1_530.30.02_linux.run时, /所在的/dev/sdb7所剩空间越来越小,最终安装失败,提示 Ensure there is enough space in /tmp and that the installation package is not corrupt。 所以必须先扩展/所在的硬盘容量

按这个步骤安装,一般不会出问题: ubuntu18.04上cuda及cudnn安装


交叉编译 PCL

在x86架构的Ubuntu22.04交叉编译PCL源码,以能在ARM aarch64系统上运行,也就是全志MR527的TinaLinux 5.15.123 aarch64

先看完成的状态,也就是ARM平台的PCL库文件链接关系:

libpcl_kdtree.so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ldd  /usr/lib/aarch64-linux-gnu/libpcl_kdtree.so
linux-vdso.so.1 (0x0000ffff8d240000)

libpcl_common.so.1.10 => /lib/aarch64-linux-gnu/libpcl_common.so.1.10 (0x0000ffff8cfb7000)
liblz4.so.1 => /lib/aarch64-linux-gnu/liblz4.so.1 (0x0000ffff8cf89000)


libstdc++.so.6 => /lib/aarch64-linux-gnu/libstdc++.so.6 (0x0000ffff8cda4000)
libgomp.so.1 => /lib/aarch64-linux-gnu/libgomp.so.1 (0x0000ffff8cd56000)
libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1 (0x0000ffff8cd32000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff8cbbf000)
/lib/ld-linux-aarch64.so.1 (0x0000ffff8d210000)
libm.so.6 => /lib/aarch64-linux-gnu/libm.so.6 (0x0000ffff8cb14000)
libpthread.so.0 => /lib/aarch64-linux-gnu/libpthread.so.0 (0x0000ffff8cae3000)
libdl.so.2 => /lib/aarch64-linux-gnu/libdl.so.2 (0x0000ffff8cacf000)

libpcl_common.so

1
2
3
4
5
6
7
8
9
10
ldd  /usr/lib/aarch64-linux-gnu/libpcl_common.so
linux-vdso.so.1 (0x0000ffff975e8000)
libstdc++.so.6 => /lib/aarch64-linux-gnu/libstdc++.so.6 (0x0000ffff972d0000)
libm.so.6 => /lib/aarch64-linux-gnu/libm.so.6 (0x0000ffff97225000)
libgomp.so.1 => /lib/aarch64-linux-gnu/libgomp.so.1 (0x0000ffff971d7000)
libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1 (0x0000ffff971b3000)
libpthread.so.0 => /lib/aarch64-linux-gnu/libpthread.so.0 (0x0000ffff97182000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff9700f000)
/lib/ld-linux-aarch64.so.1 (0x0000ffff975b8000)
libdl.so.2 => /lib/aarch64-linux-gnu/libdl.so.2


交叉编译 PCL

在终端找到/opt/cmake/bin,启动cmake-gui,这里是新安装的cmake 3.27. 选择PCL的源码目录和build目录,选择交叉编译平台。没有显示需求,所以cmake-gui不需要选择 OpenGL和VTK

The C compiler identification is unknown

既然是交叉编译,那就得用跨平台的编译器,注意在启动cmake-gui的终端要提前设置环境变量:

1
2
3
4
5
export ARCH=arm64
export TOOLCHAIN_PATH=/home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu
export LD_LIBRARY_PATH=/home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH
export CROSS_COMPILE=/home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/bin/aarch64-linux-gnu-

不重要的报警

configure PCL时出现的部分信息,可以无视

1
2
3
4
Could NOT find PkgConfig (missing: PKG_CONFIG_EXECUTABLE)                 # OMIT
Could NOT find LIBUSB_1 (missing: LIBUSB_1_LIBRARY LIBUSB_1_INCLUDE_DIR) # OMIT
Could NOT find ZLIB (missing: ZLIB_LIBRARY ZLIB_INCLUDE_DIR)
Could NOT find PNG (missing: PNG_LIBRARY PNG_PNG_INCLUDE_DIR)

编译的过程中,发现需要处理Boost, Flann, lz4, hdf5等库,它们也需要交叉编译。

交叉编译Boost

PCL需要Boost,cmake时会提示无法找到Boost的错误,不是本机的x86 Boost,是aarch64的Boost,也就是说需要交叉编译Boost。

下载Boost1.74源码,找到bootstrap.sh,然后执行

1
./bootstrap.sh  --with-libraries=system,filesystem,thread,date_time,iostreams --with-toolset=gcc 

修改project-config.jam文件,把using gcc部分修改为
using gcc : arm : /home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc ;

找到gcc.jam文件,在430行左右,增加一行

1
2
3
4
5
6
# On Windows, fPIC is the default, and specifying -fPIC explicitly leads
# to a warning.
local non-windows = [ set.difference $(all-os) : cygwin windows ] ;
compile-link-flags <link>shared/<target-os>$(non-windows) : -fPIC ;
# 增加下面这行
compile-link-flags <link>static/<target-os>$(non-windows) : -fPIC ;

此步骤的主要目的是打开-fPIC,避免PCL在编译时找不到boost库的.a文件

进行编译并安装boost:

1
sudo ./b2 cxxflags=-fPIC cflags=-fPIC -a install

编译时可能会报错: error while loading shared libraries: libisl.so.22: cannot open shared object file: No such file

将交叉编译的toolschain地址的这几个库文件都复制到/usr/lib/x86_64-linux-gnu: libctf-arm64.so.0, libopcodes-2.34-arm64.so, libbfd-2.34-arm64.so, libisl.so.22。 如果没报错就不用了。

编译结束,我把生成的Boost库的文件都放到了/usr/local/lib。也就是/usr/lib/x86_64-linux-gnu中的libboost开头的库文件仍是x86的,/usr/local/lib中的libboost开头的库文件是AArch64架构。对于ARM的so库文件,在x86上用ldd命令查看会出现 not a dynamic executable

修改PCL中的pcl_find_boost.cmake文件

1
2
3
4
set(Boost_DIR  "/usr/lib/x86_64-linux-gnu/cmake")
# Required boost modules
set(BOOST_REQUIRED_MODULES filesystem date_time iostreams system)
find_package(Boost 1.74.0 REQUIRED COMPONENTS ${BOOST_REQUIRED_MODULES})

lz4 1.9.1 arm64版本的交叉编译

下载源码后执行

1
2
3
make CC=/home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc
mkdir lz4_arm
make install PREFIX=$(pwd)/lz4_arm

运行ldconfig, 出现 /sbin/ldconfig.real: /lib/x86_64-linux-gnu/liblz4.so.1 is not a symbolic link

运行file /lib/liblz4.so,出现liblz4.so: broken symbolic link to liblz4.s

目前解决: 把地平线x3M的 /lib/liblz4.so/lib/liblz4.so.1 复制到我的本机的 /lib目录, /usr/lib/x86_64-linux-gnu的几个 liblz4.so 仍然是 x86 平台。

hdf5的问题

编译flann时,链接hdf5出错

1
2
3
/home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/bin/../lib/gcc-cross/aarch64-linux-gnu/9/../../../../aarch64-linux-gnu/bin/ld:   /usr/lib/x86_64-linux-gnu/hdf5/serial/libhdf5.so:   error adding symbols: file in wrong format

collect2: error: ld returned 1 exit status

显然需要的是ARM的hdf5,这个库的交叉编译特别麻烦,最后解决方法是从一台ARM平台的主机上找到 hdf5文件夹,放到了 /usr/lib/x86_64-linux-gnu

如果交叉编译hdf5,参考:

  1. 使用cmake-gui进行configure和generate,第一次configure会失败,重新configure即可
  2. 进行make,两次,编译失败
  3. 拷贝bin/H5detect bin/H5make_libsettings libhdf5.settings到arm平台
  4. 在arm平台修改文件执行权限,执行H5detect和H5make_libsettings,把程序输出分别保存到H5Tinit.c和H5lib_settings.c
  5. 拷贝H5Tinit.c和H5lib_settings.c到主机的编译目录下,继续编译

交叉编译flann库

在一台ARM平台的设备上,flann库文件是这样的

1
2
3
4
5
6
7
8
9
/usr/lib/aarch64-linux-gnu/libflann.so
/usr/lib/aarch64-linux-gnu/libflann.so.1.9
/usr/lib/aarch64-linux-gnu/libflann.so.1.9.1

/usr/lib/aarch64-linux-gnu/libflann_cpp.so
/usr/lib/aarch64-linux-gnu/libflann_cpp.so.1.9
/usr/lib/aarch64-linux-gnu/libflann_cpp.so.1.9.1
/usr/lib/aarch64-linux-gnu/libflann_cpp_s.a
/usr/lib/aarch64-linux-gnu/libflann_s.a

链接关系:
libflann.so
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ldd /usr/lib/aarch64-linux-gnu/libflann.so
linux-vdso.so.1 (0x0000ffff85887000)
libstdc++.so.6 => /lib/aarch64-linux-gnu/libstdc++.so.6 (0x0000ffff8511b000)
libm.so.6 => /lib/aarch64-linux-gnu/libm.so.6 (0x0000ffff85070000)
# 缺
libgomp.so.1 => /lib/aarch64-linux-gnu/libgomp.so.1 (0x0000ffff85022000)

libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1 (0x0000ffff84ffe000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff84e8b000)
/lib/ld-linux-aarch64.so.1 (0x0000ffff85857000)
# 目前缺这两个
libdl.so.2 => /lib/aarch64-linux-gnu/libdl.so.2 (0x0000ffff84e77000)
libpthread.so.0 => /lib/aarch64-linux-gnu/libpthread.so.0 (0x0000ffff84e46000)


ldd /usr/lib/aarch64-linux-gnu/libflann_cpp.so
linux-vdso.so.1 (0x0000ffff9fb1f000)
# 目前需要交叉编译
liblz4.so.1 => /lib/aarch64-linux-gnu/liblz4.so.1 (0x0000ffff9fa68000)
libstdc++.so.6 => /lib/aarch64-linux-gnu/libstdc++.so.6 (0x0000ffff9f883000)
# 目前缺
libpthread.so.0 => /lib/aarch64-linux-gnu/libpthread.so.0 (0x0000ffff9f852000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff9f6df000)
/lib/ld-linux-aarch64.so.1 (0x0000ffff9faef000)
libm.so.6 => /lib/aarch64-linux-gnu/libm.so.6 (0x0000ffff9f634000)
libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1

如果之前缺hdf5,编译会报警 hdf5 library not found, not compiling flann_example.cpp

解决方法: 把ARM平台的的flann库文件复制到了 /usr/lib/x86_64-linux-gnu,原来的x86的flann库文件复制到了新文件夹 /usr/lib/x86_64-linux-gnu/x86_flann

如果要交叉编译,需要修改CMakeLists:

1
2
3
4
5
pkg_check_modules(LZ4 REQUIRED liblz4)
#include_directories(${LZ4_INCLUDE_DIRS})
include_directories(/home/user/Downloads/lz4-1.9.1/lz4_arm/include)

link_directories(/home/user/Downloads/lz4-1.9.1/lz4_arm/lib)


现在继续使用cmake-gui交叉编译PCL,在界面中没有选择所有模块,执行ConfigureGenerate后,会有日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
编译的模块:
common kdtree octree search
sample_consensus filters 2d
geometry io features ml
segmentation surface keypoints
tracking stereo tools

不会编译的模块:
visualization: VTK was not found.
registration: Disabled manually.
recognition: Requires registration.
apps: Disabled: registration missing.
outofcore: Requires visualization.
examples: Code examples are disabled by default.
people: Requires visualization.
simulation: Disabled: visualization missing.
global_tests: No reason
tools: Disabled: registration missing.

然后去build文件夹找每个模块的Makefile,比如pcl-pcl-1.10.0/build/kdtree,然后执行make即可。

PCL交叉编译得到的so文件特别大,有的到了800M。使用strip瘦身,结果报错 strip: Unable to recognise the format of the input file ,因为so文件是aarch64的,应当使用gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/aarch64-linux-gnu/bin/strip,结果让libpcl_features.so.1.10.0从797M 下降到 39M。

所有编译生成的库文件在pcl-pcl-1.10.0/build/lib,把这些库文件放到开发板上,比如路径/pcl_libs,所有Boost的库文件也放过去,比如/boost_libs


测试程序

在本机上写一个PCL的测试程序,CMake部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
cmake_minimum_required(VERSION 3.5)

project(test_cross_pcl LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Boost COMPONENTS system filesystem thread REQUIRED)

include_directories(
include
/usr/include/eigen3

/home/user/Downloads/pcl-pcl-1.10.0/common/include
/home/user/Downloads/pcl-pcl-1.10.0/build/include
/home/user/Downloads/pcl-pcl-1.10.0/io/include
/home/user/Downloads/pcl-pcl-1.10.0/filters/include
)
LINK_DIRECTORIES(/home/user/Downloads/pcl-pcl-1.10.0/build/lib)
LINK_DIRECTORIES(/home/user/toolschain/gcc-ubuntu-9.3.0-2020.03-x86_64-aarch64-linux-gnu/aarch64-linux-gnu/lib)

add_executable(test_cross_pcl main.cpp)
target_link_libraries(test_cross_pcl
-lpcl_common
-lpcl_io
-lpcl_filters
-lpthread
)

不能使用本机上的编译器,还用cmake-gui进行交叉编译: configure, generate, 然后到对应的build文件夹里执行make,把生成的可执行文件放到开发板。

执行export LD_LIBRARY_PATH=/pcl_libs:/boost_libs:$LD_LIBRARY_PATH,也就是指定链接库的地址,再执行就成功了。

参考:Windows 10上源码编译PCL 1.8.1支持VTK和QT,可视化三维点云
交叉编译Boost

解决vcpkg无法交叉编译arm64版本 HDF5 库的问题
编译HDF5
Linux安装HDF5及遇到的问题总结

FLANN 1.9.2 源码编译
PCL与VTK


判断文件是否存在
1
2
3
4
5
6
7
8
#include <fstream>

std::ifstream fin(file_name);
if (!fin) {
printf("file not exist");
return false;
}
fin.close();

C++17中有std::filesystem::path,相当于Boost的filesystem模块,只需要set(CMAKE_CXX_STANDARD 17)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;


int main()
{
std::filesystem::path test_path("/home/user/test.yaml");
//判断路径是否存在
std::cout << "is_exist = " << std::filesystem::exists(test_path) << std::endl;

// 判断路径为绝对路径还是相对路径
std::cout << "is_abs = " << test_path.is_absolute() << std::endl;
std::cout << "is_relative = " << test_path.is_relative() << std::endl;
// 取得不带扩展名的文件名
std::cout << "stem = " << test_path.stem() << std::endl;
// 取得扩展名,如果没有,则为空
std::cout << "extension = " << test_path.extension() << std::endl;
// 取得文件名
std::cout << "filename = " << test_path.filename() << std::endl;

std::cout << "----------------------------------------" << std::endl;

std::filesystem::path test_dir("/home/user/Pictures");
//判断是文件还是文件夹
std::cout << "is_file = " << std::filesystem::is_regular_file(test_path) << std::endl;
std::cout << "is_dir = " << std::filesystem::is_directory(test_dir) << std::endl;

//关于路径拼接
std::filesystem::path new_path = test_dir / "test.txt";
std::cout << "new_path = " << new_path << std::endl;

//取得当前绝对工作路径 编译后可执行文件的路径文件夹
std::filesystem::path workPath = std::filesystem::current_path();
std::cout << "current exe path = " << workPath << std::endl;

//递归遍历指定路径下所有文件,文件夹名字也会返回
std::filesystem::recursive_directory_iterator iterDir(test_dir);
for (auto &it: iterDir)
{
//打印绝对路径
std::cout << it << std::endl;
// 不打印路径而是打印文件名
std::cout << it.path().filename() << std::endl;
}

//给一个相对路径,返回绝对路径
std::filesystem::path path_test("./");
std::cout << "abs_path = " << std::filesystem::absolute(path_test) << std::endl;


// 删除文件, 路径不存在不报错
// std::filesystem::remove(test_dir / "deepfakes"/"yolov7.pdf");

// 拷贝文件夹, 删除文件夹
// std::filesystem::copy(test_dir, test_dir.parent_path() / "works", std::filesystem::copy_options::recursive);
// std::filesystem::remove_all(test_dir.parent_path() / "works");
return 0;
}


yaml-cpp的使用

使用yaml-cpp读文件

1
2
3
4
5
6
7
8
9
10
YAML::Node gps_root;
try {
gps_root = YAML::LoadFile("/home/user/111.yaml");
} catch (YAML::ParserException &ex) {
std::cerr << "gps.yaml parse failed: " << ex.what() << std::endl;
exit(-1);
} catch (YAML::BadFile &ex) {
std::cerr << "gps.yaml load failed: " << ex.what() << std::endl;
exit(-1);
}

所用函数的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Node LoadFile(const std::string& filename) {
std::ifstream fin(filename.c_str());
if (!fin) {
throw BadFile();
}
return Load(fin);
}

Node Load(std::istream& input)
{
Parser parser(input);
NodeBuilder builder;
if (!parser.HandleNextDocument(builder)) {
return Node();
}

return builder.Root();
}

如果读文件失败,会抛出异常,不会运行到Load,所以无法用IsDefined函数判断是否读文件成功。