laser_line_extraction

论文

论文A comparison of line extraction algorithms using 2D range data for indoor mobile robotics对比了6种提取雷达数据线条算法的效果。结论如下:

Split-and-MergeIncremental由于处理速度和准确性而表现最好。 对于实时的应用,Split-and-Merge是最佳选择,因为处理速度很快。它也是地图定位问题的第一选择,此时FalsePos不太重要。但是,算法实现的细节对应用是很重要的。

对于laser_line_extraction对应的03年论文,这篇论文在介绍Split-and-Merge时没有提到,只在Related Work里提了一下。但是laser_line_extraction的介绍里声称是使用了Split-and-Merge算法,还有待观察。

以及2017年的论文 A line segment extraction algorithm using laser data based on seeded region growing,效果在Line Segment Extraction

原理


对雷达扫描的结果,如果形状接近直线,就进行连线。

先对离群点进行过滤,再执行split-and-merge算法决定哪些点属于线,然后使用weighted line fitting算法找到最合适的线和相应的协方差矩阵。

使用roslaunch laser_line_extraction example.launch,修改参数以符合自己的需求。

参数

  • bearing_std_dev (default: 0.001): The standard deviation of bearing uncertainty in the laser scans (rad).

  • min_split_dist (default: 0.05): When performing “split” step of split and merge, a split between two points results when the two points are at least this far apart (m) 线段split的阈值,过大时很多线段被合并成一条,过小时,出现很多碎短的线段

  • outlier_dist (default: 0.05): Points who are at least this distance from all their neighbours are considered outliers (m).

  • range_std_dev (default: 0.02): The standard deviation of range uncertainty in the laser scans (m).

1
2
3
4
5
float32 radius
float32 angle
float32[4] covariance
float32[2] start
float32[2] end
  • start和end分别是这个线段的起点坐标和终点坐标。
  • angle: 原点到直线的垂线的角度,角度以机器人为坐标系计算,范围是-PI~PI
  • radius: 原点到直线的距离

这两个参数完全能够根据start和end点计算出来。不理解这两个参数存在的意义。

covariance是2X2的矩阵,是radius和angle的协方差

line_extractor

line_extractor发布:

1
2
3
4
5
6
7
8
camera_line_markers     [visualization_msgs/Marker]
camera_line_segments [laser_line_extraction/LineSegmentList]
line_markers [visualization_msgs/Marker]
line_segments [laser_line_extraction/LineSegmentList]

taimi/line_center_point [nav_msgs/Odometry]
taimi/line_wall_position [geometry_msgs/PoseStamped]
taimi/waste_aruco_position [geometry_msgs/PoseStamped]

订阅:

1
2
3
4
5
camera1/scan [sensor_msgs/LaserScan]
shelf_lifter/cmd [std_msgs/String]
sick_tim551_scan [sensor_msgs/LaserScan]
tf [tf2_msgs/TFMessage]
tf_static

参考:室内移动机器人二维激光数据线特征提取算法的总结与开源算法分享


对纯跟踪算法的改进
Welcome to my blog, enter password to read.
Read more
ROS2的节点

有顺序的的启动节点,暂停节点,关闭节点是ROS1的一个痛点。因为在ROS1中节点启动是无序的。ROS1系统设计时并没有考虑节点启动时可能存在的互相依赖。

但在实际生产使用环境中,某些节点能正常工作是需要其他一些节点已经启动了的。

你想从建图功能切到导航功能。在ROS1中,需要先将建图功能的程序杀干净。然后再启动导航程序。在ROS2中,各个节点的状态是可管理的。

在这个场景里,大可让建图程序休眠,而不用杀掉。切换功能时只需要激活相应功能的节点即可。

ROS2中引入了节点生命周期管理的概念,正是为了处理上面描述的问题。这项功能的实现需要做下面两件事情。

继承LifecycleNode 来实现自己的节点
将节点名称注册到Lifecycle Manager 中,由它来管理各个节点的状态
实现一个功能通常需要一些节点互相配合来实现。这样的话,我们可以将某一个功能涉及到的节点使用一个Lifecycle Manager 程序来管理。从而实现了起停一项功能的效果。

ROS2中提供了两种节点类型。

Node() 是和ROS1中一样的节点基类
LifecycleNode() 是可管理状态的节点基类

LifecycleNode 类型的节点可以处于下面几种状态:

Unconfigured
Inactive
Active
Finalized

参考: LifecycleNode管理节点起停等状态


配置万集716雷达

万集的716雷达马上会停产。
万集WLR-716

记录一下配置的过程。雷达只有网口和24V的电源线,红正黑负。

windows上的上位机配置,按照说明书进行就可以了,没什么复杂的。只看ubuntu的配置过程

插上网线后,找到有线网络的参数。这是ubuntu的参数,不是雷达的
有线网络显示的参数
到IPV4页面进行配置,MAC地址也要配置,可能填192.168.0.4会ping不同雷达,那就换别的IP试试

ping通雷达后,启动launch应该没问题了
launch成功启动


强度信息



加入反光板后
此时的强度信息

测试发现,距离反光板小于1.8m时,强度都是最大值1500。之后距离增大,强度值减小,当距离4.4m时,强度值仍在900以上。 看来雷达对高反物体还是很敏感的。

网上声称万集718的角分辨率降到0.25°,对不锈钢面、货架腿的识别效果极好,能达到30mm精度。但是我查万集的官网发现WLR-718角分辨率还是0.33°,不知这说的是不是WLR-718 Mini,如果是真的,那么对拖尾现象会避免很好。 对强光、雨雾、对射干扰,能滤除干扰回波,测量很可靠。


transform和back_inserter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::string s("Hello");
std::string f;

// 前两个参数是变换的范围,第三个是变换的起始位置
// 第四个参数为lambda表达式,变换的规则
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::toupper(c); });
std::cout <<"after transform, s: "<< s << std::endl; // HELLO

std::transform( s.begin(), s.end(), std::back_inserter(f),
[](unsigned char c) { return std::tolower(c); } );
std::cout << "after transform, f: " << f << std::endl; // hello

std::copy( s.begin(), s.end(), std::back_inserter(f) );
std::cout << "after copy, f: " << f << std::endl; // helloHELLO

back_inserter属于插入迭代器,有三种插入迭代器(back_inserter,inserter,front_inserter),插入迭代器是指被绑定在一个容器上,可用来向容器插入元素的迭代器。 back_inserter是创建一个使用push_back的迭代器

1
2
3
vector<int> v={1,2,3,4,5,6};
vector<int> s;
copy(v.begin(), v.end(), back_inserter(s) ); // s: 123456

追加数据比较适合这种copy+back_inserter的做法。如果把新数据转为string,再用string的+运算符,这就太复杂了。数据很有可能是不以'\0'结尾,这样的话将出现严重bug。


tf2的学习(二)

有次我如此使用:

1
2
3
tf2_ros::Buffer buffer_;
tf2_ros::TransformListener listener(buffer_);
buffer_.transform(in_pose, out_pose, frame, transform_duration_);

结果报错: Exception in transformPose: “map” passed to lookupTransform argument source_frame does not exist

结果发现还是这个问题,我的用法其实相似,最好是把 buffer_listener都作为成员变量(指针),在构造函数里初始化listenner: listener = new tf2_ros::TransformListener(buffer_);, transform函数的使用就比较自由了

问题

使用tf2_ros::Buffer.transform()tf2_ros::Buffer.lookupTransform() 导致 “undefined reference” errors

解决: #include "tf2_geometry_msgs/tf2_geometry_msgs.h"

参考:


悬崖检测 防跌落
abstract Welcome to my blog, enter password to read.
Read more
解析map_server和Image Truncated的报错

话题map应该由map_server发布,而不是SLAM算法发布。代价地图会订阅map话题,如果由SLAM发布,订阅的内容恐怕错误,甚至是空消息。

MapServer的代码,我简化了读yaml的部分

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class MapServer
{
public:
/** Trivial constructor */
MapServer(const std::string& fname, double res)
{
std::string mapfname = "";
double origin[3];
int negate;
double occ_th, free_th;
MapMode mode = TRINARY;
std::string frame_id;
ros::NodeHandle private_nh("~");
private_nh.param("frame_id", frame_id, std::string("map"));
deprecated = (res != 0);
if (!deprecated)
{
// 省略读 yaml 文件的代码,其实就是把yaml里的各个参数赋值给变量
#ifdef HAVE_YAMLCPP_GT_0_5_0
// The document loading process changed in yaml-cpp 0.5.
YAML::Node doc = YAML::Load(fin);
#else
YAML::Parser parser(fin);
YAML::Node doc;
parser.GetNextDocument(doc);
#endif
std::string modeS = "";
doc["mode"] >> modeS;

if(modeS=="trinary")
mode = TRINARY;
else if(modeS=="scale")
mode = SCALE;
else if(modeS=="raw")
mode = RAW;
else mode = TRINARY;

ROS_INFO("Loading map from image \"%s\"", mapfname.c_str());
// 这个核心函数是最底层的,使用SDL库解析图片
map_server::loadMapFromFile(&map_resp_,mapfname.c_str(),res,negate,occ_th,free_th, origin, mode);

// To make sure get a consistent time in simulation
ros::Time::waitForValid();

map_resp_.map.info.map_load_time = ros::Time::now();
map_resp_.map.header.frame_id = frame_id;
map_resp_.map.header.stamp = ros::Time::now();
ROS_INFO("Read a %d X %d map @ %.3lf m/cell",
map_resp_.map.info.width,
map_resp_.map.info.height,
map_resp_.map.info.resolution);
meta_data_message_ = map_resp_.map.info;

service = n.advertiseService("static_map", &MapServer::mapCallback, this);

metadata_pub= n.advertise<nav_msgs::MapMetaData>("map_metadata", 1, true);
metadata_pub.publish( meta_data_message_ );

map_pub = n.advertise<nav_msgs::OccupancyGrid>("map", 1, true);
map_pub.publish( map_resp_.map );
}
private:
ros::NodeHandle n;
ros::Publisher map_pub;
ros::Publisher metadata_pub;
ros::ServiceServer service;
bool deprecated;

bool mapCallback(nav_msgs::GetMap::Request &req,
nav_msgs::GetMap::Response &res )
{
// request is empty; we ignore it
// = operator is overloaded to make deep copy (tricky!)
res = map_resp_;
ROS_INFO("Sending map");
return true;
}
// The map data is cached here, to be sent out to service callers
nav_msgs::MapMetaData meta_data_message_;
nav_msgs::GetMap::Response map_resp_;
};
}

这里最核心的函数是map_server::loadMapFromFile,再看它的源码,发现使用的是SDL库读图片,核心是函数SDL_Surface * pSurfaceTemp = IMG_Load(file_name.c_str());, IMG_Load()可以读取多种格式的图片文件,包括pgm。 到此为止,再深层的没必要了解了。

地图的yaml文件中,`origin`的绝对值很大很大时,地图不能正常显示。

报错: Image Truncated

报错 [map_server-1] process has died [pid 635, exit code 255

首先修改 yaml 中的 origin: [0.000000, 0.000000, -nan]origin: [0.000000, 0.000000, 0],结果仍然报错

发现用Gimp也无法打开,报错 PNM Image plug-in could not open image,在Properties里看不出有什么特殊。

最终发现是 VINS 生成地图时,所用的点没有加时间戳

报错: failed to open image file “/home/user/map.pgm”: file truncated

用图片工具打开,发现pgm全黑,建图问题

无法针对pgm生成代价地图

pgm文件,为了保证边缘有黑边,只给一圈黑色像素不够,给4个黑色像素。


文本操作和读写文件

对字符串根据字符进行分段

1
2
3
4
5
6
7
8
9
vector<std::string> params;
std::string file = "123.45.abc.0";
std::istringstream param_stream(file);
std::string param;
while(getline(param_stream, param, '.'))
{
cout << param <<endl;
params.push_back(param);
}

运行结果:

1
2
3
4
123
45
abc
0

读Excel文件

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
bool readCSVInput(int N, vector<double> &input_points_x, vector<double> &input_points_y, vector<double> &anchor_points_x, vector<double> &anchor_points_y)
{
ifstream readCSV("/home/user/data.csv", ios::in);
int lineNum = 0;
string lineString;
vector<vector<string>> stringArray;
cout << endl;
while (getline(readCSV, lineString) )
{
//cout << lineString << endl;
if (lineNum > 0)
{
stringstream ss(lineString);
string s;
vector<string> oneLineStrings;
while (getline(ss, s, ',') )
oneLineStrings.push_back(s);
if (oneLineStrings.size() != 5)
{
cout << "ERROR:oneLineStrings.size() != 5" << endl;
return false;
}
else
{
input_points_x.push_back(std::stod(oneLineStrings[1]));
input_points_y.push_back(std::stod(oneLineStrings[2]));
anchor_points_x.push_back(std::stod(oneLineStrings[3]));
anchor_points_y.push_back(std::stod(oneLineStrings[4]));
}
}
lineNum++;
}
if (N == input_points_x.size())
{
return true;
}
else
{
input_points_x.clear();
input_points_y.clear();
anchor_points_x.clear();
anchor_points_y.clear();
cout << "ERROR:N == input_points_x.size()" << endl;
return false;
}
}

使用多个相机的记录

Xvisio相机

按照指导书进行配置即可,需要注意的只有noetic的配置有些不同。
启动驱动

在xvisio_viewer查看SLAM的信息
我模仿驱动里的demo-api,写了一个程序获得SLAM位姿,跟xvisio_viewer的效果相同。发布的话题vslam_pose的频率足够满足需求。

xvisio相机启动时,必须面对有特征的环境,否则之后会一直不能获取SLAM信息。在相机移动过程中也要尽量面对有特征环境,如果面对纯色的环境,会失败,和遮挡镜头是一样的。 但即使这样,偶尔也有失败的情况。

反复测试后,感觉这个相机不适合我的需求,只好放弃了。

奥比中光-大白相机

我直接修改了奥比相机的驱动,从源头直通滤波、体素滤波、统计滤波。否则比较远的点云(比如3m以上)乱七八糟还极不稳定,根本不需要。另外两个方向的点云,坐标值比较大的时候,也极不稳定,没有使用的必要。

原始点云256000个点,滤波后,最多20000+个点。

装在机器人上,移动时,偶尔收不到点云数据

realsense D435f

比D435增加一个750nm 近红外滤光片(厚度0.5mm, CLAREX NIR-75N),一个mask。 红外滤光片(IR pass filter)装在相机的前端,两个孔对应红外发射和RGB相机。减少了光反射和阳光引起的深度噪声,它可传播近红外光和吸收可见光。

D435 和 D435f的对比
我做了很多试验,发现D435f确实好很多,不仅在阳光和有光反射情况下,连普通环境也减少了空洞现象。

D435f的射程收到红外发射器的限制,校正可能需要额外的红外补光。

考虑自己买750nm的滤光片装在D435上,淘宝发现很小的一块要80,真是贵。

参考: realsense-d435f


肇观电子的N1(价格 350)和M1(价格 750)相机

M1 主动双目,空洞率少。 N1 被动双目,空洞太多,想使用深度图和点云就不必考虑了。


mipi转USB