Quellcode durchsuchen

完善后端源码备注,开始写后端

Mj23366 vor 7 Monaten
Ursprung
Commit
07730f675d

+ 140 - 81
apps/hdl_graph_slam_nodelet.cpp

@@ -60,6 +60,20 @@
 #include <g2o/edge_se3_priorvec.hpp>
 #include <g2o/edge_se3_priorquat.hpp>
 
+/*
+后端的作用(GPT):
+1. 构建位资图:
+  后端根据前端生成的关键帧位姿和相对位姿变换,构建位姿图(Pose Graph),每个关键帧对应图中的一个节点,节点之间的相对运动构成边。
+
+2. 闭环检测
+  后端通过检测闭环(即机器人经过某个已经访问过的地方)来减少累积误差。闭环检测通过计算当前帧和之前帧之间的相似度来发现闭环位置,并将其加入位姿图中,形成闭环约束边。
+
+3. 图优化
+  利用优化算法(如g2o或Ceres)对整个位姿图进行优化。目标是调整位姿图中所有关键帧的位姿,
+  使之符合所有的约束(包括相邻关键帧的相对位姿约束以及闭环检测生成的约束),从而减少传感器噪声和漂移,生成全局一致的地图
+
+ */
+
 namespace hdl_graph_slam {
 
 class HdlGraphSlamNodelet : public nodelet::Nodelet {
@@ -110,12 +124,29 @@ public:
     imu_acceleration_edge_stddev = private_nh.param<double>("imu_acceleration_edge_stddev", 3.0);  // IMU加速度测量的标准差,用于量化IMU加速度测量的不确定性
 
     points_topic = private_nh.param<std::string>("points_topic", "/velodyne_points");  // 点云数据的ROS话题名称,SLAM系统将订阅这个话题来获取点云数据
+    // 这个话题的info是:
+    /* 
+    (slam_env) mj@mj-Lenovo-Legion-R9000P2021H:~$ rostopic info /velodyne_points
+    Type: sensor_msgs/PointCloud2
+
+    Publishers: 
+    * /player (http://mj-Lenovo-Legion-R9000P2021H:35735/)         
+    这个发布方是 rosbag 呀,是没处理过的点云信息,后端怎么会订阅没处理过的点云数据?   
+    也不是用来订阅的,这个文件是用这个话题来发布的,但是发布的内容又只有时间戳和frameid? 为什么这么奇怪?
+    结合 read_until_pub 的解释来看,似乎是用来指示操作应该持续到何时,所以这个应该不用管
+
+    Subscribers: 
+    * /velodyne_nodelet_manager (http://mj-Lenovo-Legion-R9000P2021H:39235/)
+    * /rviz_1729770290778318983 (http://mj-Lenovo-Legion-R9000P2021H:34629/)
+    */
+
+
 
     // subscribers
     // 话题通信的接受方
     odom_sub.reset(new message_filters::Subscriber<nav_msgs::Odometry>(mt_nh, published_odom_topic, 256));      // 创建一个新的message_filters::Subscriber对象,用于订阅nav_msgs::Odometry类型的消息
     cloud_sub.reset(new message_filters::Subscriber<sensor_msgs::PointCloud2>(mt_nh, "/filtered_points", 32));  // 创建一个新的message_filters::Subscriber对象,用于订阅sensor_msgs::PointCloud2类型的消息。
-    // ApproxSyncPolicy是一个同步策略,允许在一定时间窗口内的消息进行同步,这里设置的窗口大小为32
+    // ApproxSyncPolicy 是一个同步策略,允许在一定时间窗口内的消息进行同步,这里设置的窗口大小为32
     sync.reset(new message_filters::Synchronizer<ApproxSyncPolicy>(ApproxSyncPolicy(32), *odom_sub, *cloud_sub));  // 创建一个新的message_filters::Synchronizer对象,用于同步上面创建的两个订阅者(odom_sub和cloud_sub)的消息。
     sync->registerCallback(boost::bind(&HdlGraphSlamNodelet::cloud_callback, this, _1, _2));                       // 为同步器注册了一个回调函数,当同步条件满足时,会调用HdlGraphSlamNodelet类的cloud_callback成员函数
     // 调用 cloud_callback 函数的条件:
@@ -137,21 +168,24 @@ public:
     // 话题通信的发布方
     markers_pub = mt_nh.advertise<visualization_msgs::MarkerArray>("/hdl_graph_slam/markers", 16);      // 发布可视化数据(具体是什么还不知道)   这种类型的消息通常用于在RViz等可视化工具中显示标记(markers)
     odom2map_pub = mt_nh.advertise<geometry_msgs::TransformStamped>("/hdl_graph_slam/odom2pub", 16);    // 发布从里程计到地图的变换
-    map_points_pub = mt_nh.advertise<sensor_msgs::PointCloud2>("/hdl_graph_slam/map_points", 1, true);  //
+    map_points_pub = mt_nh.advertise<sensor_msgs::PointCloud2>("/hdl_graph_slam/map_points", 1, true);  // 这个话题会保留以前扫描过的点云!! 扫过的地方的点云就是
     read_until_pub = mt_nh.advertise<std_msgs::Header>("/hdl_graph_slam/read_until", 32);               // 这种类型的消息通常用于指示读取操作应该持续到何时
 
     // 服务通信:一个节点(客户端)发送一个服务请求给服务服务器,服务服务器处理这个请求并返回一个响应。这是一种同步的、请求/响应模式的通信方式。
-    load_service_server = mt_nh.advertiseService("/hdl_graph_slam/load", &HdlGraphSlamNodelet::load_service, this);  // 创建了一个服务服务器load_service_server,用于处理名为"/hdl_graph_slam/load"的服务请求,服务的处理函数是HdlGraphSlamNodelet类的load_service成员函数
-    dump_service_server = mt_nh.advertiseService("/hdl_graph_slam/dump", &HdlGraphSlamNodelet::dump_service, this);  // 创建了一个服务服务器dump_service_server,用于处理名为"/hdl_graph_slam/dump"的服务请求,服务的处理函数是HdlGraphSlamNodelet类的dump_service成员函数
-    save_map_service_server = mt_nh.advertiseService("/hdl_graph_slam/save_map", &HdlGraphSlamNodelet::save_map_service, this);
+    load_service_server = mt_nh.advertiseService("/hdl_graph_slam/load", &HdlGraphSlamNodelet::load_service, this);  // 加载系统数据或状态 创建了一个服务服务器load_service_server,用于处理名为"/hdl_graph_slam/load"的服务请求,服务的处理函数是HdlGraphSlamNodelet类的load_service成员函数
+    dump_service_server = mt_nh.advertiseService("/hdl_graph_slam/dump", &HdlGraphSlamNodelet::dump_service, this);  // 转储系统当前的状态或数据 创建了一个服务服务器dump_service_server,用于处理名为"/hdl_graph_slam/dump"的服务请求,服务的处理函数是HdlGraphSlamNodelet类的dump_service成员函数
+    save_map_service_server = mt_nh.advertiseService("/hdl_graph_slam/save_map", &HdlGraphSlamNodelet::save_map_service, this);  // 保存地图
+
+    graph_updated = false;                                                                           // 用于跟踪图优化是否已经更新
+    double graph_update_interval = private_nh.param<double>("graph_update_interval", 3.0);           // 表示图优化更新的时间间隔,默认值为3.0秒    interval:间隔
+    double map_cloud_update_interval = private_nh.param<double>("map_cloud_update_interval", 10.0);  // 地图点云更新的时间间隔,默认值为10.0秒
 
-    graph_updated = false;                                                                                                                          // 用于跟踪图优化是否已经更新
-    double graph_update_interval = private_nh.param<double>("graph_update_interval", 3.0);                                                          // 表示图优化更新的时间间隔,默认值为3.0秒    interval:间隔
-    double map_cloud_update_interval = private_nh.param<double>("map_cloud_update_interval", 10.0);                                                 // 地图点云更新的时间间隔,默认值为10.0秒
+    // 主要是这个回调函数!!!
     optimization_timer = mt_nh.createWallTimer(ros::WallDuration(graph_update_interval), &HdlGraphSlamNodelet::optimization_timer_callback, this);  // 创建一个定时器,定时器会在每个 graph_update_interval 秒后触发,然后调用 optimization_timer_callback 函数
+
     map_publish_timer = mt_nh.createWallTimer(ros::WallDuration(map_cloud_update_interval), &HdlGraphSlamNodelet::map_points_publish_timer_callback, this);  // 创建一个定时器,定时器会在每个 map_cloud_update_interval 秒后触发,然后调用 map_points_publish_timer_callback 函数
 
-    // 函数入口都是 callback 函数,也即三个:cloud_callback 、 optimization_timer_callback 、 map_points_publish_timer_callback
+    // 函数入口都是 callback 函数,也即三个: cloud_callback 、 optimization_timer_callback 、 map_points_publish_timer_callback
   }
 
 private:
@@ -161,23 +195,25 @@ private:
    * @param cloud_msg // 前端滤波后的点云数据
    */
   void cloud_callback(const nav_msgs::OdometryConstPtr& odom_msg, const sensor_msgs::PointCloud2::ConstPtr& cloud_msg) {
-    const ros::Time& stamp = cloud_msg->header.stamp;   // 获取点云数据的时间戳
-    Eigen::Isometry3d odom = odom2isometry(odom_msg);   // 将里程计消息转换为Eigen库中的Isometry3d类型,它表示一个3D刚体变换(包括旋转和平移)
+    // 接收并处理激光里程计数据和滤波后的点云数据,并判断是否生成关键帧,然后将新生成的关键帧推送到 keyframe_queue 队列中
+    // 处理点云信息主要是:格式转换、设置frame_id
+    const ros::Time& stamp = cloud_msg->header.stamp;  // 获取点云数据的时间戳
+    Eigen::Isometry3d odom = odom2isometry(odom_msg);  // 将里程计消息转换为Eigen库中的Isometry3d类型,它表示一个3D刚体变换(包括旋转和平移)
 
     pcl::PointCloud<PointT>::Ptr cloud(new pcl::PointCloud<PointT>());  // 创建一个点云对象的指针
-    pcl::fromROSMsg(*cloud_msg, *cloud);    // 将ROS的点云消息 cloud_msg 转换为PCL(Point Cloud Library)的点云格式
+    pcl::fromROSMsg(*cloud_msg, *cloud);                                // 将ROS的点云消息 cloud_msg 转换为PCL(Point Cloud Library)的点云格式
     if(base_frame_id.empty()) {
-      base_frame_id = cloud_msg->header.frame_id;   // 将当前点云消息的坐标系 frame_id 设置为基础坐标系      base_frame_id 是整个系统中关键帧数据的参考坐标系
-    } 
+      base_frame_id = cloud_msg->header.frame_id;  // 将当前点云消息的坐标系 frame_id 设置为基础坐标系      base_frame_id 是整个系统中关键帧数据的参考坐标系
+    }
 
     // 更新关键帧判断
-    if(!keyframe_updater->update(odom)) {   // 根据当前的里程计信息 odom 判断是否需要生成新的关键帧
-      std::lock_guard<std::mutex> lock(keyframe_queue_mutex); // 这行代码的作用是确保线程安全,防止多个线程同时访问或修改 keyframe_queue 队列
+    if(!keyframe_updater->update(odom)) {                      // 根据当前的里程计信息 odom 判断是否需要生成新的关键帧
+      std::lock_guard<std::mutex> lock(keyframe_queue_mutex);  // 这行代码的作用是确保线程安全,防止多个线程同时访问或修改 keyframe_queue 队列
       // std::lock_guard<std::mutex> 是 C++ 标准库中的一个类,用来简化互斥锁(std::mutex)的使用。它的作用是在其作用域内自动对给定的互斥量加锁,并在作用域结束时自动解锁
       // keyframe_queue_mutex 是一个互斥量(std::mutex),用于保护关键帧队列 keyframe_queue。当多个线程试图同时访问或修改 keyframe_queue 时,互斥量确保只有一个线程能够访问该资源,避免数据竞争和并发问题
       // 工作原理:当这行代码执行时,lock_guard 会锁住 keyframe_queue_mutex,使其他线程无法访问与之关联的资源(即 keyframe_queue) 当程序执行离开该作用域时,lock_guard 会自动释放锁,无需显式调用 unlock() 函数
       // 为什么需要锁?   在多线程环境中(比如 ROS 回调函数可能被不同线程调用),如果多个线程同时读取或写入 keyframe_queue,可能导致数据不一致或崩溃。使用互斥量确保对 keyframe_queue 的操作是原子性的,即一个线程在操作时,其他线程必须等待
-      
+
       if(keyframe_queue.empty()) {  // 如果关键帧队列是空的,发布一个 ROS 消息通知系统继续读取点云数据,直到指定时间点(stamp + ros::Duration(10, 0))。这段代码确保在没有关键帧生成时,系统能够继续读取点云数据
         std_msgs::Header read_until;
         read_until.stamp = stamp + ros::Duration(10, 0);
@@ -187,28 +223,27 @@ private:
         read_until_pub.publish(read_until);
       }
 
-      return; 
+      return;
     }
 
     double accum_d = keyframe_updater->get_accum_distance();  // 获取累计的运动距离,用于判断关键帧生成的条件
     KeyFrame::Ptr keyframe(new KeyFrame(stamp, odom, accum_d, cloud));  // 创建一个新的关键帧 keyframe,其中包含当前的时间戳 stamp,里程计信息 odom,累计距离 accum_d,以及处理后的点云数据 cloud(这里的处理就是做了个消息类型的转换)
 
-    std::lock_guard<std::mutex> lock(keyframe_queue_mutex);   // 使用 std::lock_guard 加锁,确保对关键帧队列 keyframe_queue 的操作是线程安全的
-    keyframe_queue.push_back(keyframe);   // 将新生成的关键帧 keyframe 推入 keyframe_queue 队列,供后续的处理使用
+    std::lock_guard<std::mutex> lock(keyframe_queue_mutex);  // 使用 std::lock_guard 加锁,确保对关键帧队列 keyframe_queue 的操作是线程安全的
+    keyframe_queue.push_back(keyframe);                      // 将新生成的关键帧 keyframe 推入 keyframe_queue 队列,供后续的处理使用
   }
 
-
-
-
   /**
-   * @brief this method adds all the keyframes in #keyframe_queue to the pose graph (odometry edges)
-   * @return if true, at least one keyframe was added to the pose graph
+   * @brief this method adds all the keyframes in #keyframe_queue to the pose graph (odometry edges)    此方法将#keyframe_queue中的所有关键帧添加到姿势图中(里程计边)
+   * @return if true, at least one keyframe was added to the pose graph   如果为真,则至少有一个关键帧被添加到姿势图中
    * 将队列中的关键帧添加到位姿图中,并返回是否至少有一个关键帧被添加
+   *
+   * pose graph  是什么?
    */
   bool flush_keyframe_queue() {
-    std::lock_guard<std::mutex> lock(keyframe_queue_mutex);
+    std::lock_guard<std::mutex> lock(keyframe_queue_mutex);  // 多线程锁
 
-    if(keyframe_queue.empty()) {
+    if(keyframe_queue.empty()) {  // 没有关键帧就返回
       return false;
     }
 
@@ -473,7 +508,7 @@ private:
     return updated;
   }
 
-  /**
+  /**   不做地板检测,这个函数不用看
    * @brief received floor coefficients are added to #floor_coeffs_queue
    * @param floor_coeffs_msg
    */
@@ -486,9 +521,11 @@ private:
     floor_coeffs_queue.push_back(floor_coeffs_msg);
   }
 
-  /**
+  /**   不做地板检测,这个函数不用看
    * @brief this methods associates floor coefficients messages with registered keyframes, and then adds the associated coeffs to the pose graph
+   * @brief 该方法将地板系数消息与注册的关键帧相关联,然后将关联的系数添加到姿势图中。
    * @return if true, at least one floor plane edge is added to the pose graph
+   * @return 如果为真,则至少有一个地板平面边被添加到姿势图中
    */
   bool flush_floor_queue() {
     std::lock_guard<std::mutex> lock(floor_coeffs_queue_mutex);
@@ -564,52 +601,71 @@ private:
 
   /**
    * @brief this methods adds all the data in the queues to the pose graph, and then optimizes the pose graph
-   * @param event
+   * @brief 此方法将队列中的所有数据添加到姿势图中,然后优化姿势图
+   * @param event   // 是ROS提供的定时器时间类,包含信息有:计时器出发的时间戳、上一次出发的时间戳、计时器的周期信息等
    */
   void optimization_timer_callback(const ros::WallTimerEvent& event) {
-    std::lock_guard<std::mutex> lock(main_thread_mutex);
+    std::lock_guard<std::mutex> lock(main_thread_mutex);  // 创建了一个锁,用于保护对主线程的访问,确保线程安全
 
-    // add keyframes and floor coeffs in the queues to the pose graph
-    bool keyframe_updated = flush_keyframe_queue();
+    // add keyframes and floor coeffs in the queues to the pose graph  将队列中的关键帧和楼层系数添加到姿势图中
+    bool keyframe_updated = flush_keyframe_queue();  // 调用 flush_keyframe_queue 函数,将关键帧队列中的数据添加到位姿图中,并返回是否有关键帧被更新
 
-    if(!keyframe_updated) {
+    if(!keyframe_updated) {  // 如果没有关键帧被更新,则执行大括号内的代码
       std_msgs::Header read_until;
-      read_until.stamp = ros::Time::now() + ros::Duration(30, 0);
+      read_until.stamp = ros::Time::now() + ros::Duration(30, 0);  // 时间戳为什么要加30秒?
       read_until.frame_id = points_topic;
       read_until_pub.publish(read_until);
       read_until.frame_id = "/filtered_points";
-      read_until_pub.publish(read_until);
+      read_until_pub.publish(read_until);  // 不同的 frame_id 和 话题 发布两次, 是干嘛用的?
     }
 
     if(!keyframe_updated & !flush_floor_queue() & !flush_gps_queue() & !flush_imu_queue()) {
+      // 检查地板、GPS、IMU等队列,分别调用相应的 flush 函数(例如 flush_floor_queue、flush_gps_queue、flush_imu_queue),如果这些数据没有更新,则函数直接返回,不进行后续操作。
       return;
     }
 
-    // loop detection
+    // loop detection   闭环检测
     std::vector<Loop::Ptr> loops = loop_detector->detect(keyframes, new_keyframes, *graph_slam);
-    for(const auto& loop : loops) {
+    // 使用 loop_detector->detect 检测关键帧之间的闭环。闭环检测用于检测机器人是否回到了之前的某个位置,这在位姿图优化中起到约束作用。  (这部分都是GPT解释)
+    // keyframes: 包含所有历史关键帧的容器
+    // new_keyframes: 当前生成的最新的关键帧
+    // *graph_slam: 一个图优化的实例,用来存储闭环检测的结果,可能包括相对位资信息,图结构,以及优化过程中所需要的数据
+    // 相对位资信息: new_keyframes 与 keyframes 中所有帧的相对位资
+    // 图结构: 一种数学表达,用来描述机器人在环境中的位资及其与环境中特征(如地标和其他关键帧)的关系. 具体而言,包括下面几个基本要素:
+    //    节点: 每个节点代表一个特定的位资或状态  例如slam中,一个节点可以表示机器人在某个点的位置信息,也可以表示环境中的特征点
+    //    边: 表示节点之间的约束关系, 这些约束可以是基于传感器测量的数据(例如激光里程计)或者闭环检测结果.    边通常回代有权重,表示约束的可信度和不确定性
+    //    优化目标: 通过最小化节点之间的约束误差,图结构可以通过图优化算法进行优化.  常见的方法包括最小二乘法, 梯度下降法等等.   优化的目标是使得地图和机器人轨迹尽可能一致, 提高整体的定位和建图精度
+    //    闭环检测: 在闭环检测过程中, 图结构会被更新,以反映新的闭环约束.这意味着新的边被添加到图中, 链接当前节点和历史节点,改善整体地图的一致性
+    //    数据关联: 通过匹配特征和关键帧, 确定节点之间的关联性,从而形成合理的地图和路径
+
+    // 检测不到闭环的情况: 没有足够的关键帧、环境发生显著变化、特征匹配失败(发生错误或特征点数量不足)、定位精度不足、参数设置不当、移动机器人的运动太快、内存或计算资源不够
+
+    // 如果检测到闭环,则为闭环关系添加一个新的边到位姿图中,计算相对位姿并生成信息矩阵,确保闭环边的优化权重。
+    for(const auto& loop : loops) {  // 遍历闭环检测结果
       Eigen::Isometry3d relpose(loop->relative_pose.cast<double>());
       Eigen::MatrixXd information_matrix = inf_calclator->calc_information_matrix(loop->key1->cloud, loop->key2->cloud, relpose);
       auto edge = graph_slam->add_se3_edge(loop->key1->node, loop->key2->node, relpose, information_matrix);
       graph_slam->add_robust_kernel(edge, private_nh.param<std::string>("loop_closure_edge_robust_kernel", "NONE"), private_nh.param<double>("loop_closure_edge_robust_kernel_size", 1.0));
     }
 
-    std::copy(new_keyframes.begin(), new_keyframes.end(), std::back_inserter(keyframes));
-    new_keyframes.clear();
+    std::copy(new_keyframes.begin(), new_keyframes.end(), std::back_inserter(keyframes));  // 将新关键帧 new_keyframes 合并到已有的关键帧列表 keyframes 中
+    new_keyframes.clear();                                                                 // 清空 new_keyframes
+
+    // move the first node anchor position to the current estimate of the first node pose   将第一节点锚点位置移动到第一节点姿态的当前估计值
+    // so the first node moves freely while trying to stay around the origin                因此,第一个节点在试图停留在原点附近的同时可以自由移动
 
-    // move the first node anchor position to the current estimate of the first node pose
-    // so the first node moves freely while trying to stay around the origin
-    if(anchor_node && private_nh.param<bool>("fix_first_node_adaptive", true)) {
+    if(anchor_node && private_nh.param<bool>("fix_first_node_adaptive", true)) {  // launch文件中,fix_first_node_adaptive 设置为 true
       Eigen::Isometry3d anchor_target = static_cast<g2o::VertexSE3*>(anchor_edge->vertices()[1])->estimate();
       anchor_node->setEstimate(anchor_target);
+      // 如果启用了自适应固定第一帧的功能(参数 "fix_first_node_adaptive"),则将第一个关键帧的锚点位置更新为当前估计的位置,以允许它自由移动但仍然保持在原点附近。
     }
 
     // optimize the pose graph
-    int num_iterations = private_nh.param<int>("g2o_solver_num_iterations", 1024);
-    graph_slam->optimize(num_iterations);
+    int num_iterations = private_nh.param<int>("g2o_solver_num_iterations", 1024);  // launch文件中都是设置成512
+    graph_slam->optimize(num_iterations);                                           // 使用 g2o 优化器对位姿图进行优化,优化的迭代次数由参数 "g2o_solver_num_iterations" 控制
 
-    // publish tf
-    const auto& keyframe = keyframes.back();
+    // publish tf     发布位姿变换
+    const auto& keyframe = keyframes.back();  // 获取最新的关键帧估计
     Eigen::Isometry3d trans = keyframe->node->estimate() * keyframe->odom.inverse();
     trans_odom2map_mutex.lock();
     trans_odom2map = trans.matrix().cast<float>();
@@ -625,7 +681,7 @@ private:
 
     if(odom2map_pub.getNumSubscribers()) {
       geometry_msgs::TransformStamped ts = matrix2transform(keyframe->stamp, trans.matrix().cast<float>(), map_frame_id, odom_frame_id);
-      odom2map_pub.publish(ts);
+      odom2map_pub.publish(ts);  // 发布odom2map_pub话题中
     }
 
     if(markers_pub.getNumSubscribers()) {
@@ -1047,7 +1103,6 @@ private:  // 一些参数的定义
   ros::Subscriber gps_sub;
   ros::Subscriber nmea_sub;
   ros::Subscriber navsat_sub;
-
   ros::Subscriber imu_sub;
   ros::Subscriber floor_sub;
 
@@ -1057,24 +1112,24 @@ private:  // 一些参数的定义
   std::string map_frame_id;
   std::string odom_frame_id;
 
-  std::mutex trans_odom2map_mutex;
-  Eigen::Matrix4f trans_odom2map;
-  ros::Publisher odom2map_pub;
+  std::mutex trans_odom2map_mutex;  // 一个互斥量,用来确保在多线程环境中对 trans_odom2map 变量的访问是线程安全的。
+  Eigen::Matrix4f trans_odom2map;   // 表示从 里程计坐标系(odom frame)到地图坐标系(map frame)的变换矩阵       这个变换矩阵是跟机械臂变换矩阵类似的 齐次变换矩阵
+  ros::Publisher odom2map_pub;      // 发布里程计到地图的变换信息
 
-  std::string points_topic;
+  std::string points_topic;  // 点云数据的ROS话题名称,SLAM系统将订阅这个话题来获取点云数据
   ros::Publisher read_until_pub;
   ros::Publisher map_points_pub;
 
-  tf::TransformListener tf_listener;
+  tf::TransformListener tf_listener;  // ROS的一个类,用来监听和查询不同坐标系之间的变换关系
 
-  ros::ServiceServer load_service_server;
-  ros::ServiceServer dump_service_server;
-  ros::ServiceServer save_map_service_server;
+  ros::ServiceServer load_service_server;      // 一个 ROS 服务服务器,用于加载系统数据或状态
+  ros::ServiceServer dump_service_server;      // 一个 ROS 服务服务器,用于转储(dump)系统当前的状态或数据
+  ros::ServiceServer save_map_service_server;  // 一个 ROS 服务服务器,用于保存地图
 
   // keyframe queue
   std::string base_frame_id;
-  std::mutex keyframe_queue_mutex;
-  std::deque<KeyFrame::Ptr> keyframe_queue;
+  std::mutex keyframe_queue_mutex;           // std::mutex 是一种独占式互斥量
+  std::deque<KeyFrame::Ptr> keyframe_queue;  // 关键帧队列
 
   // gps queue
   double gps_time_offset;
@@ -1085,13 +1140,13 @@ private:  // 一些参数的定义
   std::deque<geographic_msgs::GeoPointStampedConstPtr> gps_queue;
 
   // imu queue
-  double imu_time_offset;
-  bool enable_imu_orientation;
-  double imu_orientation_edge_stddev;
-  bool enable_imu_acceleration;
-  double imu_acceleration_edge_stddev;
-  std::mutex imu_queue_mutex;
-  std::deque<sensor_msgs::ImuConstPtr> imu_queue;
+  double imu_time_offset;                          // 用于对IMU数据时间戳进行校正或偏移
+  bool enable_imu_orientation;                     // 控制是否启用IMU提供姿态信息
+  double imu_orientation_edge_stddev;              // 用于指定IMU姿态数据的不确定性(标准差)
+  bool enable_imu_acceleration;                    // 控制是否启用IMU提供的加速信息
+  double imu_acceleration_edge_stddev;             // 指定IMU加速度信息的不确定性(标准差)
+  std::mutex imu_queue_mutex;                      // 互斥量
+  std::deque<sensor_msgs::ImuConstPtr> imu_queue;  // IMU队列
 
   // floor_coeffs queue
   double floor_edge_stddev;
@@ -1099,24 +1154,28 @@ private:  // 一些参数的定义
   std::deque<hdl_graph_slam::FloorCoeffsConstPtr> floor_coeffs_queue;
 
   // for map cloud generation
-  std::atomic_bool graph_updated;
-  double map_cloud_resolution;
-  std::mutex keyframes_snapshot_mutex;
-  std::vector<KeyFrameSnapshot::Ptr> keyframes_snapshot;
-  std::unique_ptr<MapCloudGenerator> map_cloud_generator;
+  std::atomic_bool graph_updated;                          // 用于标识 位资图(pose graph)是否更新,确保多线程环境下的线程安全
+  double map_cloud_resolution;                             // 控制生成的地图点云的分辨率
+  std::mutex keyframes_snapshot_mutex;                     // 互斥量
+  std::vector<KeyFrameSnapshot::Ptr> keyframes_snapshot;   // 用来存储关键帧的快照。这个变量很重要,发布 map_point 和 保存 pcd 地图都是用这个变量!!
+  std::unique_ptr<MapCloudGenerator> map_cloud_generator;  // 生成点云地图的对象指针
 
   // graph slam
   // all the below members must be accessed after locking main_thread_mutex
-  std::mutex main_thread_mutex;
-
-  int max_keyframes_per_update;
-  std::deque<KeyFrame::Ptr> new_keyframes;
-
-  g2o::VertexSE3* anchor_node;
-  g2o::EdgeSE3* anchor_edge;
-  g2o::VertexPlane* floor_plane_node;
-  std::vector<KeyFrame::Ptr> keyframes;
-  std::unordered_map<ros::Time, KeyFrame::Ptr, RosTimeHash> keyframe_hash;
+  std::mutex main_thread_mutex;  // 互斥量
+
+  int max_keyframes_per_update;             // 限制每次更新中可以处理的最大关键帧数量
+  std::deque<KeyFrame::Ptr> new_keyframes;  // 存储新生成的关键帧指针
+
+  g2o::VertexSE3* anchor_node;           // 一个指向 g2o::VertexSE3 类型的指针,表示位姿图中的一个特定节点(锚点节点)。锚点节点通常是用来约束图的某个部分,使得图优化时可以保持稳定。
+                                         // 锚点的引入可以帮助优化算法更好地收敛到一个合理的解,特别是在 SLAM 系统中,锚点可以用于固定某个已知的位置(如起始位置或特征点)。
+  g2o::EdgeSE3* anchor_edge;             // 一个指向 g2o::EdgeSE3 类型的指针,表示连接锚点节点和其他节点的边。这条边用于描述锚点与其他位姿节点之间的相对约束关系。
+                                         // 在图优化中,这种约束有助于保持图的稳定性,并确保锚点的影响在优化过程中得以体现。
+  g2o::VertexPlane* floor_plane_node;    // 用于表示地面平面节点的指针。
+  std::vector<KeyFrame::Ptr> keyframes;  // 存储当前系统中的所有关键帧。
+  std::unordered_map<ros::Time, KeyFrame::Ptr, RosTimeHash> keyframe_hash;  // 用于快速查找关键帧的哈希表
+                                                                            // keyframe_hash 是一个无序映射,使用 ros::Time 作为键,以关键帧指针为值。此哈希表用于存储每个关键帧的时间戳与其对应的指针,便于快速查找特定时间的关键帧。
+                                                                            // 这种数据结构可以加速对关键帧的查找过程,提高 SLAM 系统在处理数据时的效率,尤其是在需要频繁检索关键帧时。
 
   // std::unique_ptr:一种独占所有权的智能指针,意味着同一时间内只能有一个std::unique_ptr指向某个对象
   // 智能指针是一种自动管理动态分配内存的类,它们在对象不再使用时自动释放内存,从而帮助防止内存泄漏

+ 1 - 1
apps/scan_matching_odometry_nodelet.cpp

@@ -57,7 +57,7 @@ public:
     odom_pub = nh.advertise<nav_msgs::Odometry>(published_odom_topic, 32);                                   // 发布机器人的里程计信息
     trans_pub = nh.advertise<geometry_msgs::TransformStamped>("/scan_matching_odometry/transform", 32);      //
     status_pub = private_nh.advertise<ScanMatchingStatus>("/scan_matching_odometry/status", 8);              //
-    aligned_points_pub = nh.advertise<sensor_msgs::PointCloud2>("/aligned_points", 32);                      //
+    aligned_points_pub = nh.advertise<sensor_msgs::PointCloud2>("/aligned_points", 32);                      // 对齐后的点云
   }
 
 

+ 43 - 0
include/hdl_backend.hpp

@@ -0,0 +1,43 @@
+#include <ros/ros.h>
+#include <string>
+#include <Eigen/Dense>
+#include <memory>
+#include <message_filters/subscriber.h>
+#include <nav_msgs/Odometry.h>
+#include <sensor_msgs/PointCloud2.h>
+#include <message_filters/sync_policies/approximate_time.h>
+#include <message_filters/synchronizer.h>
+
+typedef message_filters::sync_policies::ApproximateTime<nav_msgs::Odometry, sensor_msgs::PointCloud2> ApproxSyncPolicy;
+typedef pcl::PointXYZI PointT;
+
+class hdl_backend {
+private:
+  
+
+  ros::NodeHandle nh;             // ros 的节点句柄
+  ros::Publisher map_points_pub;  // 话题通常包含了所有之前扫描过的点云,随着机器人在环境中移动,新的点云数据会被不断添加到这个地图中,从而提供一个逐步完善的环境表示
+
+  // std::string published_odom_topic;
+  std::string map_frame_id;
+  // std::string odom_frame_id;
+  
+  std::string map_cloud_resolution;  // 地图分辨率
+  Eigen::Matrix4f trans_odom2map;    // 表示从 里程计坐标系(odom frame)到地图坐标系(map frame)的变换矩阵       这个变换矩阵是跟机械臂变换矩阵类似的 齐次变换矩阵
+
+  int max_keyframes_per_update;  // 限制每次更新中可以处理的最大关键帧数量
+
+  std::string points_topic;  // 点云数据的ROS话题名称,SLAM系统将订阅这个话题来获取点云数据
+
+  std::unique_ptr<message_filters::Subscriber<nav_msgs::Odometry>> odom_sub;
+  std::unique_ptr<message_filters::Subscriber<sensor_msgs::PointCloud2>> cloud_sub;
+  std::unique_ptr<message_filters::Synchronizer<ApproxSyncPolicy>> sync;
+
+  void cloud_callback(const nav_msgs::OdometryConstPtr& odom_msg, const sensor_msgs::PointCloud2::ConstPtr& cloud_msg){};
+
+public:
+  hdl_backend(){};   // 构造函数
+  ~hdl_backend(){};  // 析构函数
+  void init();       // 初始化函数
+  std::string base_frame_id;
+};

+ 89 - 0
src/hdl_backend.cpp

@@ -0,0 +1,89 @@
+#include <iostream>
+#include <ros/ros.h>
+#include "hdl_backend.hpp"
+// #include <pcl_ros/point_cloud.h>
+#include <pcl_ros/point_cloud.h>
+#include <hdl_graph_slam/ros_utils.hpp>
+#include <functional>  // std::bind 和 std::placeholders
+
+
+
+int main(int argc, char* argv[]) {
+  // 执行 ros 节点初始化
+  ros::init(argc, argv, "hdl_backend");
+
+  return 0;
+}
+
+void hdl_backend::init() {
+  map_frame_id = "map";
+  map_cloud_resolution = "0.05";  // 地图点云分辨率
+  trans_odom2map.setIdentity();   // 设置为单位矩阵
+  max_keyframes_per_update = 10;
+
+  // odom_sub.reset(new message_filters::Subscriber<nav_msgs::Odometry>(nh, "/laser_odom", 256));                   // 创建 message_filters::Subscriber 对象,用于订阅nav_msgs::Odometry类型的消息
+  // cloud_sub.reset(new message_filters::Subscriber<sensor_msgs::PointCloud2>(nh, "/filtered_points", 32));        // 创建 message_filters::Subscriber 对象,用于订阅sensor_msgs::PointCloud2类型的消息。
+  // sync.reset(new message_filters::Synchronizer<ApproxSyncPolicy>(ApproxSyncPolicy(32), *odom_sub, *cloud_sub));  // 创建 message_filters::Synchronizer 对象,用于同步上面创建的两个订阅者(odom_sub和cloud_sub)的消息。
+  // // sync->registerCallback(boost::bind(&hdl_backend::cloud_callback, this, _1, _2));                               // 为同步器注册了一个回调函数,当同步条件满足时,会调用HdlGraphSlamNodelet类的cloud_callback成员函数
+  // sync->registerCallback(std::bind(&hdl_backend::cloud_callback, this, std::placeholders::_1, std::placeholders::_2));
+
+  // 订阅者的创建
+  odom_sub.reset(new message_filters::Subscriber<nav_msgs::Odometry>(nh, "/laser_odom", 256)); // 创建 message_filters::Subscriber 对象,用于订阅nav_msgs::Odometry类型的消息
+  cloud_sub.reset(new message_filters::Subscriber<sensor_msgs::PointCloud2>(nh, "/filtered_points", 32)); // 创建 message_filters::Subscriber 对象,用于订阅sensor_msgs::PointCloud2类型的消息
+
+  // 同步器的创建
+  sync.reset(new message_filters::Synchronizer<ApproxSyncPolicy>(ApproxSyncPolicy(32), *odom_sub, *cloud_sub)); // 创建 message_filters::Synchronizer 对象,用于同步上面创建的两个订阅者(odom_sub和cloud_sub)的消息
+
+  // 注册回调函数
+  sync->registerCallback(boost::bind(&hdl_backend::cloud_callback, this, std::placeholders::_1, std::placeholders::_2)); // 使用 std::bind 注册回调函数
+
+
+
+}
+
+/**
+ * @brief 接收点云数据并放到 keyframe_queue 中
+ * @param odom_msg  前端的激光里程计数据
+ * @param cloud_msg 前端滤波后的点云数据
+ */
+void cloud_callback(const nav_msgs::OdometryConstPtr& odom_msg, const sensor_msgs::PointCloud2::ConstPtr& cloud_msg) {
+    const ros::Time& stamp = cloud_msg->header.stamp;  // 获取点云数据的时间戳
+    Eigen::Isometry3d odom = hdl_graph_slam::odom2isometry(odom_msg);  // 将里程计消息转换为Eigen库中的Isometry3d类型,它表示一个3D刚体变换(包括旋转和平移)
+
+    pcl::PointCloud<PointT>::Ptr cloud(new pcl::PointCloud<PointT>());  // 创建一个点云对象的指针
+    pcl::fromROSMsg(*cloud_msg, *cloud);                                // 将ROS的点云消息 cloud_msg 转换为PCL(Point Cloud Library)的点云格式
+    if(base_frame_id.empty()) { // 用类的单例实现应该就ok?  
+      // 明天再试一下
+
+
+      base_frame_id = cloud_msg->header.frame_id;  // 将当前点云消息的坐标系 frame_id 设置为基础坐标系      base_frame_id 是整个系统中关键帧数据的参考坐标系
+    }
+
+    // 更新关键帧判断
+    if(!keyframe_updater->update(odom)) {                      // 根据当前的里程计信息 odom 判断是否需要生成新的关键帧
+      std::lock_guard<std::mutex> lock(keyframe_queue_mutex);  // 这行代码的作用是确保线程安全,防止多个线程同时访问或修改 keyframe_queue 队列
+      // std::lock_guard<std::mutex> 是 C++ 标准库中的一个类,用来简化互斥锁(std::mutex)的使用。它的作用是在其作用域内自动对给定的互斥量加锁,并在作用域结束时自动解锁
+      // keyframe_queue_mutex 是一个互斥量(std::mutex),用于保护关键帧队列 keyframe_queue。当多个线程试图同时访问或修改 keyframe_queue 时,互斥量确保只有一个线程能够访问该资源,避免数据竞争和并发问题
+      // 工作原理:当这行代码执行时,lock_guard 会锁住 keyframe_queue_mutex,使其他线程无法访问与之关联的资源(即 keyframe_queue) 当程序执行离开该作用域时,lock_guard 会自动释放锁,无需显式调用 unlock() 函数
+      // 为什么需要锁?   在多线程环境中(比如 ROS 回调函数可能被不同线程调用),如果多个线程同时读取或写入 keyframe_queue,可能导致数据不一致或崩溃。使用互斥量确保对 keyframe_queue 的操作是原子性的,即一个线程在操作时,其他线程必须等待
+      
+      if(keyframe_queue.empty()) {  // 如果关键帧队列是空的,发布一个 ROS 消息通知系统继续读取点云数据,直到指定时间点(stamp + ros::Duration(10, 0))。这段代码确保在没有关键帧生成时,系统能够继续读取点云数据
+        std_msgs::Header read_until;
+        read_until.stamp = stamp + ros::Duration(10, 0);
+        read_until.frame_id = points_topic;
+        read_until_pub.publish(read_until);
+        read_until.frame_id = "/filtered_points";
+        read_until_pub.publish(read_until);
+      }
+
+      return;
+    }
+
+    double accum_d = keyframe_updater->get_accum_distance();  // 获取累计的运动距离,用于判断关键帧生成的条件
+    KeyFrame::Ptr keyframe(new KeyFrame(stamp, odom, accum_d, cloud));  // 创建一个新的关键帧 keyframe,其中包含当前的时间戳 stamp,里程计信息 odom,累计距离 accum_d,以及处理后的点云数据 cloud(这里的处理就是做了个消息类型的转换)
+
+    std::lock_guard<std::mutex> lock(keyframe_queue_mutex);  // 使用 std::lock_guard 加锁,确保对关键帧队列 keyframe_queue 的操作是线程安全的
+    keyframe_queue.push_back(keyframe);                      // 将新生成的关键帧 keyframe 推入 keyframe_queue 队列,供后续的处理使用
+
+
+}

+ 7 - 3
src/laser_odom.cpp

@@ -35,7 +35,6 @@
 int main(int argc, char* argv[]) {
   // 执行 ros 节点初始化
   ros::init(argc, argv, "laser_odom");
-  // 创建 ros 节点句柄
 
   Laser_Odom laser_odom;
   laser_odom.init();
@@ -66,14 +65,19 @@ void Laser_Odom::init() {
   odom_pub = nh.advertise<nav_msgs::Odometry>("/laser_odom", 32);  // 发布机器人的里程计信息
   // keyframe_pub = nh.advertise<pcl::PointCloud<PointT>>("/keyframe", 32);
   // read_until_pub = nh.advertise<std_msgs::Header>("/keyframe", 32);
-  aligned_points_pub = nh.advertise<sensor_msgs::PointCloud2>("/aligned_points", 32);                      //
+  aligned_points_pub = nh.advertise<sensor_msgs::PointCloud2>("/aligned_points", 32);                      // 对齐后的点云
   map_points_pub = nh.advertise<sensor_msgs::PointCloud2>("/map_points", 1, true);
 
   ros::Subscriber sub = nh.subscribe<sensor_msgs::LaserScan>("/scan", 10, Laser_Odom::cloud_callback);
   ros::spin();
 }
 
-Laser_Odom* Laser_Odom::laser_odom = nullptr;  // 在 .cpp 文件中定义静态成员变量
+Laser_Odom* Laser_Odom::laser_odom = nullptr;  // 在 .cpp 文件中定义静态成员变量  
+// 这种实现方法不算单例实现,如果有多个laser_odom的实例,this指针都会被覆盖,指向最后创建的那个实例
+// 这里就不改了吧,后端不能这么写,因为后端要多线程
+// 多线程实现中有一个跟这个很类似的,是 懒汉式(线程不安全) 实现, 它是在给 laser_odom 赋值前先判断是不是等于 nullptr , 如果是才赋值,否则直接返回laser_odom
+// 但多线程很有可能同时满足 laser_odom == nullptr, 然后创建多个实例
+// 但其实我的cpp文件只跑一次main而已, 多线程问题在这里不会出现呀, 
 void Laser_Odom::cloud_callback(const sensor_msgs::LaserScan::ConstPtr& laser_scan_msg) {
   if(!ros::ok()) {
     return;