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"

参考:


文本操作和读写文件

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

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


雷达的拖尾现象和处理
abstract Welcome to my blog, enter password to read.
Read more
ssh自动登录远程机和expect的使用

从本机连接到远程机,发起SSH连接,执行命令cd ~/Downloads;ls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash

# router parameters
ROUTER_USER="xiaoqiang"
ROUTER_HOST="192.168.0.110"
ROUTER_PASSWORD="1"
ROUTER_PORT="22" # default

# start except
expect <<-EOF
set timeout -1
spawn ssh $ROUTER_USER@$ROUTER_HOST -p $ROUTER_PORT -o StrictHostKeyChecking=no -C "cd ~/Downloads;ls"
expect "*password:"
send "$ROUTER_PASSWORD\n\r\n"
expect eof;
EOF

首次远程连接时,会提示公钥确认,远程IP改变时,会提示公钥不一致,这种情况有时非常烦人。对于自动化任务,我们希望跳过公钥确认,修改/etc/ssh/ssh_config,加入:
1
StrictHostKeyChecking no

或者在命令行中使用-o参数: ssh -o StrictHostKeyChecking=no

运行结果:

1
2
3
4
5
6
7
spawn ssh xiaoqiang@192.168.0.110 -p 22 -o StrictHostKeyChecking=no -C cd ~/Downloads;ls
xiaoqiang@192.168.0.110's password:
c-ares-1.17.0.tar.gz
cd1folder.zsh
CursorTheme
darcula.css
g2o_curve_fitting

在password一行之后为命令执行的结果,一直没法实现换行,不太完美


修改roslaunch源码
abstract Welcome to my blog, enter password to read.
Read more
编译链接 ${catkin_LIBRARIES} 的问题

编译xvisio相机驱动时,遇到了这样的错误

显然是ddynamic_reconfigure包的问题,于是安装对应的ROS包,但是问题依旧,真是奇怪

于是在cmake中使用MESSAGE(${catkin_LIBRARIES})看看链接的文件有什么问题,结果发现其他的ROS库都是链接到了ROS安装目录,比如 /opt/ros/noetic/lib/libcamera_info_manager.so/usr/lib/x86_64-linux-gnu/libtinyxml2.so。 唯独ddynamic_reconfigure链接到了我之前放的库文件: /home/user/catkin_ws/devel/lib/libddynamic_reconfigure.so

这真是太奇怪了,环境变量catkin_LIBRARIES是如何换到另一个so库文件的??? 于是我把ROS安装目录的libddynamic_reconfigure.so覆盖catkin_ws的so库文件,重新编译成功了,相机驱动可以正常运行。

但是这样其实没有解决根本问题,最后只好把target_link_libraries${catkin_LIBRARIES}换成一个个的so文件,在开头要LINK_DIRECTORIES(/opt/ros/noetic/lib)


cartographer的杂项模块

detect_floors.cc

detect_floors.h定义了关于3D扫描楼层的数据结构。

1
2
3
4
5
6
7
8
9
10
struct Timespan {
common::Time start;
common::Time end;
};
Timespan表征扫描的时间范围。

struct Floor {
std::vector< Timespan> timespans;
double z; //z轴的中值
};

一个楼层对应多个扫描timespan:有可能重复的扫描多次,但只有一个高度z。

std::vector DetectFloors(const proto::Trajectory& trajectory); 使用启发式搜索寻找building的不同楼层的z值。

normal_estimation_2d.cc

用于计算TSDF地图中的点云法向量,可以不看。 normal_estimation_2d_test.cc是测试文件,但是没有在CMakeLists中设置 。


优雅地订阅话题

这段代码其实是模仿cartographer的LaunchSubscribers函数,订阅多个传感器的数据话题,同时可以获得主函数里多个参数的赋值

主函数长这样

1
2
3
4
5
6
7
8
int main(int argc, char** argv)
{
ros::init(argc, argv, "launch_subscribers");
ros::NodeHandle nh;
Node node;
node.LaunchSubscribers("base_scan", 1, "/base_odometry/odom", 1, "/torso_lift_imu/data", 1);
ros::spin();
}

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
void Node::LaunchSubscribers(const std::string& scan_topic, const int scan_num,
const std::string& odom_topic, const int use_odom,
const std::string& imu_topic, const int use_imu
)
{
if(scan_num)
LaunchLaserScanSub(scan_topic, scan_num);
if(use_odom)
LaunchOdomSub(odom_topic, use_odom);
if(use_imu)
LaunchImuSub(imu_topic, use_imu);
}

void Node::LaunchLaserScanSub(const std::string& scan_topic, const int scan_num)
{
subscribers_.push_back(
{ SubscribeWithHandler<sensor_msgs::LaserScan>(
&Node::HandleLaserScanMessage,
scan_num, scan_topic, &node_handle_, this),
scan_topic} );
}

// 这里也可以把 int 改为 bool
template <typename MessageType>
::ros::Subscriber SubscribeWithHandler(
void (Node::*handler)(int, const std::string&,
const typename MessageType::ConstPtr&),
const int num, const std::string& topic,
::ros::NodeHandle* const node_handle, Node* const node)
{
return node_handle->subscribe<MessageType>(
topic, 1,
boost::function<void(const typename MessageType::ConstPtr&)>(
[node, handler, num,
topic](const typename MessageType::ConstPtr& msg) {
(node->*handler)(num, topic, msg);
}) );
}

// 根据上面的void (Node::*handler)部分,参数依次为int, string&, MessageType::ConstPtr&
// scan_num 就是main函数里的1
void Node::HandleLaserScanMessage(int scan_num, const std::string& sensor_id,
const sensor_msgs::LaserScan::ConstPtr& msg)
{
ROS_INFO("range_min: %f, scan_num: %d", msg->range_min, scan_num );
}