整个项目是用ROS环境下的catkin make进行编译的,初学者主要关注include、launch、rviz_cfg、src文件夹和README、CMakeLists、package文件。
CMakeLists文件:
find_package(catkin REQUIRED COMPONENTS
geometry_msgs
nav_msgs
sensor_msgs
roscpp
rospy
rosbag
std_msgs
image_transport
cv_bridge
tf
)
该框架使用了ROS自带的package。
find_package(PCL REQUIRED)
find_package(OpenCV REQUIRED)
find_package(Ceres REQUIRED)
include_directories(
include
${catkin_INCLUDE_DIRS}
${PCL_INCLUDE_DIRS}
${CERES_INCLUDE_DIRS}
${OpenCV_INCLUDE_DIRS})
add_executable(ascanRegistration src/scanRegistration.cpp)
target_link_libraries(ascanRegistration ${catkin_LIBRARIES} ${PCL_LIBRARIES})
add_executable(alaserOdometry src/laserOdometry.cpp)
target_link_libraries(alaserOdometry ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${CERES_LIBRARIES})
add_executable(alaserMapping src/laserMapping.cpp)
target_link_libraries(alaserMapping ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${CERES_LIBRARIES})
add_executable(kittiHelper src/kittiHelper.cpp)
target_link_libraries(kittiHelper ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${OpenCV_LIBS})
包含这些第三方库的头文件,将第三方库文件跟源文件进行关联。
catkin_package(
CATKIN_DEPENDS geometry_msgs nav_msgs roscpp rospy std_msgs
DEPENDS EIGEN3 PCL
INCLUDE_DIRS include
)
DEPENDS 和 CATKIN_DEPENDS 用来告诉 catkin ,我们建立的程序包有哪些依赖项。
README文件:
介绍了A-LOAM如何部署,还有一些数据集如何获取,作者的信息和Docker的配置。
Package.xml文件:
这是A-LOAM包信息,包含作者邮箱、包构建和运行的依赖项。
Include文件夹:
Common.h:
inline double rad2deg(double radians)
{
return radians * 180.0 / M_PI;
}
inline double deg2rad(double degrees)
{
return degrees * M_PI / 180.0;
}
该头文件定义了两个函数,一个是弧度转角度,一个是角度转弧度。
Tic_toc.h :
class TicToc
{
public:
TicToc()
{
tic();
}
void tic()
{
start = std::chrono::system_clock::now();
}
double toc()
{
end = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end - start;
return elapsed_seconds.count() * 1000;
}
private:
std::chrono::time_point<std::chrono::system_clock> start, end;
};
该头文件定义了一个类,是作者自己写的用于计时的类。用了C++的chrono时间库,调用system_clock类里面的now方法获取当前系统时间,在一个代码块的开头调用tic()方法(构造函数只需要实例化对象就可调用),结尾调用toc()方法,传回的参数就是代码块的执行时间,单位为ms。
Launch文件夹:
<launch>
<param name="scan_line" type="int" value="16" />
<!-- if 1, do mapping 10 Hz, if 2, do mapping 5 Hz. Suggest to use 1, it will adjust frequence automaticlly -->
<param name="mapping_skip_frame" type="int" value="1" />
<!-- remove too closed points -->
<param name="minimum_range" type="double" value="0.3"/>
<param name="mapping_line_resolution" type="double" value="0.2"/>
<param name="mapping_plane_resolution" type="double" value="0.4"/>
<node pkg="aloam_velodyne" type="ascanRegistration" name="ascanRegistration" output="screen" />
<node pkg="aloam_velodyne" type="alaserOdometry" name="alaserOdometry" output="screen" />
<node pkg="aloam_velodyne" type="alaserMapping" name="alaserMapping" output="screen" />
<arg name="rviz" default="true" />
<group if="$(arg rviz)">
<node launch-prefix="nice" pkg="rviz" type="rviz" name="rviz" args="-d $(find aloam_velodyne)/rviz_cfg/aloam_velodyne.rviz" />
</group>
</launch>
以aloam_velodyne_VLP_16.launch为例,launch文件中通过node符号可以开启多个节点,且rosmaster也同时开启了;同时还定义了包名,这在运行launch文件中会用到。通过param符号可作为参数服务器,以便在程序中读取。同时也可设置ROS自带显示软件RVIZ是否开启和开启的RVIZ配置路径。
rviz_cfg文件夹:
在这个文件夹存放rviz的配置文件,可在rviz软件中修改参数后进行保存。
src文件夹:
kittiHelper.cpp:
这个源文件的作用是将kitti数据转换成ros下的话题数据,并可选择保存为bag文件。首先我们先看主函数的代码注释。
int main(int argc, char** argv)
{
ros::init(argc, argv, "kitti_helper"); //该节点名称,且初始化
ros::NodeHandle n("~");
std::string dataset_folder, sequence_number, output_bag_file;
n.getParam("dataset_folder", dataset_folder);
//从参数服务器获取数据集文件夹和对应号码,可在launch文件中根据数据存放地址来修改
n.getParam("sequence_number", sequence_number);
std::cout << "Reading sequence " << sequence_number << " from " << dataset_folder << '\n';
bool to_bag;
n.getParam("to_bag", to_bag); //读取是否转换成bag标志
if (to_bag)
n.getParam("output_bag_file", output_bag_file); //同上,获取bag输出文件夹
int publish_delay;
n.getParam("publish_delay", publish_delay); //同上,获取发布延迟时间
publish_delay = publish_delay <= 0 ? 1 : publish_delay; //三目运算符,延迟时间<=0 则设为1
//初始化发布雷达话题数据
ros::Publisher pub_laser_cloud = n.advertise<sensor_msgs::PointCloud2>("/velodyne_points", 2);
//用image_transport初始化发布左右相机数据,对ImageTransport类进行实例化
image_transport::ImageTransport it(n);
image_transport::Publisher pub_image_left = it.advertise("/image_left", 2);
image_transport::Publisher pub_image_right = it.advertise("/image_right", 2);
//初始化里程计发布,ground_truth包括旋转的四元数和位置坐标向量,这里没有用上
ros::Publisher pubOdomGT = n.advertise<nav_msgs::Odometry> ("/odometry_gt", 5);
nav_msgs::Odometry odomGT;
odomGT.header.frame_id = "/camera_init";
odomGT.child_frame_id = "/ground_truth";
ros::Publisher pubPathGT = n.advertise<nav_msgs::Path> ("/path_gt", 5);
nav_msgs::Path pathGT;
pathGT.header.frame_id = "/camera_init";
//获取时间戳地址
std::string timestamp_path = "sequences/" + sequence_number + "/times.txt";
std::ifstream timestamp_file(dataset_folder + timestamp_path, std::ifstream::in);
std::string ground_truth_path = "results/" + sequence_number + ".txt";
std::ifstream ground_truth_file(dataset_folder + ground_truth_path, std::ifstream::in);
rosbag::Bag bag_out;
if (to_bag)
bag_out.open(output_bag_file, rosbag::bagmode::Write); //新建并打开一个bag文件
Eigen::Matrix3d R_transform; //用在ground_truth数据中,这里没有用上
R_transform << 0, 0, 1, -1, 0, 0, 0, -1, 0;
Eigen::Quaterniond q_transform(R_transform);
std::string line;
std::size_t line_num = 0;
ros::Rate r(10.0 / publish_delay); //ros循环频率
// 遍历时间戳这个文本文件,得到时间戳信息
std::cout << "timestamp_file " << std::string(dataset_folder + timestamp_path) << std::endl;
while (std::getline(timestamp_file, line) && ros::ok())
{
// 把string转成浮点型float
float timestamp = stof(line);
std::stringstream left_image_path, right_image_path;
// 找到对应的左右目的图片位置,并用opencv接口读取
left_image_path << dataset_folder << "sequences/" + sequence_number + "/image_0/" << std::setfill('0') << std::setw(6) << line_num << ".png";
cv::Mat left_image = cv::imread(left_image_path.str(), CV_LOAD_IMAGE_GRAYSCALE);
right_image_path << dataset_folder << "sequences/" + sequence_number + "/image_1/" << std::setfill('0') << std::setw(6) << line_num << ".png";
cv::Mat right_image = cv::imread(left_image_path.str(), CV_LOAD_IMAGE_GRAYSCALE);
std::stringstream lidar_data_path;
// 获取lidar数据的文件名
lidar_data_path << dataset_folder << "velodyne/sequences/" + sequence_number + "/velodyne/"
<< std::setfill('0') << std::setw(6) << line_num << ".bin";
//这里调用读雷达数据的函数
std::vector<float> lidar_data = read_lidar_data(lidar_data_path.str());
std::cout << "totally " << lidar_data.size() / 4.0 << " points in this lidar frame \n";
std::vector<Eigen::Vector3d> lidar_points;
std::vector<float> lidar_intensities;
pcl::PointCloud<pcl::PointXYZI> laser_cloud;
// 每个点数据占四个float数据,分别是xyz,intensity,存到laser_cloud容器中
for (std::size_t i = 0; i < lidar_data.size(); i += 4)
{
lidar_points.emplace_back(lidar_data[i], lidar_data[i+1], lidar_data[i+2]);
lidar_intensities.push_back(lidar_data[i+3]);
// 构建pcl的点云格式
pcl::PointXYZI point;
point.x = lidar_data[i];
point.y = lidar_data[i + 1];
point.z = lidar_data[i + 2];
point.intensity = lidar_data[i + 3];
laser_cloud.push_back(point);
}
// 转成ros的消息格式
sensor_msgs::PointCloud2 laser_cloud_msg;
pcl::toROSMsg(laser_cloud, laser_cloud_msg);
laser_cloud_msg.header.stamp = ros::Time().fromSec(timestamp); //设定雷达时间戳
laser_cloud_msg.header.frame_id = "/camera_init"; //设定在相机坐标系下
// 发布点云数据
pub_laser_cloud.publish(laser_cloud_msg);
// 图片也转成ros的消息发布出去
sensor_msgs::ImagePtr image_left_msg = cv_bridge::CvImage(laser_cloud_msg.header, "mono8", left_image).toImageMsg();
sensor_msgs::ImagePtr image_right_msg = cv_bridge::CvImage(laser_cloud_msg.header, "mono8", right_image).toImageMsg();
pub_image_left.publish(image_left_msg);
pub_image_right.publish(image_right_msg);
// 也可以写到rosbag包中去
if (to_bag)
{
bag_out.write("/image_left", ros::Time::now(), image_left_msg);
bag_out.write("/image_right", ros::Time::now(), image_right_msg);
bag_out.write("/velodyne_points", ros::Time::now(), laser_cloud_msg);
bag_out.write("/path_gt", ros::Time::now(), pathGT); //下面两个实际没有数据
bag_out.write("/odometry_gt", ros::Time::now(), odomGT);
}
line_num ++;
r.sleep();
}
bag_out.close();
std::cout << "Done \n";
return 0;
}
我们再看一下read_lidar_data函数,功能是将雷达每一帧的数据读取到一个float类型的vector容器中。
std::vector<float> read_lidar_data(const std::string lidar_data_path)
{
std::ifstream lidar_data_file(lidar_data_path, std::ifstream::in | std::ifstream::binary);
lidar_data_file.seekg(0, std::ios::end); // 文件指针指向文件末尾
const size_t num_elements = lidar_data_file.tellg() / sizeof(float); // 统计一下文件有多少float数据
lidar_data_file.seekg(0, std::ios::beg); // 再把指针指向文件开始
std::vector<float> lidar_data_buffer(num_elements);
// 读取所有的数据
lidar_data_file.read(reinterpret_cast<char*>(&lidar_data_buffer[0]), num_elements*sizeof(float));
return lidar_data_buffer;
}
下一节讲解scanRegistration.cpp。