├── LICENSE ├── README.md ├── doc ├── challenges.png ├── ltmapper.pdf ├── outputs.png ├── parkinglot_ltslam.png └── pipeline.png ├── docker ├── Dockerfile ├── build.sh └── run.sh ├── ltmapper-final.pdf ├── ltremovert ├── CMakeLists.txt ├── config │ └── params_ltmapper.yaml ├── include │ └── removert │ │ ├── Removerter.h │ │ ├── RosParamServer.h │ │ ├── Session.h │ │ └── utility.h ├── launch │ ├── removert_visualization.rviz │ └── run_ltmapper.launch ├── package.xml └── src │ ├── Removerter.cpp │ ├── RosParamServer.cpp │ ├── Session.cpp │ ├── removert_main.cpp │ └── utility.cpp └── ltslam ├── CMakeLists.txt ├── README.md ├── config └── params.yaml ├── include └── ltslam │ ├── BetweenFactorWithAnchoring.h │ ├── KDTreeVectorOfVectorsAdaptor.h │ ├── LTslam.h │ ├── RosParamServer.h │ ├── Scancontext.h │ ├── Session.h │ ├── nanoflann.hpp │ ├── tictoc.h │ └── utility.h ├── launch ├── ltmapper_visualization.rviz └── run.launch ├── package.xml └── src ├── LTslam.cpp ├── RosParamServer.cpp ├── Scancontext.cpp ├── Session.cpp ├── main.cpp └── utility.cpp /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Giseop Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LT-mapper: A Modular Framework for LiDAR-based Lifelong Mapping 2 | 3 | 4 | 5 | 6 | **LT-mapper** is an open-source, modular framework designed for **LiDAR-based lifelong mapping** in dynamic environments. It enables robots to maintain accurate 3D maps over long time scales, addressing challenges such as: 7 | 8 | - Multi-session trajectory alignment (LT-SLAM) 9 | - High/low dynamic object filtering and change detection (LT-removert) 10 | - Efficient map management and change composition (LT-map) 11 |

12 | The framework supports real-world scenarios, handling ephemeral and persistent changes across multiple sessions without requiring perfect initial alignment. 13 | 14 | For more deatils, please refer to our [paper](./doc/ltmapper.pdf) and [video](https://youtu.be/vlYKfp1p2j8). 15 | 16 | 17 | 18 | 19 | 20 | ## Features 21 | - **Multi-Session SLAM (LT-SLAM):** Align multiple sessions in a shared coordinate system using anchor-node-based pose graph optimization. 22 | - **Dynamic Object Removal (LT-removert):** Efficiently filter high dynamic points and detect low-dynamic changes between sessions. 23 | - **Map Management (LT-map):** Maintain live maps, generate meta-maps, and efficiently compose changes using delta maps. 24 | - **Seamless Modular Integration:** Each module can run seperately via file-based in/out protocol. 25 |

26 | 27 | 28 | ## Getting Started 29 | ### Prerequisites 30 | - ROS (tested with Melodic and Noetic) 31 | ```shell 32 | sudo apt install -y ros-distro-navigation 33 | sudo apt install -y ros-distro-robot-localization 34 | sudo apt install -y ros-distro-robot-state-publisher 35 | ``` 36 | - GTSAM 37 | ```shell 38 | sudo add-apt-repository ppa:borglab/gtsam-release-4.0 39 | sudo apt install -y libgtsam-dev libgtsam-unstable-dev 40 | ``` 41 | 42 | ### Installation 43 | #### Clone the repository 44 | ```shell 45 | cd ~/catkin_ws/src 46 | git clone https://github.com/gisbi-kim/lt-mapper 47 | ``` 48 | 49 | #### Build the package 50 | ```shell 51 | cd .. 52 | catkin build ltslam removert 53 | ``` 54 | 55 | #### Using Docker 56 | ```shell 57 | cd ~/catkin_ws/src/lt-mapper/docker 58 | bash build.sh 59 | bash run.sh 60 | ``` 61 | or you can pull the pre-built image from Docker Hub. 62 | ```shell 63 | docker pull dongjae0107/lt-mapper:latest 64 | bash run.sh 65 | ``` 66 | 67 | 68 | ## How to use it 69 | 70 | ### 0. Single-session Data Generation 71 | To run the LT-mapper, you first need to generate a set of session data containing the following files: 72 | 73 | - **Keyframe Point Clouds** 74 | - **Keyframe Scan Context Descriptors (SCDs)** 75 | - **Pose Graph File** 76 | 77 | You can generate these session data files using the [saver tool](https://github.com/gisbi-kim/SC-LIO-SAM#applications) provided with [SC-LIO-SAM](https://github.com/gisbi-kim/SC-LIO-SAM). Alternatively, the saver is also available in [SC-A-LOAM](https://github.com/gisbi-kim/SC-A-LOAM) and [FAST_LIO_SLAM](https://github.com/gisbi-kim/FAST_LIO_SLAM). 78 | 79 | ### 1. LT-SLAM 80 | **LT-SLAM** module aligns trajectories across multiple sessions. For a step-by-step guide, check out this [tutorial video](https://youtu.be/BXBTVurNToU). 81 | 82 | ```shell 83 | # modify paths in ltslam/config/params.yaml before you run 84 | roslaunch ltslam run.launch 85 | ``` 86 | 87 | ### 2. LT-removert & LT-map 88 | **LT-removert & LT-map** module utilize [Removert](https://github.com/gisbi-kim/removert) to clean single-session data and detect changes between two sessions. 89 | 90 | ```shell 91 | # modify paths in ltremovert/config/params_ltmapper.yaml before you run 92 | roslaunch removert run_ltmapper.launch 93 | ``` 94 | 95 | **Outputs** 96 | 97 | Once the process is complete, following outputs will be generated: 98 |

99 | 100 | 101 | 102 | ## Example: LT-mapper on ParkingLot dataset 103 | 104 | You can download the ParkingLot dataset from this [link](https://bit.ly/ltmapper_parkinglot_data). To replay the data, you may need a [file player](https://github.com/RPM-Robotics-Lab/file_player_mulran). 105 | 106 | ParkingLot dataset includes 6 sequences captured over three days at the same spatial site but with varying initial poses. 107 | 108 | #### Step 1: Run LT-SLAM 109 | Update paths in ``ltslam/config/param.yaml`` and execute LT-SLAM. 110 | 111 | LT-SLAM will automatically align multiple sessions within a shared coordinate system. 112 | 113 |

114 | 115 | #### Step 2: Run LT-Removert & LT-Map 116 | Update paths in ``ltremovert/config/param_ltmapper.yaml`` and execute LT-removert & LT-map. 117 | 118 | After running LT-Removert & LT-Map, you will get cleaned session data and detected changes between sessions. 119 | 120 | 121 | 122 | 123 | 124 | ## Citation 125 | ```bibtex 126 | @inproceedings{kim2022lt, 127 | title={Lt-mapper: A modular framework for lidar-based lifelong mapping}, 128 | author={Kim, Giseop and Kim, Ayoung}, 129 | booktitle={2022 International Conference on Robotics and Automation (ICRA)}, 130 | pages={7995--8002}, 131 | year={2022}, 132 | organization={IEEE} 133 | } 134 | ``` 135 | 136 | 137 | ## Contact 138 | - Maintained by Giseop Kim and please contact the author via ``gsk@dgist.ac.kr`` 139 | 140 | ### Acknowledgement 141 | - Big thanks to [Dongjae Lee](https://dongjae0107.github.io/) for contributions in refactoring, dockerization, documentation, and various verifications. 142 | 143 | ### Contributors 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /doc/challenges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/lt-mapper/80b67567b356850a10e883c1530e4e69314eaa62/doc/challenges.png -------------------------------------------------------------------------------- /doc/ltmapper.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/lt-mapper/80b67567b356850a10e883c1530e4e69314eaa62/doc/ltmapper.pdf -------------------------------------------------------------------------------- /doc/outputs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/lt-mapper/80b67567b356850a10e883c1530e4e69314eaa62/doc/outputs.png -------------------------------------------------------------------------------- /doc/parkinglot_ltslam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/lt-mapper/80b67567b356850a10e883c1530e4e69314eaa62/doc/parkinglot_ltslam.png -------------------------------------------------------------------------------- /doc/pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/lt-mapper/80b67567b356850a10e883c1530e4e69314eaa62/doc/pipeline.png -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM osrf/ros:noetic-desktop-full 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt update && apt upgrade -y 6 | 7 | # Essential tools 8 | RUN apt install -y \ 9 | build-essential \ 10 | cmake \ 11 | git \ 12 | gedit \ 13 | unzip \ 14 | curl \ 15 | vim \ 16 | wget \ 17 | software-properties-common \ 18 | libboost-all-dev \ 19 | libeigen3-dev \ 20 | python3-catkin-tools 21 | 22 | # ROS 23 | RUN apt install -y ros-noetic-navigation \ 24 | ros-noetic-robot-localization \ 25 | ros-noetic-robot-state-publisher 26 | 27 | # catkin workspace 28 | ARG CATKIN_WS_DIR=/root/catkin_ws 29 | RUN . /opt/ros/noetic/setup.sh && mkdir -p ${CATKIN_WS_DIR}/src \ 30 | && cd ${CATKIN_WS_DIR} \ 31 | && catkin build \ 32 | && echo "source ${CATKIN_WS_DIR}/devel/setup.bash" >> ~/.bashrc 33 | 34 | # gtsam 35 | RUN apt update 36 | RUN add-apt-repository ppa:borglab/gtsam-release-4.0 37 | RUN apt update 38 | RUN apt install -y libgtsam-dev libgtsam-unstable-dev 39 | RUN rm -rf /var/lib/apt/lists/* 40 | 41 | # livox_ros_driver 42 | RUN git clone https://github.com/Livox-SDK/livox_ros_driver ${CATKIN_WS_DIR}/src/livox_ros_driver 43 | RUN git clone https://github.com/Livox-SDK/Livox-SDK2.git ${CATKIN_WS_DIR}/src/Livox-SDK2 44 | RUN cd ${CATKIN_WS_DIR}/src/Livox-SDK2/ && mkdir build && cd build && cmake .. && make -j && make install 45 | 46 | # ceres-solver 47 | RUN git clone https://ceres-solver.googlesource.com/ceres-solver -b 2.0.0 ${CATKIN_WS_DIR}/src/ceres-solver 48 | RUN apt update 49 | RUN apt install -y libgoogle-glog-dev libgflags-dev 50 | RUN apt install -y libatlas-base-dev 51 | RUN cd ${CATKIN_WS_DIR}/src/ceres-solver/ && mkdir build && cd build && cmake .. && make -j3 && make install 52 | 53 | # SC-LIO-SAM 54 | RUN git clone https://github.com/dongjae0107/SC-LIO-SAM.git ${CATKIN_WS_DIR}/src/SC-LIO-SAM 55 | RUN cd ${CATKIN_WS_DIR} && catkin build lio_sam 56 | 57 | # FAST-LIO-SLAM 58 | RUN git clone https://github.com/dongjae0107/FAST_LIO_SLAM.git ${CATKIN_WS_DIR}/src/FAST_LIO_SLAM 59 | # RUN cd ${CATKIN_WS_DIR}/src/FAST_LIO_SLAM && git submodule update --init 60 | RUN cd ${CATKIN_WS_DIR}/src/FAST_LIO_SLAM 61 | RUN cd ${CATKIN_WS_DIR} && catkin build livox_ros_driver fast_lio aloam_velodyne 62 | 63 | # LT-mapper 64 | RUN git clone https://github.com/dongjae0107/lt-mapper ${CATKIN_WS_DIR}/src/lt-mapper 65 | RUN cd ${CATKIN_WS_DIR} && catkin build ltslam removert 66 | 67 | RUN echo "source /opt/ros/noetic/setup.bash" >> /root/.bashrc 68 | RUN echo "source /root/catkin_ws/devel/setup.bash" >> /root/.bashrc 69 | 70 | # nvidia-container-runtime 71 | ENV NVIDIA_VISIBLE_DEVICES \ 72 | ${NVIDIA_VISIBLE_DEVICES:-all} 73 | ENV NVIDIA_DRIVER_CAPABILITIES \ 74 | ${NVIDIA_DRIVER_CAPABILITIES:+$NVIDIA_DRIVER_CAPABILITIES,}graphics 75 | 76 | 77 | ENTRYPOINT [ "/bin/bash" ] -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | docker build --no-cache --force-rm -f Dockerfile -t dongjae0107/lt-mapper:latest . -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | xhost + 2 | docker run --rm -it --ipc=host --net=host --privileged \ 3 | --env="DISPLAY" \ 4 | --volume="/etc/localtime:/etc/localtime:ro" \ 5 | dongjae0107/lt-mapper:latest 6 | xhost - -------------------------------------------------------------------------------- /ltmapper-final.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/lt-mapper/80b67567b356850a10e883c1530e4e69314eaa62/ltmapper-final.pdf -------------------------------------------------------------------------------- /ltremovert/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.3) 2 | project(removert) 3 | 4 | set(CMAKE_BUILD_TYPE "Release") 5 | set(CMAKE_CXX_STANDARD 17) # C++17 to use stdc++fs 6 | # set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Wall -g -pthread") 7 | 8 | find_package(catkin REQUIRED COMPONENTS 9 | tf 10 | roscpp 11 | rospy 12 | cv_bridge 13 | # pcl library 14 | pcl_conversions 15 | # msgs 16 | std_msgs 17 | sensor_msgs 18 | geometry_msgs 19 | nav_msgs 20 | message_generation 21 | image_transport 22 | ) 23 | 24 | find_package(OpenMP REQUIRED) 25 | find_package(PCL REQUIRED QUIET) 26 | find_package(OpenCV REQUIRED QUIET) 27 | # find_package(GTSAM REQUIRED QUIET) 28 | 29 | # add_message_files( 30 | # DIRECTORY msg 31 | # FILES 32 | # # cloud_info.msg 33 | # ) 34 | 35 | # generate_messages( 36 | # DEPENDENCIES 37 | # geometry_msgs 38 | # std_msgs 39 | # nav_msgs 40 | # sensor_msgs 41 | # ) 42 | 43 | catkin_package( 44 | INCLUDE_DIRS include 45 | DEPENDS PCL 46 | # GTSAM 47 | 48 | CATKIN_DEPENDS 49 | std_msgs 50 | nav_msgs 51 | geometry_msgs 52 | sensor_msgs 53 | message_runtime 54 | message_generation 55 | ) 56 | 57 | # include directories 58 | include_directories( 59 | include 60 | ${catkin_INCLUDE_DIRS} 61 | ${PCL_INCLUDE_DIRS} 62 | ${OpenCV_INCLUDE_DIRS} 63 | # ${GTSAM_INCLUDE_DIR} 64 | ) 65 | 66 | # link directories 67 | link_directories( 68 | include 69 | ${PCL_LIBRARY_DIRS} 70 | ${OpenCV_LIBRARY_DIRS} 71 | # ${GTSAM_LIBRARY_DIRS} 72 | ) 73 | 74 | ########### 75 | ## Build ## 76 | ########### 77 | 78 | # Range Image Projection 79 | add_executable( ${PROJECT_NAME}_removert src/removert_main.cpp 80 | src/utility.cpp 81 | src/RosParamServer.cpp 82 | src/Session.cpp 83 | src/Removerter.cpp 84 | ) 85 | 86 | add_dependencies(${PROJECT_NAME}_removert ${catkin_EXPORTED_TARGETS}) 87 | target_compile_options(${PROJECT_NAME}_removert PRIVATE ${OpenMP_CXX_FLAGS}) 88 | target_link_libraries(${PROJECT_NAME}_removert 89 | ${catkin_LIBRARIES} 90 | ${PCL_LIBRARIES} 91 | ${OpenCV_LIBRARIES} 92 | ${OpenMP_CXX_FLAGS} 93 | stdc++fs 94 | ) 95 | 96 | 97 | -------------------------------------------------------------------------------- /ltremovert/config/params_ltmapper.yaml: -------------------------------------------------------------------------------- 1 | removert: 2 | 3 | # 4 | isScanFileKITTIFormat: false 5 | 6 | # @ save option 7 | saveMapPCD: true 8 | saveCleanScansPCD: true 9 | save_pcd_directory: "/home/user/Desktop/ltslam-tutorial-kitti/data_ltmapper_mulran/riverside-rs/out/" # replace to your path (please use an absolute path) 10 | 11 | # @ sequence info (replace to your paths) 12 | # please follow the KITTI odometry dataset's scan and pose formats (i.e., make them .bin and line-by-line corresponding se3 poses composed of 12 numbers) 13 | central_sess_scan_dir: "/home/user/Desktop/ltslam-tutorial-kitti/data_scaloam_mulran/riverside/01/Scans/" 14 | central_sess_pose_path: "/home/user/Desktop/ltslam-tutorial-kitti/data_ltslam_mulran/riverside0102rs/01_central_aft_intersession_loops.txt" 15 | 16 | query_sess_scan_dir: "/home/user/Desktop/ltslam-tutorial-kitti/data_scaloam_mulran/riverside/02/Scans/" 17 | query_sess_pose_path: "/home/user/Desktop/ltslam-tutorial-kitti/data_ltslam_mulran/riverside0102rs/02_central_aft_intersession_loops.txt" 18 | 19 | # 20 | sequence_vfov: 50 # including upper and lower fovs, 21 | # for example, KITTI's HDL64E is 2 + 24.9 ~ 27 deg. (so 25 + 25 = 50 is recommended because it is enough) 22 | # , MulRan's Ouster OS1-64 has +22.5 to -22.5 d8eg. (so 22.5 + 22.5 = 45 < 50 is also enough and able to cover the all vfov) 23 | sequence_hfov: 360 # generally for targetting scanning LiDAR but a lidar having restricted hfov also just can use the 360 (because no point regions are considered in the algorithm) 24 | 25 | 26 | # @ Sequence's BaseToLidar info 27 | # @ If you use the lidar-itself odometry (e.g., from SC-LIO-SAM's pose saver, the pose is from lidar itself so use the identity) 28 | ExtrinsicLiDARtoPoseBase: [1.0, 0.0, 0.0, 0.0, 29 | 0.0, 1.0, 0.0, 0.0, 30 | 0.0, 0.0, 1.0, 0.0, 31 | 0.0, 0.0, 0.0, 1.0] 32 | 33 | 34 | # @ Sampling nodes 35 | use_keyframe_gap: true 36 | keyframe_gap: 1 # if you use SC-LIO-SAM's poses, the nodes are already keyframe (i.e., spatially apart enough) 37 | 38 | start_idx: 1100 # change this // for 0103 39 | end_idx: 1200 # change this // for 0103 40 | 41 | 42 | # @ auto repeat 43 | # means the removeter runs on all scans in a sequence 44 | clean_for_all_scan: false # default is false 45 | batch_size: 150 46 | valid_ratio_to_save: 0.75 # i.e., no save cleaned scans located at the boundaries of a batch 47 | 48 | 49 | # @ Range image resolution 50 | # the below is actually magnifier ratio (i.e., 5 means x5 resolution, the x1 means 1 deg x 1 deg per pixel) 51 | # - recommend to use the first removing resolution's magnifier ratio should meet the seonsor vertical fov / number of rays 52 | # - e.g., HDL 64E of KITTI dataset -> appx 25 deg / 64 ray ~ 0.4 deg per pixel -> the magnifier ratio = 1/0.4 = 2.5 53 | # - e.g., Ouster OS1-64 of MulRan dataset -> appx 45 deg / 64 ray ~ 0.7 deg per pixel -> the magnifier ratio = 1/0.7 = 1.4 54 | # - recommend to use the first reverting resolution's magnifier ratio should lied in 1.0 to 1.5 55 | remove_resolution_list: [2.5] # for Ouster OS1-64 of MulRan dataset 56 | revert_resolution_list: [2.2] # TODO 57 | 58 | 59 | # @ Removert params 60 | # about density 61 | downsample_voxel_size: 0.05 # user parameter but recommend to use 0.05 to make sure an enough density (this value is related to the removing resolution's expected performance) 62 | 63 | # about Static sensitivity (you need to tune these below two values, depends on the environement) 64 | # - if you use a raw scan (saved from the SC-A-LOAM or SC-LIO-SAM saver utility) 65 | num_nn_points_within: 2 # how many - using higher, more strict static 66 | dist_nn_points_within: 0.01 # meter - using smaller, more strict static (if you use a already downsampled feature cloud, use quite large value such as > 0.2 ) 67 | 68 | # @ For faster 69 | num_omp_cores: 16 # for faster map points projection (to make a map range image) 70 | 71 | # @ For visualization of range images (rviz -d removert_visualization.rviz) 72 | rimg_color_min: 0.0 # meter 73 | rimg_color_max: 20.0 # meter -------------------------------------------------------------------------------- /ltremovert/include/removert/Removerter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "removert/RosParamServer.h" 4 | #include "removert/Session.h" 5 | 6 | namespace ltremovert 7 | { 8 | 9 | class Removerter : public RosParamServer 10 | { 11 | private: 12 | 13 | ros::Subscriber subLaserCloud; // TODO, for real-time integration (later) 14 | 15 | Session central_sess_; 16 | Session query_sess_; 17 | 18 | std::string updated_scans_save_dir_; 19 | std::string updated_strong_scans_save_dir_; 20 | std::string pd_scans_save_dir; 21 | std::string strong_pd_scans_save_dir; 22 | std::string strong_nd_scans_save_dir; 23 | 24 | std::string central_map_static_save_dir_; 25 | std::string central_map_dynamic_save_dir_; 26 | 27 | pcl::IterativeClosestPoint icp_; 28 | 29 | // unuseds 30 | pcl::PointCloud::Ptr map_subset_global_curr_; // unused 31 | pcl::PointCloud::Ptr map_global_accumulated_static_; // TODO, the S_i after reverted 32 | pcl::PointCloud::Ptr map_global_accumulated_dynamic_; // TODO, the D_i after reverted 33 | 34 | std::vector::Ptr> static_map_global_history_; // TODO 35 | std::vector::Ptr> dynamic_map_global_history_; // TODO 36 | 37 | // configs 38 | std::pair curr_rimg_shape_; 39 | 40 | float curr_res_alpha_; // just for tracking current status 41 | 42 | const int base_node_idx_ = 0; 43 | 44 | unsigned long kPauseTimeForClearStaticScanVisualization = 1000; // microsec 45 | 46 | // NOT recommend to use for under 5 million points map input (becausing not-using is just faster) 47 | const bool kUseSubsetMapCloud = false; 48 | const float kBallSize = 80.0; // meter 49 | 50 | // for viz 51 | image_transport::ImageTransport ROSimg_transporter_; 52 | 53 | sensor_msgs::ImagePtr scan_rimg_msg_; 54 | image_transport::Publisher scan_rimg_msg_publisher_; 55 | 56 | sensor_msgs::ImagePtr map_rimg_msg_; 57 | image_transport::Publisher map_rimg_msg_publisher_; 58 | 59 | sensor_msgs::ImagePtr diff_rimg_msg_; 60 | image_transport::Publisher diff_rimg_msg_publisher_; 61 | 62 | sensor_msgs::ImagePtr map_rimg_ptidx_msg_; 63 | image_transport::Publisher map_rimg_ptidx_msg_publisher_; 64 | 65 | 66 | public: 67 | Removerter(); 68 | ~Removerter(); 69 | 70 | void cloudHandler(const sensor_msgs::PointCloud2ConstPtr& laserCloudMsg); // TODO (if removert run online) 71 | 72 | void allocateMemory(); 73 | 74 | void loadSessionInfo( void ); 75 | void parseKeyframes( void ); 76 | void loadKeyframes( void ); 77 | void precleaningKeyframes(float _radius); 78 | 79 | // void readValidScans(); 80 | 81 | void selfRemovert(const Session& _sess, int _repeat); 82 | void removeHighDynamicPoints(void); 83 | void detectLowDynamicPoints(void); 84 | 85 | void mergeScansWithinGlobalCoord( 86 | const std::vector::Ptr>& _scans, 87 | const std::vector& _scans_poses, 88 | pcl::PointCloud::Ptr& _ptcloud_to_save ); 89 | 90 | void makeGlobalMap(Session& _sess); 91 | void makeGlobalMap(); 92 | 93 | void updateCurrentMap(void); // the updated one is saved in the central session's class 94 | 95 | void run(void); 96 | 97 | void parseStaticScansViaProjection(Session& _sess); 98 | void parseStaticScansViaProjection(void); 99 | 100 | std::pair::Ptr, pcl::PointCloud::Ptr> 101 | partitionCurrentMap(const Session& _target_sess, const Session& _source_sess, float _res_alpha); 102 | 103 | void removeOnce(const Session& _target_sess, const Session& _source_sess, float _res_alpha); 104 | void removeOnce(float _res); 105 | 106 | void revertOnce( const Session& _target_sess, const Session& _source_sess, float _res_alpha ); 107 | void revertOnce(float _res); 108 | 109 | void resetCurrrentMapAsDynamic(const Session& _sess, bool _as_dynamic); 110 | void resetCurrrentMapAsDynamic(const Session& _sess); 111 | void resetCurrrentMapAsStatic(const Session& _sess); 112 | 113 | void saveCurrentStaticMapHistory(void); // the 0th element is a noisy (original input) (actually not static) map. 114 | void saveCurrentDynamicMapHistory(void); 115 | 116 | void takeGlobalMapSubsetWithinBall( int _center_scan_idx ); 117 | void transformGlobalMapSubsetToLocal(int _base_scan_idx); 118 | 119 | cv::Mat scan2RangeImg(const pcl::PointCloud::Ptr& _scan, 120 | const std::pair _fov, /* e.g., [vfov = 50 (upper 25, lower 25), hfov = 360] */ 121 | const std::pair _rimg_size); 122 | // std::pair map2RangeImg(const pcl::PointCloud::Ptr& _scan, 123 | // const std::pair _fov, /* e.g., [vfov = 50 (upper 25, lower 25), hfov = 360] */ 124 | // const std::pair _rimg_size); 125 | pcl::PointCloud::Ptr parseProjectedPoints(const pcl::PointCloud::Ptr& _scan, 126 | const std::pair _fov, /* e.g., [vfov = 50 (upper 25, lower 25), hfov = 360] */ 127 | const std::pair _rimg_size); 128 | 129 | std::vector calcDescrepancyAndParseDynamicPointIdx( 130 | const cv::Mat& _scan_rimg, const cv::Mat& _diff_rimg, const cv::Mat& _map_rimg_ptidx, float _diff_thres = 0.1); 131 | 132 | std::vector calcDescrepancyAndParseDynamicPointIdxForEachScan( std::pair _rimg_shape ); 133 | std::vector calcDescrepancyAndParseDynamicPointIdxForEachScan( 134 | const Session& _target_sess, const Session& _source_sess, std::pair _rimg_shape); 135 | 136 | std::vector getStaticIdxFromDynamicIdx(const std::vector& _dynamic_point_indexes, int _num_all_points); 137 | std::vector getGlobalMapStaticIdxFromDynamicIdx(const std::vector& _dynamic_point_indexes); 138 | std::vector getGlobalMapStaticIdxFromDynamicIdx(const pcl::PointCloud::Ptr& _map_global_curr, 139 | std::vector &_dynamic_point_indexes); 140 | 141 | void parsePointcloudSubsetUsingPtIdx( const pcl::PointCloud::Ptr& _ptcloud_orig, 142 | std::vector& _point_indexes, pcl::PointCloud::Ptr& _ptcloud_to_save ); 143 | void parseMapPointcloudSubsetUsingPtIdx( std::vector& _point_indexes, pcl::PointCloud::Ptr& _ptcloud_to_save ); 144 | void parseStaticMapPointcloudUsingPtIdx( std::vector& _point_indexes ); 145 | void parseDynamicMapPointcloudUsingPtIdx( std::vector& _point_indexes ); 146 | 147 | void saveCurrentStaticAndDynamicPointCloudGlobal( void ); 148 | void saveCurrentStaticAndDynamicPointCloudLocal( int _base_pose_idx = 0); 149 | void saveCurrentStaticAndDynamicPointCloudGlobal(const Session& _sess, std::string _postfix); 150 | 151 | pcl::PointCloud::Ptr local2global(const pcl::PointCloud::Ptr& _scan_local, const Eigen::Matrix4d& _scan_pose); 152 | pcl::PointCloud::Ptr global2local(const pcl::PointCloud::Ptr& _scan_global, const Eigen::Matrix4d& _scan_pose_inverse); 153 | 154 | // scan-side removal 155 | std::pair::Ptr, pcl::PointCloud::Ptr> removeDynamicPointsOfScanByKnn ( int _scan_idx ); 156 | std::pair::Ptr, pcl::PointCloud::Ptr> removeDynamicPointsOfScanByKnn( int _scan_idx, 157 | const Session& _sess, const pcl::KdTreeFLANN::Ptr& _kdtree_map_global_curr); 158 | std::pair::Ptr, pcl::PointCloud::Ptr> removeDynamicPointsOfScanByKnnBtnSession ( int _scan_idx ); 159 | void removeDynamicPointsAndSaveStaticScanForEachScan( void ); 160 | 161 | void scansideRemovalForEachScan(void); 162 | void scansideRemovalForEachScan(const Session& _map_sess, const Session& _scan_sess); 163 | void scansideRemovalViaMapReprojectionForEachScan(void); 164 | void saveCleanedScans(void); 165 | void saveCleanedScans(const Session& _sess); 166 | // void saveMapPointcloudByMergingCleanedScans(void); 167 | void saveMapPointcloudByMergingCleanedScans( const Session& _sess, std::string _postfix ); 168 | void scansideRemovalForEachScanAndSaveThem(void); 169 | void scansideRemovalViaMapReprojectionForEachScanAndSaveThem(void); 170 | 171 | void saveStaticScan( int _scan_idx, const pcl::PointCloud::Ptr& _ptcloud ); 172 | void saveDynamicScan( int _scan_idx, const pcl::PointCloud::Ptr& _ptcloud ); 173 | 174 | void filterStrongND(Session& _sess_src, Session& _sess_cleaner); 175 | void iremoveOnceForND(const Session& _target_sess, const Session& _source_sess, float _res_alpha); 176 | std::pair partitionCurrentMapForND(const Session& _target_sess, const Session& _source_sess, float _res_alpha); 177 | std::vector calcDescrepancyAndParseDynamicPointIdxForEachScanForND( 178 | const Session& _target_sess, const Session& _source_sess, std::pair _rimg_shape); 179 | 180 | void filterStrongPD(Session& _sess_src, Session& _sess_cleaner); 181 | void removeOnceForPD(const Session& _target_sess, const Session& _source_sess, float _res_alpha); 182 | std::pair partitionCurrentMapForPD(const Session& _target_sess, const Session& _source_sess, float _res_alpha); 183 | std::vector calcDescrepancyAndParseDynamicPointIdxForEachScanForPD( 184 | const Session& _target_sess, const Session& _source_sess, std::pair _rimg_shape); 185 | 186 | void parseUpdatedStaticScansViaProjection( void ); 187 | void parseUpdatedStaticScansViaProjection(Session& _sess); 188 | 189 | void parseLDScansViaProjection(); 190 | void parseLDScansViaProjection(Session& _sess); 191 | 192 | void updateScansScanwise(Session& _sess); 193 | void updateScansScanwise(); 194 | 195 | void saveAllTypeOfScans(); 196 | void saveUpdatedScans( Session& _sess ); 197 | void saveLDScans( Session& _sess ); 198 | void savePDScans( Session& _sess ); 199 | void saveStrongPDScans( Session& _sess ); 200 | void saveStrongNDScans( Session& _sess ); 201 | void saveScans(Session& _sess, std::vector::Ptr> _scans, std::string _save_dir); 202 | 203 | 204 | }; // Removerter 205 | 206 | } // namespace ltremovert 207 | -------------------------------------------------------------------------------- /ltremovert/include/removert/RosParamServer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "removert/utility.h" 4 | 5 | class RosNodeHandle 6 | { 7 | public: 8 | ros::NodeHandle nh_super; 9 | }; // class: RosNodeHandle 10 | 11 | 12 | class RosParamServer: public RosNodeHandle 13 | { 14 | public: 15 | // 16 | ros::NodeHandle & nh; 17 | 18 | // 19 | std::string pointcloud_topic; 20 | 21 | // removert params 22 | float kVFOV; 23 | float kHFOV; 24 | std::pair kFOV; 25 | 26 | float kDownsampleVoxelSize; 27 | 28 | // pose base where 29 | std::vector kVecExtrinsicLiDARtoPoseBase; 30 | Eigen::Matrix4d kSE3MatExtrinsicLiDARtoPoseBase; 31 | // Base is where of the pose writtened (e.g., for KITTI, poses is usually in camera) 32 | // if the pose file is obtained via lidar odometry itself, then kMatExtrinsicLiDARtoBase is eye(4) 33 | Eigen::Matrix4d kSE3MatExtrinsicPoseBasetoLiDAR; 34 | 35 | // config 36 | bool isScanFileKITTIFormat_; 37 | 38 | // knn 39 | int kNumKnnPointsToCompare;// static sensitivity (increase this value, less static structure will be removed at the scan-side removal stage) 40 | float kScanKnnAndMapKnnAvgDiffThreshold; // static sensitivity (decrease this value, less static structure will be removed at the scan-side removal stage) 41 | 42 | // sessions' paths 43 | std::string central_sess_scan_dir_; 44 | std::string central_sess_pose_path_; 45 | std::string query_sess_scan_dir_; 46 | std::string query_sess_pose_path_; 47 | 48 | // target region to removerting 49 | int start_idx_; 50 | int end_idx_; 51 | 52 | bool use_keyframe_gap_; 53 | bool use_keyframe_meter_; 54 | int keyframe_gap_; 55 | float keyframe_gap_meter_; 56 | 57 | int repeat_removert_iter_; 58 | 59 | // 60 | std::vector remove_resolution_list_; 61 | std::vector revert_resolution_list_; 62 | 63 | // 64 | int kNumOmpCores; 65 | 66 | // 67 | pcl::PointCloud::Ptr single_scan; 68 | pcl::PointCloud::Ptr projected_scan; 69 | 70 | // ros pub 71 | ros::Publisher scan_publisher_; 72 | ros::Publisher global_scan_publisher_; 73 | 74 | ros::Publisher original_map_local_publisher_; 75 | 76 | ros::Publisher curr_map_local_publisher_; 77 | ros::Publisher static_map_local_publisher_; 78 | ros::Publisher dynamic_map_local_publisher_; 79 | 80 | ros::Publisher static_curr_scan_publisher_; 81 | ros::Publisher dynamic_curr_scan_publisher_; 82 | 83 | float rimg_color_min_; 84 | float rimg_color_max_; 85 | std::pair kRangeColorAxis; // meter 86 | std::pair kRangeColorAxisForDiff; // meter 87 | 88 | 89 | // 90 | bool kFlagSaveMapPointcloud; 91 | bool kFlagSaveCleanScans; 92 | std::string save_pcd_directory_; 93 | 94 | public: 95 | RosParamServer(); 96 | 97 | }; // class: RosParamServer 98 | -------------------------------------------------------------------------------- /ltremovert/include/removert/Session.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "removert/RosParamServer.h" 4 | #include "removert/utility.h" 5 | 6 | namespace ltremovert 7 | { 8 | 9 | class Session : public RosParamServer 10 | { 11 | public: 12 | 13 | const float kReprojectionAlpha = 3.0; 14 | 15 | std::string sess_type_; 16 | 17 | // config 18 | float kDownsampleVoxelSize; 19 | 20 | // all 21 | std::string scan_dir_; 22 | std::string pose_path_; 23 | 24 | // including all scans inforamtion in the session 25 | std::vector scan_names_; 26 | std::vector scan_paths_; 27 | std::vector scan_poses_; // including all scans inforamtion in the session 28 | std::vector scan_inverse_poses_; 29 | int num_scans_; 30 | 31 | // keyframes (for efficient processing) 32 | int keyframe_gap_; 33 | std::vector keyframe_names_; 34 | std::vector keyframe_paths_; 35 | std::vector keyframe_poses_; 36 | std::vector keyframe_inverse_poses_; 37 | int num_keyframes_; 38 | 39 | std::vector::Ptr> keyframe_scans_; 40 | std::vector::Ptr> keyframe_scans_static_; 41 | 42 | std::vector::Ptr> keyframe_scans_static_projected_; //keyframes 43 | std::vector::Ptr> keyframe_scans_dynamic_; 44 | 45 | pcl::PointCloud::Ptr target_map_down_for_knn_; 46 | std::vector::Ptr> scans_knn_coexist_; // keyframes, between session 47 | std::vector::Ptr> scans_knn_diff_; // keyframes, between session 48 | 49 | std::vector::Ptr> keyframe_scans_updated_; //keyframes 50 | std::vector::Ptr> keyframe_scans_updated_strong_; //keyframes 51 | std::vector::Ptr> keyframe_scans_pd_; //keyframes 52 | std::vector::Ptr> keyframe_scans_strong_pd_; //keyframes 53 | std::vector::Ptr> keyframe_scans_strong_nd_; //keyframes 54 | std::vector::Ptr> keyframe_scans_weak_nd_; //keyframes 55 | 56 | // 57 | pcl::KdTreeFLANN::Ptr kdtree_map_global_curr_; 58 | 59 | pcl::KdTreeFLANN::Ptr kdtree_target_map_global_; 60 | pcl::KdTreeFLANN::Ptr kdtree_scan_global_curr_; 61 | 62 | pcl::KdTreeFLANN::Ptr kdtree_map_global_nd_; 63 | pcl::KdTreeFLANN::Ptr kdtree_map_global_pd_; 64 | 65 | pcl::PointCloud::Ptr map_global_orig_; 66 | pcl::PointCloud::Ptr map_global_curr_; // the M_i. i.e., removert is: M1 -> S1 + D1, D1 -> M2 , M2 -> S2 + D2 ... repeat ... 67 | pcl::PointCloud::Ptr map_local_curr_; 68 | pcl::PointCloud::Ptr map_global_curr_static_; // the S_i 69 | pcl::PointCloud::Ptr map_global_curr_dynamic_; // the D_i 70 | 71 | pcl::PointCloud::Ptr map_global_updated_; 72 | pcl::PointCloud::Ptr map_global_updated_strong_; 73 | 74 | pcl::PointCloud::Ptr map_global_curr_diff_positive_; // the S_i 75 | pcl::PointCloud::Ptr map_global_curr_diff_negative_; // the D_i 76 | pcl::PointCloud::Ptr map_global_curr_down_for_icp_; 77 | 78 | pcl::PointCloud::Ptr map_global_nd_; 79 | pcl::PointCloud::Ptr map_local_nd_; 80 | pcl::PointCloud::Ptr map_global_nd_strong_; 81 | pcl::PointCloud::Ptr map_global_nd_weak_; // == ambigous 82 | 83 | pcl::PointCloud::Ptr map_global_pd_; 84 | pcl::PointCloud::Ptr map_global_pd_orig_; 85 | pcl::PointCloud::Ptr map_local_pd_; 86 | pcl::PointCloud::Ptr map_global_pd_strong_; 87 | pcl::PointCloud::Ptr map_global_pd_weak_; 88 | 89 | pcl::IterativeClosestPoint icp_for_knn_; 90 | 91 | std::mutex mtx; 92 | 93 | public: 94 | Session( void ); 95 | 96 | void allocateMemory(); 97 | 98 | void loadSessionInfo( std::string _sess_type, std::string _scan_dir, std::string _pose_path ); 99 | void setDownsampleSize( float _voxel_size ); 100 | 101 | void clearKeyframes( void ); 102 | 103 | void parseKeyframes( int _gap = 1 ); 104 | void parseKeyframes( std::pair _range, int _gap = 1 ); 105 | void parseKeyframesInROI( const std::vector& _roi_poses, int _gap = 1 ); 106 | 107 | void loadKeyframes( void ); 108 | void precleaningKeyframes ( float _radius ); 109 | void mergeScansWithinGlobalCoord(void); 110 | 111 | void parseStaticScansViaProjection(void); 112 | void parseUpdatedStaticScansViaProjection(void); 113 | void parseUpdatedStrongStaticScansViaProjection(void); 114 | void parsePDScansViaProjection(void); 115 | void parseStrongPDScansViaProjection(void); 116 | void parseWeakNDScansViaProjection(void); 117 | void parseStrongNDScansViaProjection(void); 118 | void parseScansViaProjection( 119 | pcl::PointCloud::Ptr _map, // to_be_parsed 120 | std::vector::Ptr>& _vec_to_store ); 121 | 122 | void updateScansScanwise(); 123 | 124 | void extractLowDynPointsViaKnnDiff(const pcl::PointCloud::Ptr _target_map); 125 | std::pair partitionLowDynamicPointsOfScanByKnn(int _scan_idx); 126 | 127 | void extractHighDynPointsViaKnnDiff(const pcl::PointCloud::Ptr _target_map); 128 | std::pair partitionHighDynamicPointsOfScanByKnn(int _scan_idx); 129 | 130 | void constructGlobalNDMap(); 131 | void removeWeakNDMapPointsHavingStrongNDInNear(); 132 | 133 | void constructGlobalPDMap(); 134 | void revertStrongPDMapPointsHavingWeakPDInNear(); 135 | 136 | }; 137 | 138 | 139 | 140 | } // namespace ltremovert -------------------------------------------------------------------------------- /ltremovert/include/removert/utility.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef _UTILITY_REMOVERT_H_ 3 | #define _UTILITY_REMOVERT_H_ 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | #include 43 | #include 44 | #include 45 | #include 46 | 47 | #include 48 | #include 49 | 50 | #include 51 | #include 52 | #include 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include 60 | #include 61 | #include 62 | #include 63 | #include 64 | #include 65 | #include 66 | #include 67 | #include 68 | #include 69 | 70 | #include // requires gcc version >= 8 71 | 72 | namespace fs = std::filesystem; 73 | using std::ios; 74 | using std::cout; 75 | using std::cerr; 76 | using std::endl; 77 | 78 | // struct PointXYZIS 79 | // { 80 | // PCL_ADD_POINT4D 81 | // float intensity; 82 | // float score; 83 | // EIGEN_MAKE_ALIGNED_OPERATOR_NEW 84 | // } EIGEN_ALIGN16; 85 | 86 | // POINT_CLOUD_REGISTER_POINT_STRUCT (PointXYZIS, 87 | // (float, x, x) (float, y, y) (float, z, z) (float, intensity, intensity) (float, score, score) 88 | // ) 89 | 90 | using PointType = pcl::PointXYZI; 91 | using pcPtr = pcl::PointCloud::Ptr; 92 | 93 | const float kFlagNoPOINT = 10000.0; // no point constant, 10000 has no meaning, but must be larger than the maximum scan range (e.g., 200 meters) 94 | const float kValidDiffUpperBound = 200.0; // must smaller than kFlagNoPOINT 95 | 96 | struct SphericalPoint 97 | { 98 | float az; // azimuth 99 | float el; // elevation 100 | float r; // radius 101 | }; 102 | 103 | float rad2deg(float radians); 104 | float deg2rad(float degrees); 105 | 106 | void readBin(std::string _bin_path, pcl::PointCloud::Ptr _pcd_ptr); 107 | 108 | std::vector splitPoseLine(std::string _str_line, char _delimiter); 109 | 110 | SphericalPoint cart2sph(const PointType & _cp); 111 | 112 | std::pair resetRimgSize(const std::pair _fov, const float _resize_ratio); 113 | 114 | template 115 | cv::Mat convertColorMappedImg (const cv::Mat &_src, std::pair _caxis) 116 | { 117 | T min_color_val = _caxis.first; 118 | T max_color_val = _caxis.second; 119 | 120 | cv::Mat image_dst; 121 | image_dst = 255 * (_src - min_color_val) / (max_color_val - min_color_val); 122 | image_dst.convertTo(image_dst, CV_8UC1); 123 | 124 | cv::applyColorMap(image_dst, image_dst, cv::COLORMAP_JET); 125 | 126 | return image_dst; 127 | } 128 | 129 | pcl::PointCloud::Ptr parseProjectedPoints(const pcl::PointCloud::Ptr &_scan, 130 | const std::pair _fov, /* e.g., [vfov = 50 (upper 25, lower 25), hfov = 360] */ 131 | const std::pair _rimg_size); 132 | 133 | std::pair map2RangeImg(const pcl::PointCloud::Ptr &_scan, 134 | const std::pair _fov, /* e.g., [vfov = 50 (upper 25, lower 25), hfov = 360] */ 135 | const std::pair _rimg_size); 136 | 137 | void transformGlobalMapToLocal( 138 | const pcl::PointCloud::Ptr &_map_global, 139 | Eigen::Matrix4d _base_pose_inverse, Eigen::Matrix4d _base2lidar, 140 | pcl::PointCloud::Ptr &_map_local); 141 | 142 | void octreeDownsampling(const pcl::PointCloud::Ptr &_src, pcl::PointCloud::Ptr &_to_save); 143 | void octreeDownsampling(const pcl::PointCloud::Ptr &_src, pcl::PointCloud::Ptr &_to_save, 144 | const float _kDownsampleVoxelSize); 145 | 146 | std::set convertIntVecToSet(const std::vector & v); 147 | 148 | pcl::PointCloud::Ptr mergeScansWithinGlobalCoordUtil( 149 | const std::vector::Ptr> &_scans, 150 | const std::vector &_scans_poses, Eigen::Matrix4d _lidar2base); 151 | 152 | pcl::PointCloud::Ptr local2global(const pcl::PointCloud::Ptr &_scan_local, 153 | const Eigen::Matrix4d &_scan_pose, const Eigen::Matrix4d &_base2lidar); 154 | 155 | pcl::PointCloud::Ptr global2local(const pcl::PointCloud::Ptr &_scan_global, 156 | const Eigen::Matrix4d &_scan_pose_inverse, const Eigen::Matrix4d &_base2lidar); 157 | 158 | template 159 | std::vector linspace(T a, T b, size_t N) { 160 | T h = (b - a) / static_cast(N-1); 161 | std::vector xs(N); 162 | typename std::vector::iterator x; 163 | T val; 164 | for (x = xs.begin(), val = a; x != xs.end(); ++x, val += h) 165 | *x = val; 166 | return xs; 167 | } 168 | 169 | sensor_msgs::ImagePtr cvmat2msg(const cv::Mat &_img); 170 | 171 | void pubRangeImg(cv::Mat& _rimg, sensor_msgs::ImagePtr& _msg, image_transport::Publisher& _publiser, std::pair _caxis); 172 | void publishPointcloud2FromPCLptr(const ros::Publisher& _scan_publisher, const pcl::PointCloud::Ptr _scan); 173 | sensor_msgs::PointCloud2 publishCloud(ros::Publisher *thisPub, pcl::PointCloud::Ptr thisCloud, ros::Time thisStamp, std::string thisFrame); 174 | 175 | template 176 | double ROS_TIME(T msg) 177 | { 178 | return msg->header.stamp.toSec(); 179 | } 180 | 181 | float pointDistance(PointType p); 182 | float pointDistance(PointType p1, PointType p2); 183 | 184 | #endif -------------------------------------------------------------------------------- /ltremovert/launch/removert_visualization.rviz: -------------------------------------------------------------------------------- 1 | Panels: 2 | - Class: rviz/Displays 3 | Help Height: 78 4 | Name: Displays 5 | Property Tree Widget: 6 | Expanded: 7 | - /Image1 8 | - /Image2 9 | - /Image3 10 | Splitter Ratio: 0.4148681163787842 11 | Tree Height: 308 12 | - Class: rviz/Selection 13 | Name: Selection 14 | - Class: rviz/Tool Properties 15 | Expanded: 16 | - /2D Pose Estimate1 17 | - /2D Nav Goal1 18 | - /Publish Point1 19 | Name: Tool Properties 20 | Splitter Ratio: 0.5886790156364441 21 | - Class: rviz/Views 22 | Expanded: 23 | - /Current View1 24 | Name: Views 25 | Splitter Ratio: 0.5 26 | - Class: rviz/Time 27 | Experimental: false 28 | Name: Time 29 | SyncMode: 0 30 | SyncSource: Image 31 | Preferences: 32 | PromptSaveOnExit: true 33 | Toolbars: 34 | toolButtonStyle: 2 35 | Visualization Manager: 36 | Class: "" 37 | Displays: 38 | - Alpha: 1 39 | Autocompute Intensity Bounds: true 40 | Autocompute Value Bounds: 41 | Max Value: 10 42 | Min Value: -10 43 | Value: true 44 | Axis: Z 45 | Channel Name: intensity 46 | Class: rviz/PointCloud2 47 | Color: 239; 41; 41 48 | Color Transformer: FlatColor 49 | Decay Time: 0 50 | Enabled: true 51 | Invert Rainbow: false 52 | Max Color: 255; 255; 255 53 | Max Intensity: 0.6600000262260437 54 | Min Color: 0; 0; 0 55 | Min Intensity: 0 56 | Name: PointCloud2 57 | Position Transformer: XYZ 58 | Queue Size: 10 59 | Selectable: true 60 | Size (Pixels): 3 61 | Size (m): 0.5 62 | Style: Spheres 63 | Topic: /removert/scan_single_local_dynamic 64 | Unreliable: false 65 | Use Fixed Frame: true 66 | Use rainbow: true 67 | Value: true 68 | - Alpha: 1 69 | Autocompute Intensity Bounds: true 70 | Autocompute Value Bounds: 71 | Max Value: 10 72 | Min Value: -10 73 | Value: true 74 | Axis: Z 75 | Channel Name: intensity 76 | Class: rviz/PointCloud2 77 | Color: 138; 226; 52 78 | Color Transformer: FlatColor 79 | Decay Time: 0 80 | Enabled: true 81 | Invert Rainbow: false 82 | Max Color: 255; 255; 255 83 | Max Intensity: 0.9900000095367432 84 | Min Color: 0; 0; 0 85 | Min Intensity: 0 86 | Name: PointCloud2 87 | Position Transformer: XYZ 88 | Queue Size: 10 89 | Selectable: true 90 | Size (Pixels): 1 91 | Size (m): 0.10000000149011612 92 | Style: Points 93 | Topic: /removert/scan_single_local_static 94 | Unreliable: false 95 | Use Fixed Frame: true 96 | Use rainbow: true 97 | Value: true 98 | - Class: rviz/Image 99 | Enabled: true 100 | Image Topic: /scan_rimg_single 101 | Max Value: 1 102 | Median window: 5 103 | Min Value: 0 104 | Name: Image 105 | Normalize Range: true 106 | Queue Size: 2 107 | Transport Hint: raw 108 | Unreliable: false 109 | Value: true 110 | - Class: rviz/Image 111 | Enabled: true 112 | Image Topic: /map_rimg_single 113 | Max Value: 1 114 | Median window: 5 115 | Min Value: 0 116 | Name: Image 117 | Normalize Range: true 118 | Queue Size: 2 119 | Transport Hint: raw 120 | Unreliable: false 121 | Value: true 122 | - Class: rviz/Image 123 | Enabled: true 124 | Image Topic: /diff_rimg_single 125 | Max Value: 1 126 | Median window: 5 127 | Min Value: 0 128 | Name: Image 129 | Normalize Range: true 130 | Queue Size: 2 131 | Transport Hint: raw 132 | Unreliable: false 133 | Value: true 134 | Enabled: true 135 | Global Options: 136 | Background Color: 0; 0; 0 137 | Default Light: true 138 | Fixed Frame: removert 139 | Frame Rate: 30 140 | Name: root 141 | Tools: 142 | - Class: rviz/Interact 143 | Hide Inactive Objects: true 144 | - Class: rviz/MoveCamera 145 | - Class: rviz/Select 146 | - Class: rviz/FocusCamera 147 | - Class: rviz/Measure 148 | - Class: rviz/SetInitialPose 149 | Theta std deviation: 0.2617993950843811 150 | Topic: /initialpose 151 | X std deviation: 0.5 152 | Y std deviation: 0.5 153 | - Class: rviz/SetGoal 154 | Topic: /move_base_simple/goal 155 | - Class: rviz/PublishPoint 156 | Single click: true 157 | Topic: /clicked_point 158 | Value: true 159 | Views: 160 | Current: 161 | Class: rviz/Orbit 162 | Distance: 101.88595581054688 163 | Enable Stereo Rendering: 164 | Stereo Eye Separation: 0.05999999865889549 165 | Stereo Focal Distance: 1 166 | Swap Stereo Eyes: false 167 | Value: false 168 | Focal Point: 169 | X: -0.6624842882156372 170 | Y: 3.6606950759887695 171 | Z: 0.8095508813858032 172 | Focal Shape Fixed Size: true 173 | Focal Shape Size: 0.05000000074505806 174 | Invert Z Axis: false 175 | Name: Current View 176 | Near Clip Distance: 0.009999999776482582 177 | Pitch: 0.9153987169265747 178 | Target Frame: 179 | Value: Orbit (rviz) 180 | Yaw: 4.498590469360352 181 | Saved: ~ 182 | Window Geometry: 183 | Displays: 184 | collapsed: false 185 | Height: 752 186 | Hide Left Dock: false 187 | Hide Right Dock: true 188 | Image: 189 | collapsed: false 190 | QMainWindow State: 000000ff00000000fd0000000400000000000001a1000001bffc0200000008fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003d000001bf000000c900fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261000000010000010f0000035afc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003d0000035a000000a400fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e1000001970000000300000751000000d1fc0100000006fb0000000a0049006d0061006700650100000000000002610000005e00fffffffb0000000800540069006d006500000002b4000002eb000002eb00fffffffb0000000a0049006d0061006700650100000267000002800000005e00fffffffb0000000a0049006d00610067006501000004ed000002640000005e00fffffffb0000000a0049006d00610067006501000004ef000002620000000000000000fb0000000800540069006d00650100000000000004500000000000000000000005aa000001bf00000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 191 | Selection: 192 | collapsed: false 193 | Time: 194 | collapsed: false 195 | Tool Properties: 196 | collapsed: false 197 | Views: 198 | collapsed: true 199 | Width: 1873 200 | X: 1199 201 | Y: 694 202 | -------------------------------------------------------------------------------- /ltremovert/launch/run_ltmapper.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ltremovert/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | removert 4 | 1.0.0 5 | Remove-then-revert-based static point cloud map construction 6 | 7 | 8 | Giseop Kim 9 | 10 | TODO 11 | 12 | 13 | Giseop Kim 14 | 15 | catkin 16 | 17 | roscpp 18 | roscpp 19 | rospy 20 | rospy 21 | 22 | tf 23 | tf 24 | 25 | cv_bridge 26 | cv_bridge 27 | 28 | pcl_conversions 29 | pcl_conversions 30 | 31 | std_msgs 32 | std_msgs 33 | sensor_msgs 34 | sensor_msgs 35 | geometry_msgs 36 | geometry_msgs 37 | nav_msgs 38 | nav_msgs 39 | 40 | message_generation 41 | message_generation 42 | message_runtime 43 | message_runtime 44 | 45 | image_transport 46 | image_transport 47 | 48 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ltremovert/src/RosParamServer.cpp: -------------------------------------------------------------------------------- 1 | #include "removert/RosParamServer.h" 2 | 3 | 4 | RosParamServer::RosParamServer() 5 | : nh(nh_super) 6 | { 7 | nh.param("removert/isScanFileKITTIFormat", isScanFileKITTIFormat_, true); 8 | 9 | nh.param("removert/rimg_color_min", rimg_color_min_, 0.0); 10 | nh.param("removert/rimg_color_max", rimg_color_max_, 10.0); 11 | kRangeColorAxis = std::pair {rimg_color_min_, rimg_color_max_}; // meter 12 | kRangeColorAxisForDiff = std::pair{0.0, 0.5}; // meter 13 | 14 | // fov 15 | nh.param("removert/sequence_vfov", kVFOV, 50.0); 16 | nh.param("removert/sequence_hfov", kHFOV, 360.0); 17 | kFOV = std::pair(kVFOV, kHFOV); 18 | 19 | // resolution 20 | nh.param>("removert/remove_resolution_list", remove_resolution_list_, std::vector()); 21 | nh.param>("removert/revert_resolution_list", revert_resolution_list_, std::vector()); 22 | 23 | // knn 24 | nh.param("removert/num_nn_points_within", kNumKnnPointsToCompare, 3); // using higher, more strict static 25 | nh.param("removert/dist_nn_points_within", kScanKnnAndMapKnnAvgDiffThreshold, 0.1); // using smaller, more strict static 26 | 27 | // sequcne system info 28 | nh.param>("removert/ExtrinsicLiDARtoPoseBase", kVecExtrinsicLiDARtoPoseBase, std::vector()); 29 | kSE3MatExtrinsicLiDARtoPoseBase = Eigen::Map>(kVecExtrinsicLiDARtoPoseBase.data(), 4, 4); 30 | kSE3MatExtrinsicPoseBasetoLiDAR = kSE3MatExtrinsicLiDARtoPoseBase.inverse(); 31 | 32 | // config: point cloud pre-processing 33 | nh.param("removert/downsample_voxel_size", kDownsampleVoxelSize, 0.05); 34 | 35 | // sessions' paths 36 | nh.param("removert/central_sess_scan_dir", central_sess_scan_dir_, "/use/your/directory/having/*.bin"); 37 | nh.param("removert/central_sess_pose_path", central_sess_pose_path_, "/use/your/path/having/pose.txt"); 38 | nh.param("removert/query_sess_scan_dir", query_sess_scan_dir_, "/use/your/directory/having/*.bin"); 39 | nh.param("removert/query_sess_pose_path", query_sess_pose_path_, "/use/your/path/having/pose.txt"); 40 | 41 | 42 | // target scan index range (used in Removert.cpp) 43 | nh.param("removert/start_idx", start_idx_, 1); 44 | nh.param("removert/end_idx", end_idx_, 100); 45 | 46 | nh.param("removert/use_keyframe_gap", use_keyframe_gap_, true); 47 | nh.param("removert/use_keyframe_meter", use_keyframe_meter_, false); 48 | nh.param("removert/keyframe_gap", keyframe_gap_, 10); 49 | nh.param("removert/keyframe_meter", keyframe_gap_meter_, 2.0); 50 | 51 | nh.param("removert/repeat_removert_iter", repeat_removert_iter_, 1); 52 | 53 | // faster 54 | nh.param("removert/num_omp_cores", kNumOmpCores, 4); 55 | 56 | // save info 57 | nh.param("removert/saveMapPCD", kFlagSaveMapPointcloud, false); 58 | nh.param("removert/saveCleanScansPCD", kFlagSaveCleanScans, false); 59 | nh.param("removert/save_pcd_directory", save_pcd_directory_, "/"); 60 | 61 | 62 | usleep(100); 63 | } // ctor RosParamServer 64 | 65 | -------------------------------------------------------------------------------- /ltremovert/src/Session.cpp: -------------------------------------------------------------------------------- 1 | #include "removert/Session.h" 2 | 3 | using std::cout; 4 | using std::endl; 5 | 6 | namespace ltremovert 7 | { 8 | 9 | Session::Session( void ) 10 | { 11 | allocateMemory(); 12 | } 13 | 14 | 15 | void Session::allocateMemory(void) 16 | { 17 | // 18 | kdtree_map_global_curr_.reset(new pcl::KdTreeFLANN()); 19 | kdtree_target_map_global_.reset(new pcl::KdTreeFLANN()); 20 | kdtree_scan_global_curr_.reset(new pcl::KdTreeFLANN()); 21 | 22 | kdtree_map_global_nd_.reset(new pcl::KdTreeFLANN()); 23 | kdtree_map_global_pd_.reset(new pcl::KdTreeFLANN()); 24 | 25 | map_global_orig_.reset(new pcl::PointCloud()); 26 | map_global_curr_.reset(new pcl::PointCloud()); 27 | map_local_curr_.reset(new pcl::PointCloud()); 28 | 29 | map_global_updated_.reset(new pcl::PointCloud()); 30 | map_global_updated_strong_.reset(new pcl::PointCloud()); 31 | 32 | map_global_curr_static_.reset(new pcl::PointCloud()); 33 | map_global_curr_dynamic_.reset(new pcl::PointCloud()); 34 | map_global_curr_down_for_icp_.reset(new pcl::PointCloud()); 35 | map_global_curr_diff_positive_.reset(new pcl::PointCloud()); 36 | map_global_curr_diff_negative_.reset(new pcl::PointCloud()); 37 | 38 | map_global_nd_.reset(new pcl::PointCloud()); 39 | map_local_nd_.reset(new pcl::PointCloud()); 40 | map_global_nd_strong_.reset(new pcl::PointCloud()); 41 | map_global_nd_weak_.reset(new pcl::PointCloud()); 42 | 43 | map_global_pd_.reset(new pcl::PointCloud()); 44 | map_global_pd_orig_.reset(new pcl::PointCloud()); 45 | map_local_pd_.reset(new pcl::PointCloud()); 46 | map_global_pd_strong_.reset(new pcl::PointCloud()); 47 | map_global_pd_weak_.reset(new pcl::PointCloud()); 48 | 49 | // 50 | target_map_down_for_knn_.reset(new pcl::PointCloud()); 51 | 52 | map_global_orig_->clear(); 53 | map_global_curr_->clear(); 54 | map_local_curr_->clear(); 55 | map_global_curr_static_->clear(); 56 | map_global_curr_dynamic_->clear(); 57 | 58 | map_global_updated_->clear(); 59 | map_global_updated_strong_->clear(); 60 | 61 | map_global_curr_down_for_icp_->clear(); 62 | map_global_curr_diff_positive_->clear(); 63 | map_global_curr_diff_negative_->clear(); 64 | 65 | map_global_nd_->clear(); 66 | map_local_nd_->clear(); 67 | map_global_nd_strong_->clear(); 68 | map_global_nd_weak_->clear(); 69 | 70 | map_global_pd_->clear(); 71 | map_global_pd_orig_->clear(); 72 | map_local_pd_->clear(); 73 | map_global_pd_strong_->clear(); 74 | map_global_pd_weak_->clear(); 75 | 76 | target_map_down_for_knn_->clear(); 77 | } // allocateMemory 78 | 79 | 80 | void Session::loadSessionInfo( std::string _sess_type, std::string _scan_dir, std::string _pose_path ) 81 | { 82 | sess_type_ = _sess_type; 83 | scan_dir_ = _scan_dir; 84 | pose_path_ = _pose_path; 85 | 86 | // load scan paths 87 | for(auto& _entry : fs::directory_iterator(_scan_dir)) { 88 | scan_names_.emplace_back(_entry.path().filename()); 89 | scan_paths_.emplace_back(_entry.path()); 90 | } 91 | std::sort(scan_names_.begin(), scan_names_.end()); 92 | std::sort(scan_paths_.begin(), scan_paths_.end()); 93 | 94 | num_scans_ = scan_paths_.size(); 95 | ROS_INFO_STREAM("\033[1;32m Total : " << num_scans_ << " scans in the directory.\033[0m"); 96 | 97 | // debug 98 | // for(auto& _elm: scan_paths_) 99 | // cout << _elm << endl; 100 | 101 | // load the trajectory poses 102 | std::ifstream pose_file_handle (_pose_path); 103 | std::string strOneLine; 104 | while (getline(pose_file_handle, strOneLine)) { 105 | std::vector ith_pose_vec = splitPoseLine(strOneLine, ' '); // str to vec 106 | if(ith_pose_vec.size() == 12) { // append a row [0,0,0,1] 107 | ith_pose_vec.emplace_back(double(0.0)); ith_pose_vec.emplace_back(double(0.0)); ith_pose_vec.emplace_back(double(0.0)); ith_pose_vec.emplace_back(double(1.0)); 108 | } 109 | Eigen::Matrix4d ith_pose = Eigen::Map>(ith_pose_vec.data(), 4, 4); 110 | Eigen::Matrix4d ith_pose_inverse = ith_pose.inverse(); 111 | 112 | scan_poses_.emplace_back(ith_pose); 113 | scan_inverse_poses_.emplace_back(ith_pose_inverse); 114 | } 115 | 116 | // check the number of scans and the number of poses are equivalent 117 | assert(scan_paths_.size() == scan_poses_.size()); 118 | } // loadSessionInfo 119 | 120 | 121 | void Session::setDownsampleSize( float _voxel_size ) 122 | { 123 | kDownsampleVoxelSize = _voxel_size; 124 | } // setDownsampleSize 125 | 126 | 127 | void Session::clearKeyframes( void ) 128 | { 129 | int num_keyframes_ = 0; 130 | 131 | keyframe_names_.clear(); 132 | keyframe_paths_.clear(); 133 | keyframe_poses_.clear(); 134 | keyframe_inverse_poses_.clear(); 135 | } // clearKeyframes 136 | 137 | 138 | void Session::parseKeyframes( std::pair _range, int _gap ) 139 | { 140 | clearKeyframes(); 141 | 142 | auto start_idx = _range.first; 143 | auto end_idx = _range.second; 144 | 145 | int num_valid_parsed {0}; 146 | for(int curr_idx=0; curr_idx < int(scan_paths_.size()); curr_idx++) 147 | { 148 | // check the scan idx within the target idx range 149 | if(curr_idx > end_idx || curr_idx < start_idx) { 150 | curr_idx++; 151 | continue; 152 | } 153 | 154 | if( remainder(num_valid_parsed, _gap) != 0 ) { 155 | num_valid_parsed++; 156 | continue; 157 | } 158 | 159 | // save the info (reading scan bin is in makeGlobalMap) 160 | keyframe_paths_.emplace_back(scan_paths_.at(curr_idx)); 161 | keyframe_names_.emplace_back(scan_names_.at(curr_idx)); 162 | 163 | keyframe_poses_.emplace_back(scan_poses_.at(curr_idx)); // used for local2global 164 | keyframe_inverse_poses_.emplace_back(scan_inverse_poses_.at(curr_idx)); // used for global2local 165 | 166 | // 167 | num_valid_parsed++; 168 | } 169 | 170 | ROS_INFO_STREAM("\033[1;32m Total " << keyframe_paths_.size() 171 | << " nodes are used from the index range [" << start_idx << ", " << end_idx << "]" 172 | << " (every " << _gap << " frames parsed)\033[0m"); 173 | 174 | } // parseKeyframes 175 | 176 | 177 | void Session::parseKeyframes( int _gap ) 178 | { 179 | clearKeyframes(); 180 | 181 | std::pair _range { 0, int(scan_paths_.size()) }; 182 | parseKeyframes(_range, _gap); 183 | } // parseKeyframes for all range 184 | 185 | 186 | void Session::mergeScansWithinGlobalCoord(void) 187 | { 188 | // NOTE: _scans must be in local coord 189 | for (std::size_t scan_idx = 0; scan_idx < keyframe_scans_.size(); scan_idx++) 190 | { 191 | auto ii_scan = keyframe_scans_.at(scan_idx); // pcl::PointCloud::Ptr 192 | auto ii_pose = keyframe_poses_.at(scan_idx); // Eigen::Matrix4d 193 | 194 | // local to global (local2global) 195 | pcl::PointCloud::Ptr scan_global_coord(new pcl::PointCloud()); 196 | pcl::transformPointCloud(*ii_scan, *scan_global_coord, kSE3MatExtrinsicLiDARtoPoseBase); 197 | pcl::transformPointCloud(*scan_global_coord, *scan_global_coord, ii_pose); 198 | 199 | // merge the scan into the global map 200 | *map_global_orig_ += *scan_global_coord; 201 | } 202 | } // mergeScansWithinGlobalCoord 203 | 204 | 205 | double xyzDist (const Eigen::Matrix4d& _pose1, const Eigen::Matrix4d& _pose2) 206 | { 207 | // cout << _pose1(0,0) << ", " << _pose1(0,1) << ", " << _pose1(0,2) << ", " << _pose1(0,3) << endl; 208 | // cout << _pose1(1,0) << ", " << _pose1(1,1) << ", " << _pose1(1,2) << ", " << _pose1(1,3) << endl; 209 | // cout << _pose1(2,0) << ", " << _pose1(2,1) << ", " << _pose1(2,2) << ", " << _pose1(2,3) << endl; 210 | // cout << _pose1(3,0) << ", " << _pose1(3,1) << ", " << _pose1(3,2) << ", " << _pose1(3,3) << endl; 211 | 212 | return std::sqrt( (_pose1(0,3)-_pose2(0,3))*(_pose1(0,3)-_pose2(0,3)) 213 | + (_pose1(1,3)-_pose2(1,3))*(_pose1(1,3)-_pose2(1,3)) 214 | + (_pose1(2,3)-_pose2(2,3))*(_pose1(2,3)-_pose2(2,3)) ); 215 | } // xyzDist 216 | 217 | double nnDist( const Eigen::Matrix4d& _pose, const std::vector& _roi_poses) 218 | { 219 | double nn_dist = 10000000000.0; 220 | for (auto _roi_pose: _roi_poses) { 221 | double dist = xyzDist(_pose, _roi_pose); 222 | if( dist < nn_dist ) 223 | nn_dist = dist; 224 | } 225 | 226 | return nn_dist; 227 | } // nnDist 228 | 229 | 230 | void Session::parseKeyframesInROI( const std::vector& _roi_poses, int _gap ) 231 | { 232 | clearKeyframes(); 233 | 234 | double inplace_thres = 10.0; // e.g., 10, 30 meter, user param 235 | 236 | int num_valid_parsed {0}; 237 | for(int curr_idx=0; curr_idx < int(scan_paths_.size()); curr_idx++) 238 | { 239 | auto curr_pose = scan_poses_.at(curr_idx); 240 | double dist = nnDist(curr_pose, _roi_poses); 241 | if(dist > inplace_thres) 242 | continue; 243 | 244 | if( remainder(num_valid_parsed, _gap) != 0 ) { 245 | num_valid_parsed++; 246 | continue; 247 | } 248 | 249 | // save the info (reading scan bin is in makeGlobalMap) 250 | keyframe_paths_.emplace_back(scan_paths_.at(curr_idx)); 251 | keyframe_names_.emplace_back(scan_names_.at(curr_idx)); 252 | 253 | keyframe_poses_.emplace_back(scan_poses_.at(curr_idx)); // used for local2global 254 | keyframe_inverse_poses_.emplace_back(scan_inverse_poses_.at(curr_idx)); // used for global2local 255 | 256 | // 257 | num_valid_parsed++; 258 | } 259 | 260 | ROS_INFO_STREAM("\033[1;32m Total " << keyframe_paths_.size() 261 | << " keyframes parsed in the map's ROI\033[0m"); 262 | 263 | } // parseKeyframesInROI 264 | 265 | 266 | void Session::loadKeyframes( void ) 267 | { 268 | const int cout_interval {10}; 269 | int cout_counter {0}; 270 | cout << endl; 271 | cout << " ... (display every " << cout_interval << " readings) ..." << endl; 272 | for(auto& _scan_path : keyframe_paths_) { 273 | // read bin files and save 274 | pcl::PointCloud::Ptr points (new pcl::PointCloud); // pcl::PointCloud Ptr is a shared ptr so this points will be automatically destroyed after this function block (because no others ref it). 275 | pcl::io::loadPCDFile (_scan_path, *points); // saved from SC-LIO-SAM's pcd binary (.pcd) 276 | // Deprecated: PLEASE use the binary pcd format scans saved by using SC-LIO-SAM's save utility 277 | // if( isScanFileKITTIFormat_ ) { 278 | // readBin(_scan_path, points); // For KITTI (.bin) 279 | // } else { 280 | // pcl::io::loadPCDFile (_scan_path, *points); // saved from SC-LIO-SAM's pcd binary (.pcd) 281 | // } 282 | 283 | // pcdown 284 | pcl::VoxelGrid downsize_filter; 285 | downsize_filter.setLeafSize(kDownsampleVoxelSize, kDownsampleVoxelSize, kDownsampleVoxelSize); 286 | downsize_filter.setInputCloud(points); 287 | 288 | pcl::PointCloud::Ptr downsampled_points (new pcl::PointCloud); 289 | downsize_filter.filter(*downsampled_points); 290 | 291 | // save downsampled pointcloud 292 | keyframe_scans_.emplace_back(downsampled_points); 293 | 294 | // cout for debug 295 | cout_counter++; 296 | if (remainder(cout_counter, cout_interval) == 0) { 297 | cout << _scan_path << endl; 298 | cout << "Read a pointcloud with " << points->points.size() << " points (" 299 | << "downsampled size: " << downsampled_points->points.size() << " points)" << endl; 300 | } 301 | } 302 | } // loadKeyframes 303 | 304 | 305 | void Session::parseStaticScansViaProjection(void) 306 | { 307 | parseScansViaProjection(map_global_curr_, keyframe_scans_static_projected_); 308 | 309 | // keyframe_scans_static_projected_.clear(); 310 | // for (std::size_t idx_scan = 0; idx_scan < keyframe_scans_.size(); idx_scan++) { 311 | // Eigen::Matrix4d base_pose_inverse = keyframe_inverse_poses_.at(idx_scan); 312 | // transformGlobalMapToLocal(map_global_curr_, base_pose_inverse, kSE3MatExtrinsicPoseBasetoLiDAR, map_local_curr_); 313 | // auto this_scan_static = parseProjectedPoints(map_local_curr_, kFOV, resetRimgSize(kFOV, kReprojectionAlpha)); // the most time comsuming part 2 -> so openMP applied inside 314 | // keyframe_scans_static_projected_.emplace_back(this_scan_static); 315 | // } 316 | } // parseStaticScansViaProjection 317 | 318 | void Session::parseUpdatedStaticScansViaProjection(void) 319 | { 320 | parseScansViaProjection(map_global_updated_, keyframe_scans_updated_); 321 | } 322 | 323 | void Session::parseUpdatedStrongStaticScansViaProjection(void) 324 | { 325 | parseScansViaProjection(map_global_updated_strong_, keyframe_scans_updated_strong_); 326 | } 327 | 328 | void Session::parsePDScansViaProjection(void) 329 | { 330 | parseScansViaProjection(map_global_pd_orig_, keyframe_scans_pd_); 331 | } 332 | 333 | void Session::parseStrongPDScansViaProjection(void) 334 | { 335 | parseScansViaProjection(map_global_pd_strong_, keyframe_scans_strong_pd_); // ? 336 | } 337 | 338 | void Session::parseWeakNDScansViaProjection(void) 339 | { 340 | parseScansViaProjection(map_global_nd_weak_, keyframe_scans_weak_nd_); 341 | } 342 | 343 | void Session::parseStrongNDScansViaProjection(void) 344 | { 345 | parseScansViaProjection(map_global_nd_strong_, keyframe_scans_strong_nd_); 346 | } 347 | 348 | void Session::parseScansViaProjection( 349 | pcl::PointCloud::Ptr _map, // to_be_parsed 350 | std::vector::Ptr>& _vec_to_store ) 351 | { 352 | _vec_to_store.clear(); 353 | pcl::PointCloud::Ptr map_local(new pcl::PointCloud()); 354 | for (std::size_t idx_scan = 0; idx_scan < keyframe_scans_.size(); idx_scan++) { 355 | Eigen::Matrix4d base_pose_inverse = keyframe_inverse_poses_.at(idx_scan); 356 | transformGlobalMapToLocal(_map, base_pose_inverse, kSE3MatExtrinsicPoseBasetoLiDAR, map_local); 357 | auto this_scan_static = parseProjectedPoints(map_local, kFOV, resetRimgSize(kFOV, kReprojectionAlpha)); // the most time comsuming part 2 -> so openMP applied inside 358 | _vec_to_store.emplace_back(this_scan_static); 359 | } 360 | } // parseStaticScansViaProjection 361 | 362 | void Session::updateScansScanwise() 363 | { 364 | for(std::size_t ii = 0; ii < keyframe_scans_updated_.size(); ii++) 365 | { 366 | // init 367 | pcl::PointCloud::Ptr final_updated_scan(new pcl::PointCloud()); 368 | *final_updated_scan = *keyframe_scans_updated_.at(ii); 369 | 370 | // append 371 | *final_updated_scan += *keyframe_scans_weak_nd_.at(ii); 372 | *final_updated_scan += *keyframe_scans_pd_.at(ii); 373 | 374 | // downsampling (to remove repeated points) 375 | octreeDownsampling(final_updated_scan, final_updated_scan, 0.05); 376 | 377 | // renewal 378 | *keyframe_scans_updated_[ii] = *final_updated_scan; 379 | } 380 | } // updateScansScanwise 381 | 382 | // void Session::parseDynamicScansViaProjection(void) 383 | // { 384 | // scans_dynamic_projected_.clear(); 385 | // for (std::size_t idx_scan = 0; idx_scan < keyframe_scans_.size(); idx_scan++) { 386 | // Eigen::Matrix4d base_pose_inverse = keyframe_inverse_poses_.at(idx_scan); 387 | // transformGlobalMapToLocal(map_global_curr_, base_pose_inverse, kSE3MatExtrinsicPoseBasetoLiDAR, map_local_curr_); 388 | // auto this_scan_static = parseProjectedPoints(map_local_curr_, kFOV, resetRimgSize(kFOV, kReprojectionAlpha)); // the most time comsuming part 2 -> so openMP applied inside 389 | // scans_dynamic_projected_.emplace_back(this_scan_static); 390 | // } 391 | // } // parseDynamicScansViaProjection 392 | 393 | void Session::extractLowDynPointsViaKnnDiff(const pcl::PointCloud::Ptr _target_map) 394 | { 395 | float icpVoxelSize = 0.4; // meter 396 | icp_for_knn_.setMaxCorrespondenceDistance(150); // giseop , use a value can cover 2*historyKeyframeSearchNum range in meter 397 | icp_for_knn_.setMaximumIterations(100); 398 | icp_for_knn_.setTransformationEpsilon(1e-6); 399 | icp_for_knn_.setEuclideanFitnessEpsilon(1e-6); 400 | icp_for_knn_.setRANSACIterations(0); 401 | octreeDownsampling(_target_map, target_map_down_for_knn_, icpVoxelSize); 402 | icp_for_knn_.setInputTarget(target_map_down_for_knn_); 403 | 404 | kdtree_target_map_global_->setInputCloud(_target_map); 405 | 406 | std::map::Ptr> scans_knn_coexist_map; 407 | std::map::Ptr> scans_knn_diff_map; 408 | #pragma omp parallel for num_threads(kNumOmpCores) // if using icp refinement (in current version, not recommended), do not use the this omp 409 | for (std::size_t idx_scan = 0; idx_scan < keyframe_scans_static_projected_.size(); idx_scan++) { 410 | auto [this_scan_coexist, this_scan_diff] = partitionLowDynamicPointsOfScanByKnn(idx_scan); 411 | mtx.lock(); 412 | scans_knn_coexist_map[int(idx_scan)] = this_scan_coexist; 413 | scans_knn_diff_map[int(idx_scan)] = this_scan_diff; 414 | mtx.unlock(); 415 | } 416 | 417 | scans_knn_coexist_.clear(); 418 | scans_knn_diff_.clear(); 419 | scans_knn_coexist_.reserve(keyframe_scans_static_projected_.size()); 420 | scans_knn_diff_.reserve(keyframe_scans_static_projected_.size()); 421 | 422 | for(auto& _elm: scans_knn_coexist_map) 423 | scans_knn_coexist_.emplace_back(_elm.second); 424 | 425 | for(auto& _elm: scans_knn_diff_map) 426 | scans_knn_diff_.emplace_back(_elm.second); 427 | } 428 | 429 | 430 | void Session::constructGlobalNDMap() 431 | { 432 | auto map_global_nd = mergeScansWithinGlobalCoordUtil(scans_knn_diff_, keyframe_poses_, kSE3MatExtrinsicLiDARtoPoseBase); 433 | *map_global_nd_ = *map_global_nd; 434 | octreeDownsampling(map_global_nd_, map_global_nd_, 0.05); 435 | } 436 | 437 | void Session::constructGlobalPDMap() 438 | { 439 | auto map_global_pd = mergeScansWithinGlobalCoordUtil(scans_knn_diff_, keyframe_poses_, kSE3MatExtrinsicLiDARtoPoseBase); 440 | *map_global_pd_ = *map_global_pd; 441 | octreeDownsampling(map_global_pd_, map_global_pd_, 0.05); 442 | 443 | // backup (for save) 444 | *map_global_pd_orig_ = *map_global_pd_; 445 | } 446 | 447 | void Session::revertStrongPDMapPointsHavingWeakPDInNear() 448 | { 449 | // TODO 450 | } 451 | 452 | void Session::removeWeakNDMapPointsHavingStrongNDInNear() 453 | { 454 | if( map_global_nd_strong_->points.size() == 0 ) 455 | return; 456 | 457 | kdtree_map_global_nd_->setInputCloud(map_global_nd_strong_); 458 | 459 | int num_points_of_weak_nd = map_global_nd_weak_->points.size(); 460 | pcl::PointCloud::Ptr points_added_to_strong(new pcl::PointCloud); 461 | pcl::PointCloud::Ptr points_to_be_new_weak(new pcl::PointCloud); 462 | for (std::size_t pt_idx = 0; pt_idx < num_points_of_weak_nd; pt_idx++) 463 | { 464 | std::vector topk_indexes_map; 465 | std::vector topk_L2dists_map; 466 | 467 | // hard coding for fast test 468 | int kNumKnnPointsToCompare = 2; 469 | float kScanKnnAndMapKnnAvgDiffThreshold = 1.0; // meter, should be larger than the knn diff's threshold 470 | 471 | kdtree_map_global_nd_->nearestKSearch(map_global_nd_weak_->points[pt_idx], kNumKnnPointsToCompare, topk_indexes_map, topk_L2dists_map); 472 | float sum_topknn_dists_in_map = accumulate(topk_L2dists_map.begin(), topk_L2dists_map.end(), 0.0); 473 | float avg_topknn_dists_in_map = sum_topknn_dists_in_map / float(kNumKnnPointsToCompare); 474 | 475 | if (std::abs(avg_topknn_dists_in_map) < kScanKnnAndMapKnnAvgDiffThreshold) // if this weak point have strong friend 476 | points_added_to_strong->push_back(map_global_nd_weak_->points[pt_idx]); 477 | else 478 | points_to_be_new_weak->push_back(map_global_nd_weak_->points[pt_idx]); 479 | } 480 | 481 | // update 482 | *map_global_nd_strong_ += *points_added_to_strong; // note that not = but += (append) 483 | *map_global_nd_weak_ = *points_to_be_new_weak; // note that not += but = (remake) 484 | } 485 | 486 | 487 | void Session::extractHighDynPointsViaKnnDiff(const pcl::PointCloud::Ptr _target_map) 488 | { 489 | kdtree_target_map_global_->setInputCloud(_target_map); 490 | std::map::Ptr> keyframe_scans_dynamic_map; 491 | #pragma omp parallel for num_threads(kNumOmpCores) 492 | for (std::size_t idx_scan = 0; idx_scan < keyframe_scans_.size(); idx_scan++) { 493 | auto [unused, this_scan_diff] = partitionHighDynamicPointsOfScanByKnn(idx_scan); 494 | mtx.lock(); 495 | keyframe_scans_dynamic_map[int(idx_scan)] = this_scan_diff; 496 | mtx.unlock(); 497 | } 498 | 499 | keyframe_scans_dynamic_.clear(); 500 | keyframe_scans_dynamic_.reserve(keyframe_scans_.size()); 501 | 502 | for(auto& _elm: keyframe_scans_dynamic_map) 503 | keyframe_scans_dynamic_.emplace_back(_elm.second); 504 | } 505 | 506 | void Session::precleaningKeyframes ( float _radius ) 507 | { 508 | for (std::size_t idx_kf = 0; idx_kf < keyframe_scans_.size(); idx_kf++) 509 | { 510 | auto scan = keyframe_scans_.at(idx_kf); // pcl::PointCloud::Ptr 511 | 512 | int num_points = scan->points.size(); 513 | pcl::PointCloud::Ptr scan_cleaned(new pcl::PointCloud()); 514 | for (int pt_idx = 0; pt_idx < num_points; ++pt_idx) 515 | { 516 | 517 | PointType this_point = scan->points[pt_idx]; 518 | SphericalPoint sph_point = cart2sph(this_point); 519 | float this_point_range = sph_point.r; 520 | float this_point_z = this_point.z; 521 | 522 | if( this_point_range < _radius 523 | & this_point_z < 0.5 524 | & -0.5 < this_point_z ) { 525 | continue; 526 | } 527 | 528 | scan_cleaned->push_back(this_point); 529 | } 530 | 531 | *scan = *scan_cleaned; 532 | } 533 | } // precleaningKeyframes 534 | 535 | 536 | // note: using pcPtr = pcl::PointCloud::Ptr; // in utility.h 537 | std::pair Session::partitionLowDynamicPointsOfScanByKnn(int _scan_idx) 538 | { 539 | // curr scan (in local coord) 540 | // pcl::PointCloud::Ptr scan = keyframe_scans_static_projected_.at(_scan_idx); 541 | auto scan = keyframe_scans_static_projected_.at(_scan_idx); 542 | auto scan_pose = keyframe_poses_.at(_scan_idx); 543 | 544 | // curr scan (in global coord) 545 | pcl::PointCloud::Ptr scan_global = local2global(scan, scan_pose, kSE3MatExtrinsicPoseBasetoLiDAR); 546 | // kdtree_scan_global_curr_->setInputCloud(scan_global); 547 | int num_points_of_a_scan = scan_global->points.size(); 548 | 549 | pcl::PointCloud::Ptr scan_eff_for_knn_comp(new pcl::PointCloud()); 550 | pcl::PointCloud::Ptr scan_eff_to_parse_in_cetral_frame(new pcl::PointCloud()); 551 | bool useICPrefinement { false }; // TODO: need to be tested more ... in current version not using it is better. 552 | if(useICPrefinement) { 553 | mtx.lock(); 554 | cout << "Knn diff of " << keyframe_names_.at(_scan_idx) << " of " << sess_type_ << " (with ICP refinement) ..." << endl; 555 | mtx.unlock(); 556 | 557 | pcl::PointCloud::Ptr scan_global_down(new pcl::PointCloud()); 558 | float icpVoxelSize = 0.4; // meter 559 | octreeDownsampling(scan_global, scan_global_down, icpVoxelSize); 560 | icp_for_knn_.setInputSource(scan_global_down); 561 | pcl::PointCloud::Ptr unused_result(new pcl::PointCloud()); 562 | icp_for_knn_.align(*unused_result); 563 | 564 | pcl::PointCloud::Ptr scan_global_reg(new pcl::PointCloud()); 565 | pcl::transformPointCloud(*scan_global, *scan_global_reg, icp_for_knn_.getFinalTransformation()); 566 | 567 | // pcl::io::savePCDFileBinary("/home/user/Desktop/data/rss21-ltmapper/mulran/dcc/01/removert/0102refactor/map.pcd", *target_map_down_for_knn_); 568 | // pcl::io::savePCDFileBinary("/home/user/Desktop/data/rss21-ltmapper/mulran/dcc/01/removert/0102refactor/scan.pcd", *scan_global); 569 | 570 | cout << icp_for_knn_.getFitnessScore() << endl; 571 | if (icp_for_knn_.getFitnessScore() < 1.0) { 572 | *scan_eff_for_knn_comp = *scan_global_reg; 573 | *scan_eff_to_parse_in_cetral_frame = *scan_global_reg; 574 | } else { 575 | *scan_eff_for_knn_comp = *scan_global; 576 | *scan_eff_to_parse_in_cetral_frame = *scan_global; 577 | } 578 | } else { 579 | mtx.lock(); 580 | cout << "Knn diff of " << keyframe_names_.at(_scan_idx) << " of " << sess_type_ << " (without ICP refinement) ..." << endl; 581 | mtx.unlock(); 582 | *scan_eff_for_knn_comp = *scan_global; 583 | *scan_eff_to_parse_in_cetral_frame = *scan_global; 584 | } 585 | 586 | pcl::PointCloud::Ptr scan_knn_coexist(new pcl::PointCloud); 587 | pcl::PointCloud::Ptr scan_knn_diff(new pcl::PointCloud); 588 | for (std::size_t pt_idx = 0; pt_idx < num_points_of_a_scan; pt_idx++) 589 | { 590 | std::vector topk_indexes_map; 591 | std::vector topk_L2dists_map; 592 | kdtree_target_map_global_->nearestKSearch(scan_eff_for_knn_comp->points[pt_idx], kNumKnnPointsToCompare, topk_indexes_map, topk_L2dists_map); 593 | float sum_topknn_dists_in_map = accumulate(topk_L2dists_map.begin(), topk_L2dists_map.end(), 0.0); 594 | float avg_topknn_dists_in_map = sum_topknn_dists_in_map / float(kNumKnnPointsToCompare); 595 | 596 | if (std::abs(avg_topknn_dists_in_map) < kScanKnnAndMapKnnAvgDiffThreshold) 597 | scan_knn_coexist->push_back(scan_eff_to_parse_in_cetral_frame->points[pt_idx]); 598 | else 599 | scan_knn_diff->push_back(scan_eff_to_parse_in_cetral_frame->points[pt_idx]); 600 | } 601 | 602 | // again global2local because later in the merging global map function, which requires scans within each local coord. 603 | pcl::PointCloud::Ptr scan_knn_coexist_local = global2local(scan_knn_coexist, keyframe_inverse_poses_.at(_scan_idx), kSE3MatExtrinsicPoseBasetoLiDAR); 604 | pcl::PointCloud::Ptr scan_knn_diff_local = global2local(scan_knn_diff, keyframe_inverse_poses_.at(_scan_idx), kSE3MatExtrinsicPoseBasetoLiDAR); 605 | 606 | return std::pair::Ptr, pcl::PointCloud::Ptr>(scan_knn_coexist_local, scan_knn_diff_local); 607 | } // partitionLowDynamicPointsOfScanByKnn 608 | 609 | 610 | std::pair Session::partitionHighDynamicPointsOfScanByKnn(int _scan_idx) 611 | { 612 | // curr scan (in local coord) 613 | // pcl::PointCloud::Ptr scan = keyframe_scans_static_projected_.at(_scan_idx); 614 | auto scan = keyframe_scans_.at(_scan_idx); 615 | auto scan_pose = keyframe_poses_.at(_scan_idx); 616 | 617 | // curr scan (in global coord) 618 | pcl::PointCloud::Ptr scan_global = local2global(scan, scan_pose, kSE3MatExtrinsicPoseBasetoLiDAR); 619 | // kdtree_scan_global_curr_->setInputCloud(scan_global); 620 | int num_points_of_a_scan = scan_global->points.size(); 621 | pcl::PointCloud::Ptr scan_knn_coexist(new pcl::PointCloud); 622 | pcl::PointCloud::Ptr scan_knn_diff(new pcl::PointCloud); 623 | for (std::size_t pt_idx = 0; pt_idx < num_points_of_a_scan; pt_idx++) 624 | { 625 | std::vector topk_indexes_map; 626 | std::vector topk_L2dists_map; 627 | kdtree_target_map_global_->nearestKSearch(scan_global->points[pt_idx], kNumKnnPointsToCompare, topk_indexes_map, topk_L2dists_map); 628 | float sum_topknn_dists_in_map = accumulate(topk_L2dists_map.begin(), topk_L2dists_map.end(), 0.0); 629 | float avg_topknn_dists_in_map = sum_topknn_dists_in_map / float(kNumKnnPointsToCompare); 630 | 631 | if (std::abs(avg_topknn_dists_in_map) < kScanKnnAndMapKnnAvgDiffThreshold) 632 | scan_knn_coexist->push_back(scan_global->points[pt_idx]); 633 | else 634 | scan_knn_diff->push_back(scan_global->points[pt_idx]); 635 | } 636 | 637 | // again global2local because later in the merging global map function, which requires scans within each local coord. 638 | pcl::PointCloud::Ptr scan_knn_coexist_local = global2local(scan_knn_coexist, keyframe_inverse_poses_.at(_scan_idx), kSE3MatExtrinsicPoseBasetoLiDAR); 639 | pcl::PointCloud::Ptr scan_knn_diff_local = global2local(scan_knn_diff, keyframe_inverse_poses_.at(_scan_idx), kSE3MatExtrinsicPoseBasetoLiDAR); 640 | 641 | return std::pair(scan_knn_coexist_local, scan_knn_diff_local); 642 | } // partitionHighDynamicPointsOfScanByKnn 643 | 644 | 645 | 646 | 647 | } // namespace ltremovert -------------------------------------------------------------------------------- /ltremovert/src/removert_main.cpp: -------------------------------------------------------------------------------- 1 | #include "removert/Removerter.h" 2 | 3 | int main(int argc, char** argv) 4 | { 5 | ros::init(argc, argv, "removert"); 6 | ROS_INFO("\033[1;32m----> Removert Main Started.\033[0m"); 7 | 8 | ltremovert::Removerter RMV; 9 | RMV.run(); 10 | 11 | ros::spin(); 12 | 13 | return 0; 14 | } -------------------------------------------------------------------------------- /ltremovert/src/utility.cpp: -------------------------------------------------------------------------------- 1 | #include "removert/utility.h" 2 | 3 | bool cout_debug = false; 4 | 5 | 6 | void readBin(std::string _bin_path, pcl::PointCloud::Ptr _pcd_ptr) 7 | { 8 | std::fstream input(_bin_path.c_str(), ios::in | ios::binary); 9 | if(!input.good()){ 10 | cerr << "Could not read file: " << _bin_path << endl; 11 | exit(EXIT_FAILURE); 12 | } 13 | input.seekg(0, ios::beg); 14 | 15 | for (int ii=0; input.good() && !input.eof(); ii++) { 16 | PointType point; 17 | 18 | input.read((char *) &point.x, sizeof(float)); 19 | input.read((char *) &point.y, sizeof(float)); 20 | input.read((char *) &point.z, sizeof(float)); 21 | input.read((char *) &point.intensity, sizeof(float)); 22 | 23 | _pcd_ptr->push_back(point); 24 | } 25 | input.close(); 26 | } 27 | 28 | std::vector splitPoseLine(std::string _str_line, char _delimiter) { 29 | std::vector parsed; 30 | std::stringstream ss(_str_line); 31 | std::string temp; 32 | while (getline(ss, temp, _delimiter)) { 33 | parsed.push_back(std::stod(temp)); // convert string to "double" 34 | } 35 | return parsed; 36 | } 37 | 38 | SphericalPoint cart2sph(const PointType & _cp) 39 | { // _cp means cartesian point 40 | 41 | if(cout_debug){ 42 | cout << "Cartesian Point [x, y, z]: [" << _cp.x << ", " << _cp.y << ", " << _cp.z << endl; 43 | } 44 | 45 | SphericalPoint sph_point { 46 | std::atan2(_cp.y, _cp.x), 47 | std::atan2(_cp.z, std::sqrt(_cp.x*_cp.x + _cp.y*_cp.y)), 48 | std::sqrt(_cp.x*_cp.x + _cp.y*_cp.y + _cp.z*_cp.z) 49 | }; 50 | return sph_point; 51 | } 52 | 53 | float rad2deg(float radians) 54 | { 55 | return radians * 180.0 / M_PI; 56 | } 57 | 58 | float deg2rad(float degrees) 59 | { 60 | return degrees * M_PI / 180.0; 61 | } 62 | 63 | 64 | void transformGlobalMapToLocal( 65 | const pcl::PointCloud::Ptr &_map_global, 66 | Eigen::Matrix4d _base_pose_inverse, Eigen::Matrix4d _base2lidar, pcl::PointCloud::Ptr &_map_local) 67 | { 68 | // global to local (global2local) 69 | _map_local->clear(); 70 | pcl::transformPointCloud(*_map_global, *_map_local, _base_pose_inverse); 71 | pcl::transformPointCloud(*_map_local, *_map_local, _base2lidar); // _base2lidar == kSE3MatExtrinsicPoseBasetoLiDAR 72 | } // transformGlobalMapToLocal 73 | 74 | pcl::PointCloud::Ptr parseProjectedPoints(const pcl::PointCloud::Ptr &_scan, 75 | const std::pair _fov, /* e.g., [vfov = 50 (upper 25, lower 25), hfov = 360] */ 76 | const std::pair _rimg_size) 77 | { 78 | auto [map_rimg, map_rimg_ptidx] = map2RangeImg(_scan, _fov, _rimg_size); // the most time comsuming part 2 -> so openMP applied inside 79 | pcl::PointCloud::Ptr scan_projected(new pcl::PointCloud); 80 | for (int row_idx = 0; row_idx < map_rimg_ptidx.rows; ++row_idx) { 81 | for (int col_idx = 0; col_idx < map_rimg_ptidx.cols; ++col_idx) { 82 | if(map_rimg_ptidx.at(row_idx, col_idx) == 0) // 0 means no point (see map2RangeImg) 83 | continue; 84 | 85 | scan_projected->push_back(_scan->points[map_rimg_ptidx.at(row_idx, col_idx)]); 86 | } 87 | } 88 | return scan_projected; 89 | } // map2RangeImg 90 | 91 | 92 | std::pair map2RangeImg(const pcl::PointCloud::Ptr &_scan, 93 | const std::pair _fov, /* e.g., [vfov = 50 (upper 25, lower 25), hfov = 360] */ 94 | const std::pair _rimg_size) 95 | { 96 | const float kVFOV = _fov.first; 97 | const float kHFOV = _fov.second; 98 | 99 | const int kNumRimgRow = _rimg_size.first; 100 | const int kNumRimgCol = _rimg_size.second; 101 | 102 | // @ range image initizliation 103 | cv::Mat rimg = cv::Mat(kNumRimgRow, kNumRimgCol, CV_32FC1, cv::Scalar::all(kFlagNoPOINT)); // float matrix, save range value 104 | cv::Mat rimg_ptidx = cv::Mat(kNumRimgRow, kNumRimgCol, CV_32SC1, cv::Scalar::all(0)); // int matrix, save point (of global map) index 105 | 106 | // @ points to range img 107 | int num_points = _scan->points.size(); 108 | 109 | const int kNumOmpCores = 16; // hard coding for fast dev 110 | #pragma omp parallel for num_threads(kNumOmpCores) 111 | for (int pt_idx = 0; pt_idx < num_points; ++pt_idx) 112 | { 113 | PointType this_point = _scan->points[pt_idx]; 114 | SphericalPoint sph_point = cart2sph(this_point); 115 | 116 | // @ note about vfov: e.g., (+ V_FOV/2) to adjust [-15, 15] to [0, 30] 117 | // @ min and max is just for the easier (naive) boundary checks. 118 | int lower_bound_row_idx{0}; 119 | int lower_bound_col_idx{0}; 120 | int upper_bound_row_idx{kNumRimgRow - 1}; 121 | int upper_bound_col_idx{kNumRimgCol - 1}; 122 | int pixel_idx_row = int(std::min(std::max(std::round(kNumRimgRow * (1 - (rad2deg(sph_point.el) + (kVFOV / float(2.0))) / (kVFOV - float(0.0)))), float(lower_bound_row_idx)), float(upper_bound_row_idx))); 123 | int pixel_idx_col = int(std::min(std::max(std::round(kNumRimgCol * ((rad2deg(sph_point.az) + (kHFOV / float(2.0))) / (kHFOV - float(0.0)))), float(lower_bound_col_idx)), float(upper_bound_col_idx))); 124 | 125 | float curr_range = sph_point.r; 126 | 127 | // @ Theoretically, this if-block would have race condition (i.e., this is a critical section), 128 | // @ But, the resulting range image is acceptable (watching via Rviz), 129 | // @ so I just naively applied omp pragma for this whole for-block (2020.10.28) 130 | // @ Reason: because this for loop is splited by the omp, points in a single splited for range do not race among them, 131 | // @ also, a point A and B lied in different for-segments do not tend to correspond to the same pixel, 132 | // # so we can assume practically there are few race conditions. 133 | // @ P.S. some explicit mutexing directive makes the code even slower ref: https://stackoverflow.com/questions/2396430/how-to-use-lock-in-openmp 134 | if (curr_range < rimg.at(pixel_idx_row, pixel_idx_col)) 135 | { 136 | rimg.at(pixel_idx_row, pixel_idx_col) = curr_range; 137 | rimg_ptidx.at(pixel_idx_row, pixel_idx_col) = pt_idx; 138 | } 139 | } 140 | 141 | return std::pair(rimg, rimg_ptidx); 142 | } // map2RangeImg 143 | 144 | // void octreeDownsampling(const pcl::PointCloud::Ptr &_src, pcl::PointCloud::Ptr &_to_save) 145 | // { 146 | // pcl::octree::OctreePointCloudVoxelCentroid octree(kDownsampleVoxelSize); 147 | // octree.setInputCloud(_src); 148 | // octree.defineBoundingBox(); 149 | // octree.addPointsFromInputCloud(); 150 | // pcl::octree::OctreePointCloudVoxelCentroid::AlignedPointTVector centroids; 151 | // octree.getVoxelCentroids(centroids); 152 | 153 | // // init current map with the downsampled full cloud 154 | // _to_save->points.assign(centroids.begin(), centroids.end()); 155 | // _to_save->width = 1; 156 | // _to_save->height = _to_save->points.size(); // make sure again the format of the downsampled point cloud 157 | // ROS_INFO_STREAM("\033[1;32m Octree Downsampled pointcloud have: " << _to_save->points.size() << " points.\033[0m"); 158 | // } // octreeDownsampling 159 | 160 | pcl::PointCloud::Ptr local2global(const pcl::PointCloud::Ptr &_scan_local, 161 | const Eigen::Matrix4d &_scan_pose, const Eigen::Matrix4d &_lidar2base) 162 | { 163 | pcl::PointCloud::Ptr scan_global(new pcl::PointCloud()); 164 | pcl::transformPointCloud(*_scan_local, *scan_global, _lidar2base); 165 | pcl::transformPointCloud(*scan_global, *scan_global, _scan_pose); 166 | 167 | return scan_global; 168 | } 169 | 170 | pcl::PointCloud::Ptr mergeScansWithinGlobalCoordUtil( 171 | const std::vector::Ptr> &_scans, 172 | const std::vector &_scans_poses, Eigen::Matrix4d _lidar2base) 173 | { 174 | pcl::PointCloud::Ptr ptcloud_merged(new pcl::PointCloud()); 175 | 176 | // NOTE: _scans must be in local coord 177 | for (std::size_t scan_idx = 0; scan_idx < _scans.size(); scan_idx++) 178 | { 179 | auto ii_scan = _scans.at(scan_idx); // pcl::PointCloud::Ptr 180 | auto ii_pose = _scans_poses.at(scan_idx); // Eigen::Matrix4d 181 | 182 | // local to global (local2global) 183 | pcl::PointCloud::Ptr scan_global_coord(new pcl::PointCloud()); 184 | pcl::transformPointCloud(*ii_scan, *scan_global_coord, _lidar2base); // kSE3MatExtrinsicLiDARtoPoseBase 185 | pcl::transformPointCloud(*scan_global_coord, *scan_global_coord, ii_pose); 186 | 187 | // merge the scan into the global map 188 | *ptcloud_merged += *scan_global_coord; 189 | } 190 | 191 | return ptcloud_merged; 192 | } // mergeScansWithinGlobalCoord 193 | 194 | pcl::PointCloud::Ptr global2local(const pcl::PointCloud::Ptr &_scan_global, 195 | const Eigen::Matrix4d &_scan_pose_inverse, const Eigen::Matrix4d &_base2lidar) 196 | { 197 | pcl::PointCloud::Ptr scan_local(new pcl::PointCloud()); 198 | pcl::transformPointCloud(*_scan_global, *scan_local, _scan_pose_inverse); 199 | pcl::transformPointCloud(*scan_local, *scan_local, _base2lidar); // kSE3MatExtrinsicPoseBasetoLiDAR 200 | 201 | return scan_local; 202 | } 203 | 204 | void octreeDownsampling(const pcl::PointCloud::Ptr &_src, 205 | pcl::PointCloud::Ptr &_to_save, 206 | const float _kDownsampleVoxelSize) 207 | { 208 | pcl::octree::OctreePointCloudVoxelCentroid octree(_kDownsampleVoxelSize); 209 | octree.setInputCloud(_src); 210 | octree.defineBoundingBox(); 211 | octree.addPointsFromInputCloud(); 212 | pcl::octree::OctreePointCloudVoxelCentroid::AlignedPointTVector centroids; 213 | octree.getVoxelCentroids(centroids); 214 | 215 | // init current map with the downsampled full cloud 216 | _to_save->points.assign(centroids.begin(), centroids.end()); 217 | _to_save->width = 1; 218 | _to_save->height = _to_save->points.size(); // make sure again the format of the downsampled point cloud 219 | } // octreeDownsampling 220 | 221 | 222 | std::pair resetRimgSize(const std::pair _fov, const float _resize_ratio) 223 | { 224 | // default is 1 deg x 1 deg 225 | float alpha_vfov = _resize_ratio; 226 | float alpha_hfov = _resize_ratio; 227 | 228 | float V_FOV = _fov.first; 229 | float H_FOV = _fov.second; 230 | 231 | int NUM_RANGE_IMG_ROW = std::round(V_FOV*alpha_vfov); 232 | int NUM_RANGE_IMG_COL = std::round(H_FOV*alpha_hfov); 233 | 234 | std::pair rimg {NUM_RANGE_IMG_ROW, NUM_RANGE_IMG_COL}; 235 | return rimg; 236 | } 237 | 238 | 239 | std::set convertIntVecToSet(const std::vector & v) 240 | { 241 | std::set s; 242 | for (int x : v) { 243 | s.insert(x); 244 | } 245 | return s; 246 | } 247 | 248 | void pubRangeImg(cv::Mat& _rimg, 249 | sensor_msgs::ImagePtr& _msg, 250 | image_transport::Publisher& _publiser, 251 | std::pair _caxis) 252 | { 253 | cv::Mat scan_rimg_viz = convertColorMappedImg(_rimg, _caxis); 254 | _msg = cvmat2msg(scan_rimg_viz); 255 | _publiser.publish(_msg); 256 | } // pubRangeImg 257 | 258 | 259 | sensor_msgs::ImagePtr cvmat2msg(const cv::Mat &_img) 260 | { 261 | sensor_msgs::ImagePtr msg = cv_bridge::CvImage(std_msgs::Header(), "bgr8", _img).toImageMsg(); 262 | return msg; 263 | } 264 | 265 | 266 | void publishPointcloud2FromPCLptr(const ros::Publisher& _scan_publisher, const pcl::PointCloud::Ptr _scan) 267 | { 268 | sensor_msgs::PointCloud2 tempCloud; 269 | pcl::toROSMsg(*_scan, tempCloud); 270 | tempCloud.header.stamp = ros::Time::now(); 271 | tempCloud.header.frame_id = "removert"; 272 | _scan_publisher.publish(tempCloud); 273 | } // publishPointcloud2FromPCLptr 274 | 275 | 276 | sensor_msgs::PointCloud2 publishCloud(ros::Publisher *thisPub, pcl::PointCloud::Ptr thisCloud, ros::Time thisStamp, std::string thisFrame) 277 | { 278 | sensor_msgs::PointCloud2 tempCloud; 279 | pcl::toROSMsg(*thisCloud, tempCloud); 280 | tempCloud.header.stamp = thisStamp; 281 | tempCloud.header.frame_id = thisFrame; 282 | if (thisPub->getNumSubscribers() != 0) 283 | thisPub->publish(tempCloud); 284 | return tempCloud; 285 | } 286 | 287 | float pointDistance(PointType p) 288 | { 289 | return sqrt(p.x*p.x + p.y*p.y + p.z*p.z); 290 | } 291 | 292 | float pointDistance(PointType p1, PointType p2) 293 | { 294 | return sqrt((p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y) + (p1.z-p2.z)*(p1.z-p2.z)); 295 | } 296 | 297 | -------------------------------------------------------------------------------- /ltslam/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.3) 2 | project(ltslam) 3 | 4 | set(CMAKE_BUILD_TYPE "Release") 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Wall -g -pthread") 7 | 8 | find_package(catkin REQUIRED COMPONENTS 9 | tf 10 | roscpp 11 | rospy 12 | cv_bridge 13 | # pcl library 14 | pcl_conversions 15 | # msgs 16 | std_msgs 17 | sensor_msgs 18 | geometry_msgs 19 | nav_msgs 20 | message_generation 21 | ) 22 | 23 | find_package(OpenMP REQUIRED) 24 | find_package(PCL REQUIRED QUIET) 25 | find_package(OpenCV REQUIRED QUIET) 26 | find_package(GTSAM REQUIRED QUIET) 27 | 28 | catkin_package( 29 | INCLUDE_DIRS include 30 | DEPENDS PCL GTSAM 31 | 32 | CATKIN_DEPENDS 33 | std_msgs 34 | nav_msgs 35 | geometry_msgs 36 | sensor_msgs 37 | message_runtime 38 | message_generation 39 | ) 40 | 41 | # include directories 42 | include_directories( 43 | include 44 | ${catkin_INCLUDE_DIRS} 45 | ${PCL_INCLUDE_DIRS} 46 | ${OpenCV_INCLUDE_DIRS} 47 | ${GTSAM_INCLUDE_DIR} 48 | ) 49 | 50 | # link directories 51 | link_directories( 52 | include 53 | ${PCL_LIBRARY_DIRS} 54 | ${OpenCV_LIBRARY_DIRS} 55 | ${GTSAM_LIBRARY_DIRS} 56 | ) 57 | 58 | ########### 59 | ## Build ## 60 | ########### 61 | 62 | #ltslam 63 | add_executable(${PROJECT_NAME}_ltslam src/main.cpp 64 | src/utility.cpp 65 | src/RosParamServer.cpp 66 | src/Session.cpp 67 | src/LTslam.cpp 68 | src/Scancontext.cpp 69 | ) 70 | 71 | add_dependencies(${PROJECT_NAME}_ltslam ${catkin_EXPORTED_TARGETS}) 72 | target_compile_options(${PROJECT_NAME}_ltslam PRIVATE ${OpenMP_CXX_FLAGS}) 73 | target_link_libraries(${PROJECT_NAME}_ltslam 74 | ${catkin_LIBRARIES} 75 | ${PCL_LIBRARIES} 76 | ${OpenCV_LIBRARIES} 77 | ${OpenMP_CXX_FLAGS} 78 | gtsam 79 | stdc++fs 80 | ) 81 | 82 | 83 | -------------------------------------------------------------------------------- /ltslam/README.md: -------------------------------------------------------------------------------- 1 | # LT-SLAM 2 | 3 | ## What is LT-SLAM? 4 | - Multiple Pose-graph Anchoring 5 | - Custom GTSAM factor for anchoring (see BetweenFactorWithAnchoring.h) 6 | 7 | -------------------------------------------------------------------------------- /ltslam/config/params.yaml: -------------------------------------------------------------------------------- 1 | ltslam: 2 | 3 | sessions_dir: "/home/user/Documents/catkin2021/catkin_LTslam/data_kitti/in/" # end with / 4 | 5 | central_sess_name: "011" # should be same as the directory name of that session 6 | query_sess_name: "02" # should be same as the directory name of that session 7 | 8 | save_directory: "/home/user/Documents/catkin2021/catkin_LTslam/data_kitti/out/011-02-manyloops/" # end with "/" 9 | 10 | is_display_debug_msgs: false 11 | 12 | loopFitnessScoreThreshold: 0.7 13 | 14 | kNumSCLoopsUpperBound: 1000 # if you use an enough number, use all the detected loops 15 | kNumRSLoopsUpperBound: 0 # if you use an enough number, use all the detected loops 16 | 17 | numberOfCores: 16 # currently hard-coded in the utility.cpp 18 | 19 | -------------------------------------------------------------------------------- /ltslam/include/ltslam/BetweenFactorWithAnchoring.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace gtsam { 12 | 13 | /** 14 | * A class for a measurement predicted by "between(config[key1],config[key2])" 15 | * @tparam VALUE the Value type 16 | * @addtogroup SLAM 17 | */ 18 | template 19 | class BetweenFactorWithAnchoring: public NoiseModelFactor4 { 20 | 21 | // Check that VALUE type is a testable Lie group 22 | BOOST_CONCEPT_ASSERT((IsTestable)); 23 | BOOST_CONCEPT_ASSERT((IsLieGroup)); 24 | 25 | public: 26 | 27 | typedef VALUE T; 28 | 29 | private: 30 | 31 | typedef BetweenFactorWithAnchoring This; 32 | typedef NoiseModelFactor4 Base; 33 | 34 | VALUE measured_; /** The measurement */ 35 | 36 | public: 37 | 38 | // shorthand for a smart pointer to a factor 39 | typedef typename boost::shared_ptr shared_ptr; 40 | 41 | /** default constructor - only use for serialization */ 42 | BetweenFactorWithAnchoring() {} 43 | 44 | /** Constructor */ 45 | BetweenFactorWithAnchoring( 46 | // 1 for robot 1, and 2 for robot 2 47 | Key key1, Key key2, Key anchor_key1, Key anchor_key2, 48 | const VALUE& measured, 49 | const SharedNoiseModel& model = nullptr) : 50 | Base(model, key1, key2, anchor_key1, anchor_key2), measured_(measured) { 51 | } 52 | 53 | virtual ~BetweenFactorWithAnchoring() {} 54 | 55 | /// @return a deep copy of this factor 56 | virtual gtsam::NonlinearFactor::shared_ptr clone() const { 57 | return boost::static_pointer_cast( 58 | gtsam::NonlinearFactor::shared_ptr(new This(*this))); } 59 | 60 | /** implement functions needed for Testable */ 61 | 62 | /** print */ 63 | virtual void print(const std::string& s, const KeyFormatter& keyFormatter = DefaultKeyFormatter) const { 64 | std::cout << s << "BetweenFactorWithAnchoring(" 65 | << keyFormatter(this->key1()) << "," 66 | << keyFormatter(this->key2()) << ")\n"; 67 | traits::Print(measured_, " measured: "); 68 | this->noiseModel_->print(" noise model: "); 69 | } 70 | 71 | /** equals */ 72 | virtual bool equals(const NonlinearFactor& expected, double tol=1e-9) const { 73 | const This *e = dynamic_cast (&expected); 74 | return e != nullptr && Base::equals(*e, tol) && traits::Equals(this->measured_, e->measured_, tol); 75 | } 76 | 77 | /** implement functions needed to derive from Factor */ 78 | 79 | // some useful link, giseop 80 | // line 384, https://gtsam.org/doxygen/a00317_source.html 81 | // https://gtsam.org/doxygen/a02091.html 82 | // betweenfactor https://gtsam.org/doxygen/a00935_source.html 83 | // line 224 https://gtsam.org/doxygen/a00053_source.html 84 | // isam ver. line 233, https://people.csail.mit.edu/kaess/isam/doc/slam2d_8h_source.html 85 | /** vector of errors */ 86 | Vector evaluateError( 87 | const T& p1, const T& p2, const T& anchor_p1, const T& anchor_p2, 88 | boost::optional H1 = boost::none, 89 | boost::optional H2 = boost::none, 90 | boost::optional anchor_H1 = boost::none, 91 | boost::optional anchor_H2 = boost::none 92 | ) const { 93 | 94 | // anchor node h(.) (ref: isam ver. line 233, https://people.csail.mit.edu/kaess/isam/doc/slam2d_8h_source.html) 95 | T hx1 = traits::Compose(anchor_p1, p1, anchor_H1, H1); // for the updated jacobian, see line 60, 219, https://gtsam.org/doxygen/a00053_source.html 96 | T hx2 = traits::Compose(anchor_p2, p2, anchor_H2, H2); 97 | T hx = traits::Between(hx1, hx2, H1, H2); 98 | 99 | return traits::Local(measured_, hx); 100 | } 101 | 102 | /** return the measured */ 103 | const VALUE& measured() const { 104 | return measured_; 105 | } 106 | 107 | /** number of variables attached to this factor */ 108 | std::size_t size() const { 109 | return 4; 110 | } 111 | 112 | private: 113 | 114 | /** Serialization function */ 115 | friend class boost::serialization::access; 116 | template 117 | void serialize(ARCHIVE & ar, const unsigned int /*version*/) { 118 | ar & boost::serialization::make_nvp("NoiseModelFactor4", 119 | boost::serialization::base_object(*this)); 120 | ar & BOOST_SERIALIZATION_NVP(measured_); 121 | } 122 | 123 | // // Alignment, see https://eigen.tuxfamily.org/dox/group__TopicStructHavingEigenMembers.html 124 | // enum { NeedsToAlign = (sizeof(VALUE) % 16) == 0 }; 125 | // public: 126 | // EIGEN_MAKE_ALIGNED_OPERATOR_NEW_IF(NeedsToAlign) 127 | }; // \class BetweenFactorWithAnchoring 128 | 129 | 130 | // traits 131 | template 132 | struct traits > : public Testable > {}; 133 | 134 | // /** 135 | // * Binary between constraint - forces between to a given value 136 | // * This constraint requires the underlying type to a Lie type 137 | // * 138 | // */ 139 | // template 140 | // class BetweenConstraintGiseop : public BetweenFactorWithAnchoring { 141 | // public: 142 | // typedef boost::shared_ptr > shared_ptr; 143 | 144 | // /** Syntactic sugar for constrained version */ 145 | // BetweenConstraintGiseop(const VALUE& measured, Key key1, Key key2, double mu = 1000.0) : 146 | // BetweenFactorWithAnchoring(key1, key2, measured, 147 | // noiseModel::Constrained::All(traits::GetDimension(measured), std::abs(mu))) 148 | // {} 149 | 150 | // private: 151 | 152 | // /** Serialization function */ 153 | // friend class boost::serialization::access; 154 | // template 155 | // void serialize(ARCHIVE & ar, const unsigned int /*version*/) { 156 | // ar & boost::serialization::make_nvp("BetweenFactorWithAnchoring", 157 | // boost::serialization::base_object >(*this)); 158 | // } 159 | // }; // \class BetweenConstraintGiseop 160 | 161 | // /// traits 162 | // template 163 | // struct traits > : public Testable > {}; 164 | 165 | } /// namespace gtsam -------------------------------------------------------------------------------- /ltslam/include/ltslam/KDTreeVectorOfVectorsAdaptor.h: -------------------------------------------------------------------------------- 1 | /*********************************************************************** 2 | * Software License Agreement (BSD License) 3 | * 4 | * Copyright 2011-16 Jose Luis Blanco (joseluisblancoc@gmail.com). 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 2. Redistributions in binary form must reproduce the above copyright 14 | * notice, this list of conditions and the following disclaimer in the 15 | * documentation and/or other materials provided with the distribution. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 18 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 22 | * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | *************************************************************************/ 28 | 29 | #pragma once 30 | 31 | #include "ltslam/nanoflann.hpp" 32 | 33 | #include 34 | 35 | // ===== This example shows how to use nanoflann with these types of containers: ======= 36 | //typedef std::vector > my_vector_of_vectors_t; 37 | //typedef std::vector my_vector_of_vectors_t; // This requires #include 38 | // ===================================================================================== 39 | 40 | 41 | /** A simple vector-of-vectors adaptor for nanoflann, without duplicating the storage. 42 | * The i'th vector represents a point in the state space. 43 | * 44 | * \tparam DIM If set to >0, it specifies a compile-time fixed dimensionality for the points in the data set, allowing more compiler optimizations. 45 | * \tparam num_t The type of the point coordinates (typically, double or float). 46 | * \tparam Distance The distance metric to use: nanoflann::metric_L1, nanoflann::metric_L2, nanoflann::metric_L2_Simple, etc. 47 | * \tparam IndexType The type for indices in the KD-tree index (typically, size_t of int) 48 | */ 49 | template 50 | struct KDTreeVectorOfVectorsAdaptor 51 | { 52 | typedef KDTreeVectorOfVectorsAdaptor self_t; 53 | typedef typename Distance::template traits::distance_t metric_t; 54 | typedef nanoflann::KDTreeSingleIndexAdaptor< metric_t,self_t,DIM,IndexType> index_t; 55 | 56 | index_t* index; //! The kd-tree index for the user to call its methods as usual with any other FLANN index. 57 | 58 | /// Constructor: takes a const ref to the vector of vectors object with the data points 59 | KDTreeVectorOfVectorsAdaptor(const size_t /* dimensionality */, const VectorOfVectorsType &mat, const int leaf_max_size = 10) : m_data(mat) 60 | { 61 | assert(mat.size() != 0 && mat[0].size() != 0); 62 | const size_t dims = mat[0].size(); 63 | if (DIM>0 && static_cast(dims) != DIM) 64 | throw std::runtime_error("Data set dimensionality does not match the 'DIM' template argument"); 65 | index = new index_t( static_cast(dims), *this /* adaptor */, nanoflann::KDTreeSingleIndexAdaptorParams(leaf_max_size ) ); 66 | index->buildIndex(); 67 | } 68 | 69 | ~KDTreeVectorOfVectorsAdaptor() { 70 | delete index; 71 | } 72 | 73 | const VectorOfVectorsType &m_data; 74 | 75 | /** Query for the \a num_closest closest points to a given point (entered as query_point[0:dim-1]). 76 | * Note that this is a short-cut method for index->findNeighbors(). 77 | * The user can also call index->... methods as desired. 78 | * \note nChecks_IGNORED is ignored but kept for compatibility with the original FLANN interface. 79 | */ 80 | inline void query(const num_t *query_point, const size_t num_closest, IndexType *out_indices, num_t *out_distances_sq, const int nChecks_IGNORED = 10) const 81 | { 82 | nanoflann::KNNResultSet resultSet(num_closest); 83 | resultSet.init(out_indices, out_distances_sq); 84 | index->findNeighbors(resultSet, query_point, nanoflann::SearchParams()); 85 | } 86 | 87 | /** @name Interface expected by KDTreeSingleIndexAdaptor 88 | * @{ */ 89 | 90 | const self_t & derived() const { 91 | return *this; 92 | } 93 | self_t & derived() { 94 | return *this; 95 | } 96 | 97 | // Must return the number of data points 98 | inline size_t kdtree_get_point_count() const { 99 | return m_data.size(); 100 | } 101 | 102 | // Returns the dim'th component of the idx'th point in the class: 103 | inline num_t kdtree_get_pt(const size_t idx, const size_t dim) const { 104 | return m_data[idx][dim]; 105 | } 106 | 107 | // Optional bounding-box computation: return false to default to a standard bbox computation loop. 108 | // Return true if the BBOX was already computed by the class and returned in "bb" so it can be avoided to redo it again. 109 | // Look at bb.size() to find out the expected dimensionality (e.g. 2 or 3 for point clouds) 110 | template 111 | bool kdtree_get_bbox(BBOX & /*bb*/) const { 112 | return false; 113 | } 114 | 115 | /** @} */ 116 | 117 | }; // end of KDTreeVectorOfVectorsAdaptor 118 | -------------------------------------------------------------------------------- /ltslam/include/ltslam/LTslam.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | #include 19 | 20 | #include "ltslam/RosParamServer.h" 21 | #include "ltslam/BetweenFactorWithAnchoring.h" 22 | #include "ltslam/Session.h" 23 | 24 | using namespace gtsam; 25 | using namespace LTslamParam; 26 | 27 | class LTslam : public RosParamServer 28 | { 29 | public: 30 | // Sessions 31 | static inline int num_sessions = 1; // session index should start from 1 32 | 33 | // const static inline int kSessionStartIdxOffset = 1000000; // int max 2147483647 so ok. 34 | 35 | Sessions sessions_; 36 | SessionsDict sessions_dict_; 37 | 38 | // Pose graph 39 | 40 | const int target_sess_idx = 1; // means the centralt session. recommend to use 1 for it (because for the stable indexing for anchor node index) 41 | const int source_sess_idx = 2; // TODO: generalize this function, change this sess_idx pair for arbitary pair (e.g., [2,7], [1,5]) 42 | 43 | std::vector> SCLoopIdxPairs_; 44 | std::vector> RSLoopIdxPairs_; 45 | 46 | NonlinearFactorGraph gtSAMgraph; 47 | Values initialEstimate; 48 | Values optimizedEstimate; 49 | ISAM2 *isam; 50 | Values isamCurrentEstimate; 51 | Eigen::MatrixXd poseCovariance; 52 | 53 | const gtsam::Pose3 poseOrigin; 54 | 55 | noiseModel::Diagonal::shared_ptr priorNoise; 56 | noiseModel::Diagonal::shared_ptr odomNoise; 57 | noiseModel::Diagonal::shared_ptr loopNoise; 58 | noiseModel::Diagonal::shared_ptr largeNoise; 59 | noiseModel::Base::shared_ptr robustNoise; 60 | 61 | std::mutex mtx; 62 | const int numberOfCores = 8; 63 | 64 | 65 | public: 66 | LTslam(); 67 | ~LTslam(); 68 | 69 | void run( void ); 70 | 71 | void initNoiseConstants(); 72 | void initOptimizer(); 73 | 74 | void loadAllSessions(); 75 | 76 | friend int genGlobalNodeIdx(const int&, const int&); 77 | friend int genAnchorNodeIdx(const int&); 78 | 79 | void addAllSessionsToGraph(); 80 | void initTrajectoryByAnchoring(const Session& _sess); 81 | void addSessionToCentralGraph(const Session& _sess); 82 | 83 | void detectInterSessionSCloops(); 84 | void detectInterSessionRSloops(); 85 | void addSCloops(); 86 | std::vector> equisampleElements(const std::vector>& _input_pair, float _gap, int _num); 87 | 88 | double calcInformationGainBtnTwoNodes(const int loop_idx_target_session, const int loop_idx_source_session); 89 | 90 | void findNearestRSLoopsTargetNodeIdx(); 91 | bool addRSloops(); 92 | 93 | void addFactorsWrtInfoGain(); 94 | 95 | void optimizeMultisesseionGraph(bool _toOpt); 96 | void updateSessionsPoses(); 97 | 98 | std::optional doICPVirtualRelative(Session& target_sess, Session& source_sess, 99 | const int& loop_idx_target_session, const int& loop_idx_source_session); 100 | std::optional doICPGlobalRelative(Session& target_sess, Session& source_sess, 101 | const int& loop_idx_target_session, const int& loop_idx_source_session); 102 | 103 | // saver 104 | gtsam::Pose3 getPoseOfIsamUsingKey(Key _key); 105 | void writeAllSessionsTrajectories(std::string _postfix); 106 | 107 | }; // LTslam -------------------------------------------------------------------------------- /ltslam/include/ltslam/RosParamServer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ltslam/utility.h" 4 | 5 | class RosNodeHandle 6 | { 7 | public: 8 | ros::NodeHandle nh_super; 9 | }; // class: RosNodeHandle 10 | 11 | 12 | class RosParamServer: public RosNodeHandle 13 | { 14 | public: 15 | ros::NodeHandle & nh; 16 | 17 | std::string sessions_dir_; 18 | std::string central_sess_name_; 19 | std::string query_sess_name_; 20 | 21 | std::string save_directory_; 22 | 23 | bool is_display_debug_msgs_; 24 | 25 | int kNumSCLoopsUpperBound; 26 | int kNumRSLoopsUpperBound; 27 | int numberOfCores; 28 | 29 | float loopFitnessScoreThreshold; 30 | 31 | public: 32 | RosParamServer(); 33 | 34 | }; // class: RosParamServer 35 | -------------------------------------------------------------------------------- /ltslam/include/ltslam/Scancontext.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include "ltslam/nanoflann.hpp" 26 | #include "ltslam/KDTreeVectorOfVectorsAdaptor.h" 27 | 28 | #include "ltslam/tictoc.h" 29 | 30 | using namespace Eigen; 31 | using namespace nanoflann; 32 | 33 | using std::cout; 34 | using std::endl; 35 | using std::make_pair; 36 | 37 | using std::atan2; 38 | using std::cos; 39 | using std::sin; 40 | 41 | using SCPointType = pcl::PointXYZI; // using xyz only. but a user can exchange the original bin encoding function (i.e., max hegiht) to max intensity (for detail, refer 20 ICRA Intensity Scan Context) 42 | using KeyMat = std::vector >; 43 | using InvKeyTree = KDTreeVectorOfVectorsAdaptor< KeyMat, float >; 44 | 45 | 46 | // namespace SC2 47 | // { 48 | 49 | void coreImportTest ( void ); 50 | 51 | 52 | // sc param-independent helper functions 53 | float xy2theta( const float & _x, const float & _y ); 54 | MatrixXd circshift( MatrixXd &_mat, int _num_shift ); 55 | std::vector eig2stdvec( MatrixXd _eigmat ); 56 | 57 | 58 | class SCManager 59 | { 60 | public: 61 | SCManager( ) = default; // reserving data space (of std::vector) could be considered. but the descriptor is lightweight so don't care. 62 | 63 | Eigen::MatrixXd makeScancontext( pcl::PointCloud & _scan_down ); 64 | Eigen::MatrixXd makeRingkeyFromScancontext( Eigen::MatrixXd &_desc ); 65 | Eigen::MatrixXd makeSectorkeyFromScancontext( Eigen::MatrixXd &_desc ); 66 | 67 | int fastAlignUsingVkey ( MatrixXd & _vkey1, MatrixXd & _vkey2 ); 68 | double distDirectSC ( MatrixXd &_sc1, MatrixXd &_sc2 ); // "d" (eq 5) in the original paper (IROS 18) 69 | std::pair distanceBtnScanContext ( MatrixXd &_sc1, MatrixXd &_sc2 ); // "D" (eq 6) in the original paper (IROS 18) 70 | 71 | // User-side API 72 | void makeAndSaveScancontextAndKeys( pcl::PointCloud & _scan_down ); 73 | std::pair detectLoopClosureID( void ); // int: nearest node index, float: relative yaw 74 | 75 | // for ltslam 76 | // User-side API for multi-session 77 | void saveScancontextAndKeys( Eigen::MatrixXd _scd ); 78 | std::pair detectLoopClosureIDBetweenSession ( std::vector& curr_key, Eigen::MatrixXd& curr_desc); 79 | 80 | const Eigen::MatrixXd& getConstRefRecentSCD(void); 81 | 82 | public: 83 | // hyper parameters () 84 | const double LIDAR_HEIGHT = 2.0; // lidar height : add this for simply directly using lidar scan in the lidar local coord (not robot base coord) / if you use robot-coord-transformed lidar scans, just set this as 0. 85 | 86 | const int PC_NUM_RING = 20; // 20 in the original paper (IROS 18) 87 | const int PC_NUM_SECTOR = 60; // 60 in the original paper (IROS 18) 88 | const double PC_MAX_RADIUS = 80.0; // 80 meter max in the original paper (IROS 18) 89 | const double PC_UNIT_SECTORANGLE = 360.0 / double(PC_NUM_SECTOR); 90 | const double PC_UNIT_RINGGAP = PC_MAX_RADIUS / double(PC_NUM_RING); 91 | 92 | // tree 93 | const int NUM_EXCLUDE_RECENT = 30; // simply just keyframe gap (related with loopClosureFrequency in yaml), but node position distance-based exclusion is ok. 94 | const int NUM_CANDIDATES_FROM_TREE = 3; // 10 is enough. (refer the IROS 18 paper) 95 | 96 | // loop thres 97 | const double SEARCH_RATIO = 0.1; // for fast comparison, no Brute-force, but search 10 % is okay. // not was in the original conf paper, but improved ver. 98 | // const double SC_DIST_THRES = 0.13; // empirically 0.1-0.2 is fine (rare false-alarms) for 20x60 polar context (but for 0.15 <, DCS or ICP fit score check (e.g., in LeGO-LOAM) should be required for robustness) 99 | const double SC_DIST_THRES = 0.3; // 0.4-0.6 is good choice for using with robust kernel (e.g., Cauchy, DCS) + icp fitness threshold / if not, recommend 0.1-0.15 100 | // const double SC_DIST_THRES = 0.7; // 0.4-0.6 is good choice for using with robust kernel (e.g., Cauchy, DCS) + icp fitness threshold / if not, recommend 0.1-0.15 101 | 102 | // config 103 | const int TREE_MAKING_PERIOD_ = 10; // i.e., remaking tree frequency, to avoid non-mandatory every remaking, to save time cost / in the LeGO-LOAM integration, it is synchronized with the loop detection callback (which is 1Hz) so it means the tree is updated evrey 10 sec. But you can use the smaller value because it is enough fast ~ 5-50ms wrt N. 104 | int tree_making_period_conter = 0; 105 | 106 | // data 107 | std::vector polarcontexts_timestamp_; // optional. 108 | std::vector polarcontexts_; 109 | std::vector polarcontext_invkeys_; 110 | std::vector polarcontext_vkeys_; 111 | 112 | KeyMat polarcontext_invkeys_mat_; 113 | KeyMat polarcontext_invkeys_to_search_; 114 | std::unique_ptr polarcontext_tree_; 115 | 116 | bool is_tree_batch_made = false; 117 | std::unique_ptr polarcontext_tree_batch_; 118 | 119 | }; // SCManager 120 | 121 | // } // namespace SC2 122 | -------------------------------------------------------------------------------- /ltslam/include/ltslam/Session.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ltslam/utility.h" 4 | #include "ltslam/Scancontext.h" 5 | 6 | using namespace LTslamParam; 7 | 8 | 9 | struct Edge { 10 | int from_idx; 11 | int to_idx; 12 | gtsam::Pose3 relative; 13 | }; 14 | 15 | struct Node { 16 | int idx; 17 | gtsam::Pose3 initial; 18 | }; 19 | 20 | using SessionNodes = std::multimap; // from_idx, Edge 21 | using SessionEdges = std::multimap; // from_idx, Edge 22 | 23 | 24 | class Session 25 | { 26 | public: 27 | 28 | int index_; 29 | 30 | std::string name_; 31 | std::string session_dir_path_; 32 | 33 | bool is_base_session_; 34 | 35 | SessionNodes nodes_; 36 | SessionEdges edges_; 37 | 38 | int anchor_node_idx_; 39 | 40 | pcl::PointCloud::Ptr cloudKeyPoses6D; // used for parsing submap represented in central coord 41 | pcl::PointCloud::Ptr originPoses6D; 42 | 43 | std::vector::Ptr> cloudKeyFrames; 44 | pcl::VoxelGrid downSizeFilterICP; 45 | 46 | SCManager scManager; 47 | 48 | public: 49 | Session(); 50 | Session(int _idx, std::string _name, std::string _session_dir, bool _is_base_session); 51 | 52 | void loadSessionGraph(); 53 | void loadSessionScanContextDescriptors(); 54 | void loadSessionKeyframePointclouds(); 55 | 56 | void initKeyPoses(void); 57 | void updateKeyPoses(const gtsam::ISAM2 * isam, const gtsam::Pose3& _anchor_transform); 58 | 59 | void loopFindNearKeyframesCentralCoord( 60 | pcl::PointCloud::Ptr& nearKeyframes, 61 | const int& key, const int& searchNum); 62 | void loopFindNearKeyframesLocalCoord( 63 | pcl::PointCloud::Ptr& nearKeyframes, 64 | const int& key, const int& searchNum); 65 | 66 | void allocateMemory(); 67 | 68 | }; // Session 69 | 70 | 71 | // using Sessions = std::vector; 72 | using Sessions = std::map; 73 | using SessionsDict = std::map; // session name, session information 74 | 75 | -------------------------------------------------------------------------------- /ltslam/include/ltslam/tictoc.h: -------------------------------------------------------------------------------- 1 | // Author: Tong Qin qintonguav@gmail.com 2 | // Shaozu Cao saozu.cao@connect.ust.hk 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | class TicToc 13 | { 14 | public: 15 | TicToc() 16 | { 17 | tic(); 18 | } 19 | 20 | TicToc( bool _disp ) 21 | { 22 | disp_ = _disp; 23 | tic(); 24 | } 25 | 26 | void tic() 27 | { 28 | start = std::chrono::system_clock::now(); 29 | } 30 | 31 | void toc( std::string _about_task ) 32 | { 33 | end = std::chrono::system_clock::now(); 34 | std::chrono::duration elapsed_seconds = end - start; 35 | double elapsed_ms = elapsed_seconds.count() * 1000; 36 | 37 | if( disp_ ) 38 | { 39 | std::cout.precision(3); // 10 for sec, 3 for ms 40 | std::cout << _about_task << ": " << elapsed_ms << " msec." << std::endl; 41 | } 42 | } 43 | 44 | private: 45 | std::chrono::time_point start, end; 46 | bool disp_ = false; 47 | }; 48 | -------------------------------------------------------------------------------- /ltslam/include/ltslam/utility.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | 40 | #include 41 | #include 42 | #include 43 | #include 44 | 45 | #include 46 | #include 47 | 48 | #include 49 | #include 50 | #include 51 | 52 | #include 53 | #include 54 | #include 55 | #include 56 | 57 | #include 58 | #include 59 | 60 | #include 61 | #include 62 | 63 | #include 64 | 65 | #include 66 | #include 67 | #include 68 | #include 69 | #include 70 | #include 71 | #include 72 | #include 73 | #include 74 | #include 75 | #include 76 | #include 77 | #include 78 | #include 79 | #include 80 | #include 81 | #include 82 | #include 83 | #include 84 | #include 85 | #include 86 | #include 87 | 88 | #include // requires gcc version >= 8 89 | 90 | namespace fs = std::filesystem; 91 | using std::ios; 92 | using std::cout; 93 | using std::cerr; 94 | using std::endl; 95 | 96 | // struct PointXYZIS 97 | // { 98 | // PCL_ADD_POINT4D 99 | // float intensity; 100 | // float score; 101 | // EIGEN_MAKE_ALIGNED_OPERATOR_NEW 102 | // } EIGEN_ALIGN16; 103 | 104 | // POINT_CLOUD_REGISTER_POINT_STRUCT (PointXYZIS, 105 | // (float, x, x) (float, y, y) (float, z, z) (float, intensity, intensity) (float, score, score) 106 | // ) 107 | 108 | /* 109 | * A point cloud type that has 6D pose info ([x,y,z,roll,pitch,yaw] intensity is time stamp) 110 | */ 111 | struct PointXYZIRPYT 112 | { 113 | PCL_ADD_POINT4D 114 | PCL_ADD_INTENSITY; // preferred way of adding a XYZ+padding 115 | float roll; 116 | float pitch; 117 | float yaw; 118 | double time; 119 | EIGEN_MAKE_ALIGNED_OPERATOR_NEW // make sure our new allocators are aligned 120 | } EIGEN_ALIGN16; // enforce SSE padding for correct memory alignment 121 | 122 | POINT_CLOUD_REGISTER_POINT_STRUCT (PointXYZIRPYT, 123 | (float, x, x) (float, y, y) 124 | (float, z, z) (float, intensity, intensity) 125 | (float, roll, roll) (float, pitch, pitch) (float, yaw, yaw) 126 | (double, time, time)) 127 | using PointTypePose = PointXYZIRPYT; 128 | 129 | using PointType = pcl::PointXYZI; 130 | 131 | struct G2oLineInfo 132 | { 133 | std::string type; 134 | 135 | int prev_idx = -1; // for vertex, this member is null 136 | int curr_idx; 137 | 138 | std::vector trans; 139 | std::vector quat; 140 | 141 | inline static const std::string kVertexTypeName = "VERTEX_SE3:QUAT"; 142 | inline static const std::string kEdgeTypeName = "EDGE_SE3:QUAT"; 143 | }; // G2oLine 144 | 145 | 146 | struct SphericalPoint 147 | { 148 | float az; // azimuth 149 | float el; // elevation 150 | float r; // radius 151 | }; 152 | 153 | std::vector> sortVecWithIdx(const std::vector& arr); 154 | 155 | namespace LTslamParam { 156 | 157 | const static inline int kSessionStartIdxOffset = 1000000; // int max 2147483647 so ok. 158 | 159 | int genGlobalNodeIdx (const int& _session_idx, const int& _node_offset); 160 | int genAnchorNodeIdx (const int& _session_idx); 161 | } // namespace LTslamParam 162 | 163 | pcl::PointCloud::Ptr transformPointCloud(pcl::PointCloud::Ptr cloudIn, PointTypePose* transformIn); 164 | 165 | float rad2deg(float radians); 166 | float deg2rad(float degrees); 167 | void fsmkdir(std::string _path); 168 | 169 | void readBin(std::string _bin_path, pcl::PointCloud::Ptr _pcd_ptr); 170 | 171 | sensor_msgs::PointCloud2 publishCloud(ros::Publisher *thisPub, pcl::PointCloud::Ptr thisCloud, ros::Time thisStamp, std::string thisFrame); 172 | 173 | std::vector splitPoseLine(std::string _str_line, char _delimiter); 174 | 175 | G2oLineInfo splitG2oFileLine(std::string _str_line); 176 | 177 | SphericalPoint cart2sph(const PointType & _cp); 178 | 179 | std::pair resetRimgSize(const std::pair _fov, const float _resize_ratio); 180 | 181 | template 182 | cv::Mat convertColorMappedImg (const cv::Mat &_src, std::pair _caxis) 183 | { 184 | T min_color_val = _caxis.first; 185 | T max_color_val = _caxis.second; 186 | 187 | cv::Mat image_dst; 188 | image_dst = 255 * (_src - min_color_val) / (max_color_val - min_color_val); 189 | image_dst.convertTo(image_dst, CV_8UC1); 190 | 191 | cv::applyColorMap(image_dst, image_dst, cv::COLORMAP_JET); 192 | 193 | return image_dst; 194 | } 195 | 196 | std::set convertIntVecToSet(const std::vector & v); 197 | 198 | template 199 | std::vector linspace(T a, T b, size_t N) { 200 | T h = (b - a) / static_cast(N-1); 201 | std::vector xs(N); 202 | typename std::vector::iterator x; 203 | T val; 204 | for (x = xs.begin(), val = a; x != xs.end(); ++x, val += h) 205 | *x = val; 206 | return xs; 207 | } 208 | 209 | sensor_msgs::ImagePtr cvmat2msg(const cv::Mat &_img); 210 | 211 | void pubRangeImg(cv::Mat& _rimg, sensor_msgs::ImagePtr& _msg, image_transport::Publisher& _publiser, std::pair _caxis); 212 | void publishPointcloud2FromPCLptr(const ros::Publisher& _scan_publisher, const pcl::PointCloud::Ptr _scan); 213 | sensor_msgs::PointCloud2 publishCloud(ros::Publisher *thisPub, pcl::PointCloud::Ptr thisCloud, ros::Time thisStamp, std::string thisFrame); 214 | 215 | template 216 | double ROS_TIME(T msg) 217 | { 218 | return msg->header.stamp.toSec(); 219 | } 220 | 221 | Eigen::Affine3f pclPointToAffine3f(PointTypePose thisPoint); 222 | gtsam::Pose3 pclPointTogtsamPose3(PointTypePose thisPoint); 223 | 224 | float pointDistance(PointType p); 225 | float pointDistance(PointType p1, PointType p2); 226 | 227 | bool isTwoStringSame(std::string _str1, std::string _str2); 228 | 229 | void collect_digits(std::vector& digits, int num); 230 | 231 | void writePose3ToStream(std::fstream& _stream, gtsam::Pose3 _pose); 232 | 233 | std::vector linspace(int a, int b, int N); 234 | 235 | void saveSCD(std::string fileName, Eigen::MatrixXd matrix, std::string delimiter); 236 | Eigen::MatrixXd readSCD(std::string fileToOpen); 237 | 238 | float poseDistance(const gtsam::Pose3& p1, const gtsam::Pose3& p2); -------------------------------------------------------------------------------- /ltslam/launch/ltmapper_visualization.rviz: -------------------------------------------------------------------------------- 1 | Panels: 2 | - Class: rviz/Displays 3 | Help Height: 78 4 | Name: Displays 5 | Property Tree Widget: 6 | Expanded: 7 | - /Image1 8 | - /Image2 9 | - /Image3 10 | Splitter Ratio: 0.4148681163787842 11 | Tree Height: 572 12 | - Class: rviz/Selection 13 | Name: Selection 14 | - Class: rviz/Tool Properties 15 | Expanded: 16 | - /2D Pose Estimate1 17 | - /2D Nav Goal1 18 | - /Publish Point1 19 | Name: Tool Properties 20 | Splitter Ratio: 0.5886790156364441 21 | - Class: rviz/Views 22 | Expanded: 23 | - /Current View1 24 | Name: Views 25 | Splitter Ratio: 0.5 26 | - Class: rviz/Time 27 | Experimental: false 28 | Name: Time 29 | SyncMode: 0 30 | SyncSource: Image 31 | Preferences: 32 | PromptSaveOnExit: true 33 | Toolbars: 34 | toolButtonStyle: 2 35 | Visualization Manager: 36 | Class: "" 37 | Displays: 38 | - Alpha: 1 39 | Autocompute Intensity Bounds: true 40 | Autocompute Value Bounds: 41 | Max Value: 10 42 | Min Value: -10 43 | Value: true 44 | Axis: Z 45 | Channel Name: intensity 46 | Class: rviz/PointCloud2 47 | Color: 239; 41; 41 48 | Color Transformer: FlatColor 49 | Decay Time: 0 50 | Enabled: true 51 | Invert Rainbow: false 52 | Max Color: 255; 255; 255 53 | Max Intensity: 0.6600000262260437 54 | Min Color: 0; 0; 0 55 | Min Intensity: 0 56 | Name: PointCloud2 57 | Position Transformer: XYZ 58 | Queue Size: 10 59 | Selectable: true 60 | Size (Pixels): 3 61 | Size (m): 0.5 62 | Style: Spheres 63 | Topic: /removert/scan_single_local_dynamic 64 | Unreliable: false 65 | Use Fixed Frame: true 66 | Use rainbow: true 67 | Value: true 68 | - Alpha: 1 69 | Autocompute Intensity Bounds: true 70 | Autocompute Value Bounds: 71 | Max Value: 10 72 | Min Value: -10 73 | Value: true 74 | Axis: Z 75 | Channel Name: intensity 76 | Class: rviz/PointCloud2 77 | Color: 138; 226; 52 78 | Color Transformer: FlatColor 79 | Decay Time: 0 80 | Enabled: true 81 | Invert Rainbow: false 82 | Max Color: 255; 255; 255 83 | Max Intensity: 0.9900000095367432 84 | Min Color: 0; 0; 0 85 | Min Intensity: 0 86 | Name: PointCloud2 87 | Position Transformer: XYZ 88 | Queue Size: 10 89 | Selectable: true 90 | Size (Pixels): 1 91 | Size (m): 0.10000000149011612 92 | Style: Points 93 | Topic: /removert/scan_single_local_static 94 | Unreliable: false 95 | Use Fixed Frame: true 96 | Use rainbow: true 97 | Value: true 98 | - Class: rviz/Image 99 | Enabled: true 100 | Image Topic: /scan_rimg_single 101 | Max Value: 1 102 | Median window: 5 103 | Min Value: 0 104 | Name: Image 105 | Normalize Range: true 106 | Queue Size: 2 107 | Transport Hint: raw 108 | Unreliable: false 109 | Value: true 110 | - Class: rviz/Image 111 | Enabled: true 112 | Image Topic: /map_rimg_single 113 | Max Value: 1 114 | Median window: 5 115 | Min Value: 0 116 | Name: Image 117 | Normalize Range: true 118 | Queue Size: 2 119 | Transport Hint: raw 120 | Unreliable: false 121 | Value: true 122 | - Class: rviz/Image 123 | Enabled: true 124 | Image Topic: /diff_rimg_single 125 | Max Value: 1 126 | Median window: 5 127 | Min Value: 0 128 | Name: Image 129 | Normalize Range: true 130 | Queue Size: 2 131 | Transport Hint: raw 132 | Unreliable: false 133 | Value: true 134 | Enabled: true 135 | Global Options: 136 | Background Color: 0; 0; 0 137 | Default Light: true 138 | Fixed Frame: removert 139 | Frame Rate: 30 140 | Name: root 141 | Tools: 142 | - Class: rviz/Interact 143 | Hide Inactive Objects: true 144 | - Class: rviz/MoveCamera 145 | - Class: rviz/Select 146 | - Class: rviz/FocusCamera 147 | - Class: rviz/Measure 148 | - Class: rviz/SetInitialPose 149 | Theta std deviation: 0.2617993950843811 150 | Topic: /initialpose 151 | X std deviation: 0.5 152 | Y std deviation: 0.5 153 | - Class: rviz/SetGoal 154 | Topic: /move_base_simple/goal 155 | - Class: rviz/PublishPoint 156 | Single click: true 157 | Topic: /clicked_point 158 | Value: true 159 | Views: 160 | Current: 161 | Class: rviz/Orbit 162 | Distance: 101.88595581054688 163 | Enable Stereo Rendering: 164 | Stereo Eye Separation: 0.05999999865889549 165 | Stereo Focal Distance: 1 166 | Swap Stereo Eyes: false 167 | Value: false 168 | Focal Point: 169 | X: -0.6624842882156372 170 | Y: 3.6606950759887695 171 | Z: 0.8095508813858032 172 | Focal Shape Fixed Size: true 173 | Focal Shape Size: 0.05000000074505806 174 | Invert Z Axis: false 175 | Name: Current View 176 | Near Clip Distance: 0.009999999776482582 177 | Pitch: 0.9153987169265747 178 | Target Frame: 179 | Value: Orbit (rviz) 180 | Yaw: 4.498590469360352 181 | Saved: ~ 182 | Window Geometry: 183 | Displays: 184 | collapsed: false 185 | Height: 1016 186 | Hide Left Dock: false 187 | Hide Right Dock: true 188 | Image: 189 | collapsed: false 190 | QMainWindow State: 000000ff00000000fd0000000400000000000001a1000002c7fc0200000008fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003d000002c7000000c900fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261000000010000010f0000035afc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003d0000035a000000a400fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e1000001970000000300000751000000d1fc0100000006fb0000000a0049006d0061006700650100000000000002610000005e00fffffffb0000000800540069006d006500000002b4000002eb000002eb00fffffffb0000000a0049006d0061006700650100000267000002800000005e00fffffffb0000000a0049006d00610067006501000004ed000002640000005e00fffffffb0000000a0049006d00610067006501000004ef000002620000000000000000fb0000000800540069006d00650100000000000004500000000000000000000005aa000002c700000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 191 | Selection: 192 | collapsed: false 193 | Time: 194 | collapsed: false 195 | Tool Properties: 196 | collapsed: false 197 | Views: 198 | collapsed: true 199 | Width: 1873 200 | X: 1967 201 | Y: 27 202 | -------------------------------------------------------------------------------- /ltslam/launch/run.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ltslam/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ltslam 4 | 1.0.0 5 | ltslam 6 | 7 | Giseop Kim 8 | 9 | TODO 10 | 11 | Giseop Kim 12 | 13 | catkin 14 | 15 | roscpp 16 | roscpp 17 | rospy 18 | rospy 19 | 20 | tf 21 | tf 22 | 23 | cv_bridge 24 | cv_bridge 25 | 26 | pcl_conversions 27 | pcl_conversions 28 | 29 | std_msgs 30 | std_msgs 31 | sensor_msgs 32 | sensor_msgs 33 | geometry_msgs 34 | geometry_msgs 35 | nav_msgs 36 | nav_msgs 37 | 38 | message_generation 39 | message_generation 40 | message_runtime 41 | message_runtime 42 | 43 | GTSAM 44 | GTSAM 45 | 46 | 47 | -------------------------------------------------------------------------------- /ltslam/src/LTslam.cpp: -------------------------------------------------------------------------------- 1 | #include "ltslam/LTslam.h" 2 | 3 | 4 | gtsam::Pose3 LTslam::getPoseOfIsamUsingKey (const Key _key) { 5 | const Value& pose_value = isam->calculateEstimate(_key); 6 | auto p_pose_value = dynamic_cast*>(&pose_value); 7 | gtsam::Pose3 pose = gtsam::Pose3{p_pose_value->value()}; 8 | return pose; 9 | } 10 | 11 | void LTslam::writeAllSessionsTrajectories(std::string _postfix = "") 12 | { 13 | // parse 14 | std::map parsed_anchor_transforms; 15 | std::map> parsed_poses; 16 | 17 | isamCurrentEstimate = isam->calculateEstimate(); 18 | for(const auto& key_value: isamCurrentEstimate) { 19 | 20 | int curr_node_idx = int(key_value.key); // typedef std::uint64_t Key 21 | 22 | std::vector parsed_digits; 23 | collect_digits(parsed_digits, curr_node_idx); 24 | int session_idx = parsed_digits.at(0); 25 | int anchor_node_idx = genAnchorNodeIdx(session_idx); 26 | 27 | auto p = dynamic_cast*>(&key_value.value); 28 | if (!p) continue; 29 | gtsam::Pose3 curr_node_pose = gtsam::Pose3{p->value()}; 30 | 31 | if( curr_node_idx == anchor_node_idx ) { 32 | // anchor node 33 | parsed_anchor_transforms[session_idx] = curr_node_pose; 34 | } else { 35 | // general nodes 36 | parsed_poses[session_idx].push_back(curr_node_pose); 37 | } 38 | } 39 | 40 | std::map session_names; 41 | for(auto& _sess_pair: sessions_) 42 | { 43 | auto& _sess = _sess_pair.second; 44 | session_names[_sess.index_] = _sess.name_; 45 | } 46 | 47 | // write 48 | for(auto& _session_info: parsed_poses) { 49 | int session_idx = _session_info.first; 50 | 51 | std::string filename_local = save_directory_ + session_names[session_idx] + "_local_" + _postfix + ".txt"; 52 | std::string filename_central = save_directory_ + session_names[session_idx] + "_central_" + _postfix + ".txt"; 53 | cout << filename_central << endl; 54 | 55 | std::fstream stream_local(filename_local.c_str(), std::fstream::out); 56 | std::fstream stream_central(filename_central.c_str(), std::fstream::out); 57 | 58 | gtsam::Pose3 anchor_transform = parsed_anchor_transforms[session_idx]; 59 | for(auto& _pose: _session_info.second) { 60 | writePose3ToStream(stream_local, _pose); 61 | 62 | gtsam::Pose3 pose_central = anchor_transform * _pose; // se3 compose (oplus) 63 | writePose3ToStream(stream_central, pose_central); 64 | } 65 | } 66 | 67 | } // writeAllSessionsTrajectories 68 | 69 | 70 | LTslam::LTslam() 71 | : poseOrigin(gtsam::Pose3(gtsam::Rot3::RzRyRx(0.0, 0.0, 0.0), gtsam::Point3(0.0, 0.0, 0.0))) 72 | { 73 | } // ctor 74 | 75 | 76 | LTslam::~LTslam() { } 77 | // dtor 78 | 79 | void LTslam::run( void ) 80 | { 81 | initOptimizer(); 82 | initNoiseConstants(); 83 | 84 | loadAllSessions(); 85 | addAllSessionsToGraph(); 86 | 87 | optimizeMultisesseionGraph(true); // optimize the graph with existing edges 88 | writeAllSessionsTrajectories(std::string("bfr_intersession_loops")); 89 | 90 | detectInterSessionSCloops(); // detectInterSessionRSloops was internally done while sc detection 91 | addSCloops(); 92 | optimizeMultisesseionGraph(true); // optimize the graph with existing edges + SC loop edges 93 | 94 | bool toOpt = addRSloops(); // using the optimized estimates (rough alignment using SC) 95 | optimizeMultisesseionGraph(toOpt); // optimize the graph with existing edges + SC loop edges + RS loop edges 96 | 97 | writeAllSessionsTrajectories(std::string("aft_intersession_loops")); 98 | } 99 | 100 | void LTslam::initNoiseConstants() 101 | { 102 | // Variances Vector6 order means 103 | // : rad*rad, rad*rad, rad*rad, meter*meter, meter*meter, meter*meter 104 | { 105 | gtsam::Vector Vector6(6); 106 | Vector6 << 1e-12, 1e-12, 1e-12, 1e-12, 1e-12, 1e-12; 107 | priorNoise = noiseModel::Diagonal::Variances(Vector6); 108 | } 109 | { 110 | gtsam::Vector Vector6(6); 111 | Vector6 << 1e-4, 1e-4, 1e-4, 1e-4, 1e-4, 1e-4; 112 | odomNoise = noiseModel::Diagonal::Variances(Vector6); 113 | } 114 | { 115 | gtsam::Vector Vector6(6); 116 | Vector6 << 1e-4, 1e-4, 1e-4, 1e-3, 1e-3, 1e-3; 117 | loopNoise = noiseModel::Diagonal::Variances(Vector6); 118 | } 119 | { 120 | gtsam::Vector Vector6(6); 121 | Vector6 << M_PI*M_PI, M_PI*M_PI, M_PI*M_PI, 1e8, 1e8, 1e8; 122 | // Vector6 << 1e-4, 1e-4, 1e-4, 1e-3, 1e-3, 1e-3; 123 | largeNoise = noiseModel::Diagonal::Variances(Vector6); 124 | } 125 | 126 | float robustNoiseScore = 0.5; // constant is ok... 127 | gtsam::Vector robustNoiseVector6(6); 128 | robustNoiseVector6 << robustNoiseScore, robustNoiseScore, robustNoiseScore, robustNoiseScore, robustNoiseScore, robustNoiseScore; 129 | robustNoise = gtsam::noiseModel::Robust::Create( 130 | gtsam::noiseModel::mEstimator::Cauchy::Create(1), // optional: replacing Cauchy by DCS or GemanMcClure, but with a good front-end loop detector, Cauchy is empirically enough. 131 | gtsam::noiseModel::Diagonal::Variances(robustNoiseVector6) 132 | ); // - checked it works. but with robust kernel, map modification may be delayed (i.e,. requires more true-positive loop factors) 133 | } 134 | 135 | 136 | void LTslam::initOptimizer() 137 | { 138 | ISAM2Params parameters; 139 | parameters.relinearizeThreshold = 0.1; 140 | parameters.relinearizeSkip = 1; // TODO: study later 141 | isam = new ISAM2(parameters); 142 | } 143 | 144 | 145 | void LTslam::updateSessionsPoses() 146 | { 147 | for(auto& _sess_pair: sessions_) 148 | { 149 | auto& _sess = _sess_pair.second; 150 | gtsam::Pose3 anchor_transform = isamCurrentEstimate.at(genAnchorNodeIdx(_sess.index_)); 151 | // cout << anchor_transform << endl; 152 | _sess.updateKeyPoses(isam, anchor_transform); 153 | } 154 | } // updateSessionsPoses 155 | 156 | 157 | void LTslam::optimizeMultisesseionGraph(bool _toOpt) 158 | { 159 | if(!_toOpt) 160 | return; 161 | 162 | isam->update(gtSAMgraph, initialEstimate); 163 | isam->update(); 164 | isam->update(); 165 | isam->update(); 166 | isam->update(); 167 | isam->update(); 168 | isamCurrentEstimate = isam->calculateEstimate(); // must be followed by update 169 | 170 | gtSAMgraph.resize(0); 171 | initialEstimate.clear(); 172 | 173 | updateSessionsPoses(); 174 | 175 | if(is_display_debug_msgs_) { 176 | std::cout << "**********************************************" << std::endl; 177 | std::cout << "***** variable values after optimization *****" << std::endl; 178 | std::cout << std::endl; 179 | isamCurrentEstimate.print("Current estimate: "); 180 | std::cout << std::endl; 181 | // std::ofstream os("/home/user/Documents/catkin2021/catkin_ltmapper/catkin_ltmapper_dev/src/ltmapper/data/3d/kaist/PoseGraphExample.dot"); 182 | // gtSAMgraph.saveGraph(os, isamCurrentEstimate); 183 | } 184 | } // optimizeMultisesseionGraph 185 | 186 | 187 | std::optional LTslam::doICPVirtualRelative( // for SC loop 188 | Session& target_sess, Session& source_sess, 189 | const int& loop_idx_target_session, const int& loop_idx_source_session) 190 | { 191 | // 20201228: get relative virtual measurements using ICP (refer LIO-SAM's code) 192 | 193 | // parse pointclouds 194 | mtx.lock(); 195 | pcl::PointCloud::Ptr cureKeyframeCloud(new pcl::PointCloud()); 196 | pcl::PointCloud::Ptr targetKeyframeCloud(new pcl::PointCloud()); 197 | 198 | int base_key = 0; // its okay. (using the origin for sc loops' co-base) 199 | int historyKeyframeSearchNum = 25; // TODO move to yaml 200 | 201 | source_sess.loopFindNearKeyframesLocalCoord(cureKeyframeCloud, loop_idx_source_session, 0); 202 | target_sess.loopFindNearKeyframesLocalCoord(targetKeyframeCloud, loop_idx_target_session, historyKeyframeSearchNum); 203 | mtx.unlock(); // unlock after loopFindNearKeyframesWithRespectTo because many new in the loopFindNearKeyframesWithRespectTo 204 | 205 | // ICP Settings 206 | pcl::IterativeClosestPoint icp; 207 | icp.setMaxCorrespondenceDistance(150); // giseop , use a value can cover 2*historyKeyframeSearchNum range in meter 208 | icp.setMaximumIterations(100); 209 | icp.setTransformationEpsilon(1e-6); 210 | icp.setEuclideanFitnessEpsilon(1e-6); 211 | icp.setRANSACIterations(0); 212 | 213 | // Align pointclouds 214 | icp.setInputSource(cureKeyframeCloud); 215 | icp.setInputTarget(targetKeyframeCloud); 216 | pcl::PointCloud::Ptr unused_result(new pcl::PointCloud()); 217 | icp.align(*unused_result); 218 | 219 | // giseop 220 | // TODO icp align with initial 221 | 222 | if (icp.hasConverged() == false || icp.getFitnessScore() > loopFitnessScoreThreshold) { 223 | mtx.lock(); 224 | std::cout << " [SC loop] ICP fitness test failed (" << icp.getFitnessScore() << " > " << loopFitnessScoreThreshold << "). Reject this SC loop." << std::endl; 225 | mtx.unlock(); 226 | return std::nullopt; 227 | } else { 228 | mtx.lock(); 229 | std::cout << " [SC loop] ICP fitness test passed (" << icp.getFitnessScore() << " < " << loopFitnessScoreThreshold << "). Add this SC loop." << std::endl; 230 | mtx.unlock(); 231 | } 232 | 233 | // Get pose transformation 234 | float x, y, z, roll, pitch, yaw; 235 | Eigen::Affine3f correctionLidarFrame; 236 | correctionLidarFrame = icp.getFinalTransformation(); 237 | pcl::getTranslationAndEulerAngles (correctionLidarFrame, x, y, z, roll, pitch, yaw); 238 | gtsam::Pose3 poseFrom = Pose3(Rot3::RzRyRx(roll, pitch, yaw), Point3(x, y, z)); 239 | gtsam::Pose3 poseTo = Pose3(Rot3::RzRyRx(0.0, 0.0, 0.0), Point3(0.0, 0.0, 0.0)); 240 | 241 | return poseFrom.between(poseTo); 242 | } // doICPVirtualRelative 243 | 244 | 245 | std::optional LTslam::doICPGlobalRelative( // For RS loop 246 | Session& target_sess, Session& source_sess, 247 | const int& loop_idx_target_session, const int& loop_idx_source_session) 248 | { 249 | // parse pointclouds 250 | mtx.lock(); 251 | pcl::PointCloud::Ptr cureKeyframeCloud(new pcl::PointCloud()); 252 | pcl::PointCloud::Ptr targetKeyframeCloud(new pcl::PointCloud()); 253 | 254 | int base_key = 0; // its okay. (using the origin for sc loops' co-base) 255 | int historyKeyframeSearchNum = 25; // TODO move to yaml 256 | 257 | source_sess.loopFindNearKeyframesCentralCoord(cureKeyframeCloud, loop_idx_source_session, 0); 258 | target_sess.loopFindNearKeyframesCentralCoord(targetKeyframeCloud, loop_idx_target_session, historyKeyframeSearchNum); 259 | mtx.unlock(); // unlock after loopFindNearKeyframesWithRespectTo because many new in the loopFindNearKeyframesWithRespectTo 260 | 261 | // ICP Settings 262 | pcl::IterativeClosestPoint icp; 263 | icp.setMaxCorrespondenceDistance(150); // giseop , use a value can cover 2*historyKeyframeSearchNum range in meter 264 | icp.setMaximumIterations(100); 265 | icp.setTransformationEpsilon(1e-6); 266 | icp.setEuclideanFitnessEpsilon(1e-6); 267 | icp.setRANSACIterations(0); 268 | 269 | // Align pointclouds 270 | icp.setInputSource(cureKeyframeCloud); 271 | icp.setInputTarget(targetKeyframeCloud); 272 | pcl::PointCloud::Ptr unused_result(new pcl::PointCloud()); 273 | icp.align(*unused_result); 274 | 275 | // giseop 276 | // TODO icp align with initial 277 | 278 | if (icp.hasConverged() == false || icp.getFitnessScore() > loopFitnessScoreThreshold) { 279 | mtx.lock(); 280 | std::cout << " [RS loop] ICP fitness test failed (" << icp.getFitnessScore() << " > " << loopFitnessScoreThreshold << "). Reject this RS loop." << std::endl; 281 | mtx.unlock(); 282 | return std::nullopt; 283 | } else { 284 | mtx.lock(); 285 | std::cout << " [RS loop] ICP fitness test passed (" << icp.getFitnessScore() << " < " << loopFitnessScoreThreshold << "). Add this RS loop." << std::endl; 286 | mtx.unlock(); 287 | } 288 | 289 | // Get pose transformation 290 | float x, y, z, roll, pitch, yaw; 291 | Eigen::Affine3f correctionLidarFrame; 292 | correctionLidarFrame = icp.getFinalTransformation(); 293 | 294 | Eigen::Affine3f tWrong = pclPointToAffine3f(source_sess.cloudKeyPoses6D->points[loop_idx_source_session]); 295 | Eigen::Affine3f tCorrect = correctionLidarFrame * tWrong;// pre-multiplying -> successive rotation about a fixed frame 296 | pcl::getTranslationAndEulerAngles (tCorrect, x, y, z, roll, pitch, yaw); 297 | gtsam::Pose3 poseFrom = Pose3(Rot3::RzRyRx(roll, pitch, yaw), Point3(x, y, z)); 298 | gtsam::Pose3 poseTo = pclPointTogtsamPose3(target_sess.cloudKeyPoses6D->points[loop_idx_target_session]); 299 | 300 | return poseFrom.between(poseTo); 301 | } // doICPGlobalRelative 302 | 303 | 304 | void LTslam::detectInterSessionSCloops() // using ScanContext 305 | { 306 | auto& target_sess = sessions_.at(target_sess_idx); 307 | auto& source_sess = sessions_.at(source_sess_idx); 308 | 309 | // Detect loop closures: Find loop edge index pairs 310 | SCLoopIdxPairs_.clear(); 311 | RSLoopIdxPairs_.clear(); 312 | auto& target_scManager = target_sess.scManager; 313 | auto& source_scManager = source_sess.scManager; 314 | for (int source_node_idx=0; source_node_idx < int(source_scManager.polarcontexts_.size()); source_node_idx++) 315 | { 316 | std::vector source_node_key = source_scManager.polarcontext_invkeys_mat_.at(source_node_idx); 317 | Eigen::MatrixXd source_node_scd = source_scManager.polarcontexts_.at(source_node_idx); 318 | 319 | auto detectResult = target_scManager.detectLoopClosureIDBetweenSession(source_node_key, source_node_scd); // first: nn index, second: yaw diff 320 | 321 | int loop_idx_source_session = source_node_idx; 322 | int loop_idx_target_session = detectResult.first; 323 | 324 | if(loop_idx_target_session == -1) { // TODO using NO_LOOP_FOUND rather using -1 325 | RSLoopIdxPairs_.emplace_back(std::pair(-1, loop_idx_source_session)); // -1 will be later be found (nn pose). 326 | continue; 327 | } 328 | 329 | SCLoopIdxPairs_.emplace_back(std::pair(loop_idx_target_session, loop_idx_source_session)); 330 | } 331 | 332 | ROS_INFO_STREAM("\033[1;32m Total " << SCLoopIdxPairs_.size() << " inter-session loops are found. \033[0m"); 333 | } // detectInterSessionSCloops 334 | 335 | 336 | void LTslam::detectInterSessionRSloops() // using ScanContext 337 | { 338 | 339 | } // detectInterSessionRSloops 340 | 341 | 342 | void LTslam::addAllSessionsToGraph() 343 | { 344 | for(auto& _sess_pair: sessions_) 345 | { 346 | auto& _sess = _sess_pair.second; 347 | initTrajectoryByAnchoring(_sess); 348 | addSessionToCentralGraph(_sess); 349 | } 350 | } // addAllSessionsToGraph 351 | 352 | 353 | std::vector> LTslam::equisampleElements( 354 | const std::vector>& _input_pair, float _gap, int _num_sampled) 355 | { 356 | std::vector> sc_loop_idx_pairs_sampled; 357 | 358 | int equisampling_counter { 0 }; 359 | 360 | std::vector equisampled_idx; 361 | for (int i=0; i<_num_sampled; i++) 362 | equisampled_idx.emplace_back(std::round(float(i) * _gap)); 363 | 364 | for (auto& _idx: equisampled_idx) 365 | sc_loop_idx_pairs_sampled.emplace_back(_input_pair.at(_idx)); 366 | 367 | return sc_loop_idx_pairs_sampled; 368 | } 369 | 370 | void LTslam::addSCloops() 371 | { 372 | if(SCLoopIdxPairs_.empty()) 373 | return; 374 | 375 | // equi-sampling sc loops 376 | int num_scloops_all_found = int(SCLoopIdxPairs_.size()); 377 | int num_scloops_to_be_added = std::min( num_scloops_all_found, kNumSCLoopsUpperBound ); 378 | int equisampling_gap = num_scloops_all_found / num_scloops_to_be_added; 379 | 380 | auto sc_loop_idx_pairs_sampled = equisampleElements(SCLoopIdxPairs_, equisampling_gap, num_scloops_to_be_added); 381 | auto num_scloops_sampled = sc_loop_idx_pairs_sampled.size(); 382 | 383 | // add selected sc loops 384 | auto& target_sess = sessions_.at(target_sess_idx); 385 | auto& source_sess = sessions_.at(source_sess_idx); 386 | 387 | std::vector idx_added_loops; 388 | idx_added_loops.reserve(num_scloops_sampled); 389 | #pragma omp parallel for num_threads(numberOfCores) 390 | for (int ith = 0; ith < num_scloops_sampled; ith++) 391 | { 392 | auto& _loop_idx_pair = sc_loop_idx_pairs_sampled.at(ith); 393 | int loop_idx_target_session = _loop_idx_pair.first; 394 | int loop_idx_source_session = _loop_idx_pair.second; 395 | 396 | auto relative_pose_optional = doICPVirtualRelative(target_sess, source_sess, loop_idx_target_session, loop_idx_source_session); 397 | 398 | if(relative_pose_optional) { 399 | mtx.lock(); 400 | gtsam::Pose3 relative_pose = relative_pose_optional.value(); 401 | gtSAMgraph.add( BetweenFactorWithAnchoring( 402 | genGlobalNodeIdx(target_sess_idx, loop_idx_target_session), genGlobalNodeIdx(source_sess_idx, loop_idx_source_session), 403 | genAnchorNodeIdx(target_sess_idx), genAnchorNodeIdx(source_sess_idx), 404 | relative_pose, robustNoise) ); 405 | mtx.unlock(); 406 | 407 | // debug msg (would be removed later) 408 | mtx.lock(); 409 | idx_added_loops.emplace_back(loop_idx_target_session); 410 | cout << "SCdetector found an inter-session edge between " 411 | << genGlobalNodeIdx(target_sess_idx, loop_idx_target_session) << " and " << genGlobalNodeIdx(source_sess_idx, loop_idx_source_session) 412 | << " (anchor nodes are " << genAnchorNodeIdx(target_sess_idx) << " and " << genAnchorNodeIdx(source_sess_idx) << ")" << endl; 413 | mtx.unlock(); 414 | } 415 | } 416 | } // addSCloops 417 | 418 | 419 | double LTslam::calcInformationGainBtnTwoNodes(const int loop_idx_target_session, const int loop_idx_source_session) 420 | { 421 | auto pose_s1 = isamCurrentEstimate.at( genGlobalNodeIdx(target_sess_idx, loop_idx_target_session) ); // node: s1 is the central 422 | auto pose_s2 = isamCurrentEstimate.at( genGlobalNodeIdx(source_sess_idx, loop_idx_source_session) ); 423 | auto pose_s1_anchor = isamCurrentEstimate.at( genAnchorNodeIdx(target_sess_idx) ); 424 | auto pose_s2_anchor = isamCurrentEstimate.at( genAnchorNodeIdx(source_sess_idx) ); 425 | 426 | gtsam::Pose3 hx1 = traits::Compose(pose_s1_anchor, pose_s1); // for the updated jacobian, see line 60, 219, https://gtsam.org/doxygen/a00053_source.html 427 | gtsam::Pose3 hx2 = traits::Compose(pose_s2_anchor, pose_s2); 428 | gtsam::Pose3 estimated_relative_pose = traits::Between(hx1, hx2); 429 | 430 | gtsam::Matrix H_s1, H_s2, H_s1_anchor, H_s2_anchor; 431 | auto loop_factor = BetweenFactorWithAnchoring( 432 | genGlobalNodeIdx(target_sess_idx, loop_idx_target_session), genGlobalNodeIdx(source_sess_idx, loop_idx_source_session), 433 | genAnchorNodeIdx(target_sess_idx), genAnchorNodeIdx(source_sess_idx), 434 | estimated_relative_pose, robustNoise); 435 | loop_factor.evaluateError(pose_s1, pose_s2, pose_s1_anchor, pose_s2_anchor, 436 | H_s1, H_s2, H_s1_anchor, H_s2_anchor); 437 | 438 | gtsam::Matrix pose_s1_cov = isam->marginalCovariance(genGlobalNodeIdx(target_sess_idx, loop_idx_target_session)); // note: typedef Eigen::MatrixXd gtsam::Matrix 439 | gtsam::Matrix pose_s2_cov = isam->marginalCovariance(genGlobalNodeIdx(source_sess_idx, loop_idx_source_session)); 440 | 441 | // calc S and information gain 442 | gtsam::Matrix Sy = Eigen::MatrixXd::Identity(6, 6); // measurement noise, assume fixed 443 | gtsam::Matrix S = Sy + (H_s1*pose_s1_cov*H_s1.transpose() + H_s2*pose_s2_cov*H_s2.transpose()); 444 | double Sdet = S.determinant(); 445 | double information_gain = 0.5 * log( Sdet / Sy.determinant()); 446 | 447 | return information_gain; 448 | } 449 | 450 | void LTslam::findNearestRSLoopsTargetNodeIdx() // based-on information gain 451 | { 452 | std::vector> validRSLoopIdxPairs; 453 | 454 | for(std::size_t i=0; i(rsloop_global_idx_source_session); 463 | gtsam::Pose3 query_sess_anchor_transform = isamCurrentEstimate.at(genAnchorNodeIdx(source_sess_idx)); 464 | auto query_pose_central_coord = query_sess_anchor_transform * query_pose; 465 | 466 | // find nn pose idx in the target sess 467 | auto& target_sess = sessions_.at(target_sess_idx); 468 | std::vector target_node_idxes_within_ball; 469 | for (int target_node_idx=0; target_node_idx < int(target_sess.nodes_.size()); target_node_idx++) { 470 | auto target_pose = isamCurrentEstimate.at(genGlobalNodeIdx(target_sess_idx, target_node_idx)); 471 | if( poseDistance(query_pose_central_coord, target_pose) < 10.0 ) // 10 is a hard-coding for fast test 472 | { 473 | target_node_idxes_within_ball.push_back(target_node_idx); 474 | // cout << "(all) RS pair detected: " << target_node_idx << " <-> " << source_node_idx << endl; 475 | } 476 | } 477 | 478 | // if no nearest one, skip 479 | if(target_node_idxes_within_ball.empty()) 480 | continue; 481 | 482 | // selected a single one having maximum information gain 483 | int selected_near_target_node_idx; 484 | double max_information_gain {0.0}; 485 | for (int i=0; i max_information_gain) { 490 | selected_near_target_node_idx = nn_target_node_idx; 491 | max_information_gain = this_information_gain; 492 | } 493 | } 494 | 495 | // cout << "RS pair detected: " << selected_near_target_node_idx << " <-> " << source_node_idx << endl; 496 | // cout << "info gain: " << max_information_gain << endl; 497 | 498 | validRSLoopIdxPairs.emplace_back(std::pair{selected_near_target_node_idx, source_node_idx}); 499 | } 500 | 501 | // update 502 | RSLoopIdxPairs_.clear(); 503 | RSLoopIdxPairs_.resize((int)(validRSLoopIdxPairs.size())); 504 | std::copy( validRSLoopIdxPairs.begin(), validRSLoopIdxPairs.end(), RSLoopIdxPairs_.begin() ); 505 | } 506 | 507 | 508 | bool LTslam::addRSloops() 509 | { 510 | if( kNumRSLoopsUpperBound == 0 ) 511 | return false; 512 | 513 | // find nearest target node idx 514 | findNearestRSLoopsTargetNodeIdx(); 515 | 516 | // parse RS loop src idx 517 | int num_rsloops_all_found = int(RSLoopIdxPairs_.size()); 518 | if( num_rsloops_all_found == 0 ) 519 | return false; 520 | 521 | int num_rsloops_to_be_added = std::min( num_rsloops_all_found, kNumRSLoopsUpperBound ); 522 | int equisampling_gap = num_rsloops_all_found / num_rsloops_to_be_added; 523 | 524 | auto rs_loop_idx_pairs_sampled = equisampleElements(RSLoopIdxPairs_, equisampling_gap, num_rsloops_to_be_added); 525 | auto num_rsloops_sampled = rs_loop_idx_pairs_sampled.size(); 526 | 527 | cout << "num of RS pair: " << num_rsloops_all_found << endl; 528 | cout << "num of sampled RS pair: " << num_rsloops_sampled << endl; 529 | 530 | // add selected rs loops 531 | auto& target_sess = sessions_.at(target_sess_idx); 532 | auto& source_sess = sessions_.at(source_sess_idx); 533 | 534 | #pragma omp parallel for num_threads(numberOfCores) 535 | for (int ith = 0; ith < num_rsloops_sampled; ith++) 536 | { 537 | auto& _loop_idx_pair = rs_loop_idx_pairs_sampled.at(ith); 538 | int loop_idx_target_session = _loop_idx_pair.first; 539 | int loop_idx_source_session = _loop_idx_pair.second; 540 | 541 | auto relative_pose_optional = doICPGlobalRelative(target_sess, source_sess, loop_idx_target_session, loop_idx_source_session); 542 | 543 | if(relative_pose_optional) { 544 | mtx.lock(); 545 | gtsam::Pose3 relative_pose = relative_pose_optional.value(); 546 | gtSAMgraph.add( BetweenFactorWithAnchoring( 547 | genGlobalNodeIdx(target_sess_idx, loop_idx_target_session), genGlobalNodeIdx(source_sess_idx, loop_idx_source_session), 548 | genAnchorNodeIdx(target_sess_idx), genAnchorNodeIdx(source_sess_idx), 549 | relative_pose, robustNoise) ); 550 | mtx.unlock(); 551 | 552 | // debug msg (would be removed later) 553 | mtx.lock(); 554 | cout << "RS loop detector found an inter-session edge between " 555 | << genGlobalNodeIdx(target_sess_idx, loop_idx_target_session) << " and " << genGlobalNodeIdx(source_sess_idx, loop_idx_source_session) 556 | << " (anchor nodes are " << genAnchorNodeIdx(target_sess_idx) << " and " << genAnchorNodeIdx(source_sess_idx) << ")" << endl; 557 | mtx.unlock(); 558 | } 559 | } 560 | 561 | return true; 562 | } // addRSloops 563 | 564 | 565 | void LTslam::initTrajectoryByAnchoring(const Session& _sess) 566 | { 567 | int this_session_anchor_node_idx = genAnchorNodeIdx(_sess.index_); 568 | 569 | if(_sess.is_base_session_) { 570 | gtSAMgraph.add(PriorFactor(this_session_anchor_node_idx, poseOrigin, priorNoise)); 571 | } else { 572 | gtSAMgraph.add(PriorFactor(this_session_anchor_node_idx, poseOrigin, largeNoise)); 573 | } 574 | 575 | initialEstimate.insert(this_session_anchor_node_idx, poseOrigin); 576 | } // initTrajectoryByAnchoring 577 | 578 | 579 | void LTslam::addSessionToCentralGraph(const Session& _sess) 580 | { 581 | // add nodes 582 | for( auto& _node: _sess.nodes_) 583 | { 584 | int node_idx = _node.second.idx; 585 | auto& curr_pose = _node.second.initial; 586 | 587 | int prev_node_global_idx = genGlobalNodeIdx(_sess.index_, node_idx - 1); 588 | int curr_node_global_idx = genGlobalNodeIdx(_sess.index_, node_idx); 589 | 590 | gtsam::Vector Vector6(6); 591 | if(node_idx == 0) { // TODO consider later if the initial node idx is not zero (but if using SC-LIO-SAM, don't care) 592 | // prior node 593 | gtSAMgraph.add(PriorFactor(curr_node_global_idx, curr_pose, priorNoise)); 594 | initialEstimate.insert(curr_node_global_idx, curr_pose); 595 | } else { 596 | // odom nodes 597 | initialEstimate.insert(curr_node_global_idx, curr_pose); 598 | } 599 | } 600 | 601 | // add edges 602 | for( auto& _edge: _sess.edges_) 603 | { 604 | int from_node_idx = _edge.second.from_idx; 605 | int to_node_idx = _edge.second.to_idx; 606 | 607 | int from_node_global_idx = genGlobalNodeIdx(_sess.index_, from_node_idx); 608 | int to_node_global_idx = genGlobalNodeIdx(_sess.index_, to_node_idx); 609 | 610 | gtsam::Pose3 relative_pose = _edge.second.relative; 611 | if( std::abs(to_node_idx - from_node_idx) == 1) { 612 | // odom edge (temporally consecutive) 613 | gtSAMgraph.add(BetweenFactor(from_node_global_idx, to_node_global_idx, relative_pose, odomNoise)); 614 | if(is_display_debug_msgs_) cout << "add an odom edge between " << from_node_global_idx << " and " << to_node_global_idx << endl; 615 | } else { 616 | // loop edge 617 | gtSAMgraph.add(BetweenFactor(from_node_global_idx, to_node_global_idx, relative_pose, robustNoise)); 618 | if(is_display_debug_msgs_) cout << "add a loop edge between " << from_node_global_idx << " and " << to_node_global_idx << endl; 619 | } 620 | } 621 | 622 | } 623 | 624 | 625 | void LTslam::loadAllSessions() 626 | { 627 | // pose 628 | ROS_INFO_STREAM("\033[1;32m Load sessions' pose dasa from: " << sessions_dir_ << "\033[0m"); 629 | for(auto& _session_entry : fs::directory_iterator(sessions_dir_)) 630 | { 631 | std::string session_name = _session_entry.path().filename(); 632 | if( !isTwoStringSame(session_name, central_sess_name_) & !isTwoStringSame(session_name, query_sess_name_) ) { 633 | continue; // jan. 2021. currently designed for two-session ver. (TODO: be generalized for N-session co-optimization) 634 | } 635 | 636 | // save a session (read graph txt flie and load nodes and edges internally) 637 | int session_idx; 638 | if(isTwoStringSame(session_name, central_sess_name_)) 639 | session_idx = target_sess_idx; 640 | else 641 | session_idx = source_sess_idx; 642 | 643 | std::string session_dir_path = _session_entry.path(); 644 | 645 | // sessions_.emplace_back(Session(session_idx, session_name, session_dir_path, isTwoStringSame(session_name, central_sess_name_))); 646 | sessions_.insert( std::make_pair(session_idx, 647 | Session(session_idx, session_name, session_dir_path, isTwoStringSame(session_name, central_sess_name_))) ); 648 | 649 | // LTslam::num_sessions++; // incr the global index // TODO: make this private and provide incrSessionIdx 650 | } 651 | 652 | std::cout << std::boolalpha; 653 | ROS_INFO_STREAM("\033[1;32m Total : " << sessions_.size() << " sessions are loaded.\033[0m"); 654 | std::for_each( sessions_.begin(), sessions_.end(), [](auto& _sess_pair) { 655 | cout << " — " << _sess_pair.second.name_ << " (is central: " << _sess_pair.second.is_base_session_ << ")" << endl; 656 | } ); 657 | 658 | } // loadSession 659 | 660 | -------------------------------------------------------------------------------- /ltslam/src/RosParamServer.cpp: -------------------------------------------------------------------------------- 1 | #include "ltslam/RosParamServer.h" 2 | 3 | 4 | RosParamServer::RosParamServer() 5 | : nh(nh_super) 6 | { 7 | nh.param("ltslam/sessions_dir", sessions_dir_, "/"); 8 | nh.param("ltslam/central_sess_name", central_sess_name_, "01"); 9 | nh.param("ltslam/query_sess_name", query_sess_name_, "02"); 10 | 11 | nh.param("ltslam/save_directory", save_directory_, "/LTslam/"); // it means /.../home/LTslam/ 12 | 13 | int unused = system((std::string("exec rm -r ") + save_directory_).c_str()); 14 | unused = system((std::string("mkdir -p ") + save_directory_).c_str()); 15 | 16 | nh.param("ltslam/is_display_debug_msgs", is_display_debug_msgs_, false); 17 | 18 | nh.param("ltslam/numberOfCores", numberOfCores, 4); 19 | 20 | nh.param("ltslam/kNumSCLoopsUpperBound", kNumSCLoopsUpperBound, 10); 21 | nh.param("ltslam/kNumRSLoopsUpperBound", kNumRSLoopsUpperBound, 10); 22 | 23 | nh.param("ltslam/loopFitnessScoreThreshold", loopFitnessScoreThreshold, 0.5); 24 | 25 | usleep(100); 26 | } // ctor RosParamServer 27 | 28 | -------------------------------------------------------------------------------- /ltslam/src/Scancontext.cpp: -------------------------------------------------------------------------------- 1 | #include "ltslam/Scancontext.h" 2 | 3 | // namespace SC2 4 | // { 5 | 6 | void coreImportTest (void) 7 | { 8 | cout << "scancontext lib is successfully imported." << endl; 9 | } // coreImportTest 10 | 11 | 12 | float rad2deg(float radians) 13 | { 14 | return radians * 180.0 / M_PI; 15 | } 16 | 17 | float deg2rad(float degrees) 18 | { 19 | return degrees * M_PI / 180.0; 20 | } 21 | 22 | 23 | float xy2theta( const float & _x, const float & _y ) 24 | { 25 | if ( (_x >= 0) & (_y >= 0)) 26 | return (180/M_PI) * atan(_y / _x); 27 | 28 | if ( (_x < 0) & (_y >= 0)) 29 | return 180 - ( (180/M_PI) * atan(_y / (-_x)) ); 30 | 31 | if ( (_x < 0) & (_y < 0)) 32 | return 180 + ( (180/M_PI) * atan(_y / _x) ); 33 | 34 | if ( (_x >= 0) & (_y < 0)) 35 | return 360 - ( (180/M_PI) * atan((-_y) / _x) ); 36 | } // xy2theta 37 | 38 | 39 | MatrixXd circshift( MatrixXd &_mat, int _num_shift ) 40 | { 41 | // shift columns to right direction 42 | assert(_num_shift >= 0); 43 | 44 | if( _num_shift == 0 ) 45 | { 46 | MatrixXd shifted_mat( _mat ); 47 | return shifted_mat; // Early return 48 | } 49 | 50 | MatrixXd shifted_mat = MatrixXd::Zero( _mat.rows(), _mat.cols() ); 51 | for ( int col_idx = 0; col_idx < _mat.cols(); col_idx++ ) 52 | { 53 | int new_location = (col_idx + _num_shift) % _mat.cols(); 54 | shifted_mat.col(new_location) = _mat.col(col_idx); 55 | } 56 | 57 | return shifted_mat; 58 | 59 | } // circshift 60 | 61 | 62 | std::vector eig2stdvec( MatrixXd _eigmat ) 63 | { 64 | std::vector vec( _eigmat.data(), _eigmat.data() + _eigmat.size() ); 65 | return vec; 66 | } // eig2stdvec 67 | 68 | 69 | double SCManager::distDirectSC ( MatrixXd &_sc1, MatrixXd &_sc2 ) 70 | { 71 | int num_eff_cols = 0; // i.e., to exclude all-nonzero sector 72 | double sum_sector_similarity = 0; 73 | for ( int col_idx = 0; col_idx < _sc1.cols(); col_idx++ ) 74 | { 75 | VectorXd col_sc1 = _sc1.col(col_idx); 76 | VectorXd col_sc2 = _sc2.col(col_idx); 77 | 78 | if( (col_sc1.norm() == 0) | (col_sc2.norm() == 0) ) 79 | continue; // don't count this sector pair. 80 | 81 | double sector_similarity = col_sc1.dot(col_sc2) / (col_sc1.norm() * col_sc2.norm()); 82 | 83 | sum_sector_similarity = sum_sector_similarity + sector_similarity; 84 | num_eff_cols = num_eff_cols + 1; 85 | } 86 | 87 | double sc_sim = sum_sector_similarity / num_eff_cols; 88 | return 1.0 - sc_sim; 89 | 90 | } // distDirectSC 91 | 92 | 93 | int SCManager::fastAlignUsingVkey( MatrixXd & _vkey1, MatrixXd & _vkey2) 94 | { 95 | int argmin_vkey_shift = 0; 96 | double min_veky_diff_norm = 10000000; 97 | for ( int shift_idx = 0; shift_idx < _vkey1.cols(); shift_idx++ ) 98 | { 99 | MatrixXd vkey2_shifted = circshift(_vkey2, shift_idx); 100 | 101 | MatrixXd vkey_diff = _vkey1 - vkey2_shifted; 102 | 103 | double cur_diff_norm = vkey_diff.norm(); 104 | if( cur_diff_norm < min_veky_diff_norm ) 105 | { 106 | argmin_vkey_shift = shift_idx; 107 | min_veky_diff_norm = cur_diff_norm; 108 | } 109 | } 110 | 111 | return argmin_vkey_shift; 112 | 113 | } // fastAlignUsingVkey 114 | 115 | 116 | std::pair SCManager::distanceBtnScanContext( MatrixXd &_sc1, MatrixXd &_sc2 ) 117 | { 118 | // 1. fast align using variant key (not in original IROS18) 119 | MatrixXd vkey_sc1 = makeSectorkeyFromScancontext( _sc1 ); 120 | MatrixXd vkey_sc2 = makeSectorkeyFromScancontext( _sc2 ); 121 | int argmin_vkey_shift = fastAlignUsingVkey( vkey_sc1, vkey_sc2 ); 122 | 123 | const int SEARCH_RADIUS = round( 0.5 * SEARCH_RATIO * _sc1.cols() ); // a half of search range 124 | std::vector shift_idx_search_space { argmin_vkey_shift }; 125 | for ( int ii = 1; ii < SEARCH_RADIUS + 1; ii++ ) 126 | { 127 | shift_idx_search_space.push_back( (argmin_vkey_shift + ii + _sc1.cols()) % _sc1.cols() ); 128 | shift_idx_search_space.push_back( (argmin_vkey_shift - ii + _sc1.cols()) % _sc1.cols() ); 129 | } 130 | std::sort(shift_idx_search_space.begin(), shift_idx_search_space.end()); 131 | 132 | // 2. fast columnwise diff 133 | int argmin_shift = 0; 134 | double min_sc_dist = 10000000; 135 | for ( int num_shift: shift_idx_search_space ) 136 | { 137 | MatrixXd sc2_shifted = circshift(_sc2, num_shift); 138 | double cur_sc_dist = distDirectSC( _sc1, sc2_shifted ); 139 | if( cur_sc_dist < min_sc_dist ) 140 | { 141 | argmin_shift = num_shift; 142 | min_sc_dist = cur_sc_dist; 143 | } 144 | } 145 | 146 | return make_pair(min_sc_dist, argmin_shift); 147 | 148 | } // distanceBtnScanContext 149 | 150 | 151 | MatrixXd SCManager::makeScancontext( pcl::PointCloud & _scan_down ) 152 | { 153 | TicToc t_making_desc; 154 | 155 | int num_pts_scan_down = _scan_down.points.size(); 156 | 157 | // main 158 | const int NO_POINT = -1000; 159 | MatrixXd desc = NO_POINT * MatrixXd::Ones(PC_NUM_RING, PC_NUM_SECTOR); 160 | 161 | SCPointType pt; 162 | float azim_angle, azim_range; // wihtin 2d plane 163 | int ring_idx, sctor_idx; 164 | for (int pt_idx = 0; pt_idx < num_pts_scan_down; pt_idx++) 165 | { 166 | pt.x = _scan_down.points[pt_idx].x; 167 | pt.y = _scan_down.points[pt_idx].y; 168 | pt.z = _scan_down.points[pt_idx].z + LIDAR_HEIGHT; // naive adding is ok (all points should be > 0). 169 | 170 | // xyz to ring, sector 171 | azim_range = sqrt(pt.x * pt.x + pt.y * pt.y); 172 | azim_angle = xy2theta(pt.x, pt.y); 173 | 174 | // if range is out of roi, pass 175 | if( azim_range > PC_MAX_RADIUS ) 176 | continue; 177 | 178 | ring_idx = std::max( std::min( PC_NUM_RING, int(ceil( (azim_range / PC_MAX_RADIUS) * PC_NUM_RING )) ), 1 ); 179 | sctor_idx = std::max( std::min( PC_NUM_SECTOR, int(ceil( (azim_angle / 360.0) * PC_NUM_SECTOR )) ), 1 ); 180 | 181 | // taking maximum z 182 | if ( desc(ring_idx-1, sctor_idx-1) < pt.z ) // -1 means cpp starts from 0 183 | desc(ring_idx-1, sctor_idx-1) = pt.z; // update for taking maximum value at that bin 184 | } 185 | 186 | // reset no points to zero (for cosine dist later) 187 | for ( int row_idx = 0; row_idx < desc.rows(); row_idx++ ) 188 | for ( int col_idx = 0; col_idx < desc.cols(); col_idx++ ) 189 | if( desc(row_idx, col_idx) == NO_POINT ) 190 | desc(row_idx, col_idx) = 0; 191 | 192 | t_making_desc.toc("PolarContext making"); 193 | 194 | return desc; 195 | } // SCManager::makeScancontext 196 | 197 | 198 | MatrixXd SCManager::makeRingkeyFromScancontext( Eigen::MatrixXd &_desc ) 199 | { 200 | /* 201 | * summary: rowwise mean vector 202 | */ 203 | Eigen::MatrixXd invariant_key(_desc.rows(), 1); 204 | for ( int row_idx = 0; row_idx < _desc.rows(); row_idx++ ) 205 | { 206 | Eigen::MatrixXd curr_row = _desc.row(row_idx); 207 | invariant_key(row_idx, 0) = curr_row.mean(); 208 | } 209 | 210 | return invariant_key; 211 | } // SCManager::makeRingkeyFromScancontext 212 | 213 | 214 | MatrixXd SCManager::makeSectorkeyFromScancontext( Eigen::MatrixXd &_desc ) 215 | { 216 | /* 217 | * summary: columnwise mean vector 218 | */ 219 | Eigen::MatrixXd variant_key(1, _desc.cols()); 220 | for ( int col_idx = 0; col_idx < _desc.cols(); col_idx++ ) 221 | { 222 | Eigen::MatrixXd curr_col = _desc.col(col_idx); 223 | variant_key(0, col_idx) = curr_col.mean(); 224 | } 225 | 226 | return variant_key; 227 | } // SCManager::makeSectorkeyFromScancontext 228 | 229 | 230 | const Eigen::MatrixXd& SCManager::getConstRefRecentSCD(void) 231 | { 232 | return polarcontexts_.back(); 233 | } 234 | 235 | 236 | void SCManager::saveScancontextAndKeys( Eigen::MatrixXd _scd ) 237 | { 238 | Eigen::MatrixXd ringkey = makeRingkeyFromScancontext( _scd ); 239 | Eigen::MatrixXd sectorkey = makeSectorkeyFromScancontext( _scd ); 240 | std::vector polarcontext_invkey_vec = eig2stdvec( ringkey ); 241 | 242 | polarcontexts_.push_back( _scd ); 243 | polarcontext_invkeys_.push_back( ringkey ); 244 | polarcontext_vkeys_.push_back( sectorkey ); 245 | polarcontext_invkeys_mat_.push_back( polarcontext_invkey_vec ); 246 | } // SCManager::makeAndSaveScancontextAndKeys 247 | 248 | 249 | void SCManager::makeAndSaveScancontextAndKeys( pcl::PointCloud & _scan_down ) 250 | { 251 | Eigen::MatrixXd sc = makeScancontext(_scan_down); // v1 252 | Eigen::MatrixXd ringkey = makeRingkeyFromScancontext( sc ); 253 | Eigen::MatrixXd sectorkey = makeSectorkeyFromScancontext( sc ); 254 | std::vector polarcontext_invkey_vec = eig2stdvec( ringkey ); 255 | 256 | polarcontexts_.push_back( sc ); 257 | polarcontext_invkeys_.push_back( ringkey ); 258 | polarcontext_vkeys_.push_back( sectorkey ); 259 | polarcontext_invkeys_mat_.push_back( polarcontext_invkey_vec ); 260 | } // SCManager::makeAndSaveScancontextAndKeys 261 | 262 | 263 | std::pair SCManager::detectLoopClosureIDBetweenSession (std::vector& _curr_key, Eigen::MatrixXd& _curr_desc) 264 | { 265 | int loop_id { -1 }; // init with -1, -1 means no loop (== LeGO-LOAM's variable "closestHistoryFrameID") 266 | 267 | auto& curr_key = _curr_key; 268 | auto& curr_desc = _curr_desc; // current observation (query) 269 | 270 | // step 0: if first, construct the tree in batch 271 | if( ! is_tree_batch_made ) // run only once 272 | { 273 | polarcontext_invkeys_to_search_.clear(); 274 | polarcontext_invkeys_to_search_.assign( polarcontext_invkeys_mat_.begin(), polarcontext_invkeys_mat_.end() ) ; 275 | 276 | polarcontext_tree_batch_.reset(); 277 | polarcontext_tree_batch_ = std::make_unique(PC_NUM_RING /* dim */, polarcontext_invkeys_to_search_, 10 /* max leaf */ ); 278 | 279 | is_tree_batch_made = true; // for running this block only once 280 | } 281 | 282 | double min_dist = 10000000; // init with somthing large 283 | int nn_align = 0; 284 | int nn_idx = 0; 285 | 286 | // step 1: knn search 287 | std::vector candidate_indexes( NUM_CANDIDATES_FROM_TREE ); 288 | std::vector out_dists_sqr( NUM_CANDIDATES_FROM_TREE ); 289 | 290 | nanoflann::KNNResultSet knnsearch_result( NUM_CANDIDATES_FROM_TREE ); 291 | knnsearch_result.init( &candidate_indexes[0], &out_dists_sqr[0] ); 292 | polarcontext_tree_batch_->index->findNeighbors( knnsearch_result, &curr_key[0] /* query */, nanoflann::SearchParams(10) ); // error here 293 | 294 | // step 2: pairwise distance (find optimal columnwise best-fit using cosine distance) 295 | TicToc t_calc_dist; 296 | for ( int candidate_iter_idx = 0; candidate_iter_idx < NUM_CANDIDATES_FROM_TREE; candidate_iter_idx++ ) 297 | { 298 | MatrixXd polarcontext_candidate = polarcontexts_[ candidate_indexes[candidate_iter_idx] ]; 299 | std::pair sc_dist_result = distanceBtnScanContext( curr_desc, polarcontext_candidate ); 300 | 301 | double candidate_dist = sc_dist_result.first; 302 | int candidate_align = sc_dist_result.second; 303 | 304 | if( candidate_dist < min_dist ) 305 | { 306 | min_dist = candidate_dist; 307 | nn_align = candidate_align; 308 | 309 | nn_idx = candidate_indexes[candidate_iter_idx]; 310 | } 311 | } 312 | t_calc_dist.toc("Distance calc"); 313 | 314 | // step 3: similarity threshold 315 | if( min_dist < SC_DIST_THRES ) 316 | loop_id = nn_idx; 317 | 318 | // To do: return also nn_align (i.e., yaw diff) 319 | float yaw_diff_rad = deg2rad(nn_align * PC_UNIT_SECTORANGLE); 320 | std::pair result {loop_id, yaw_diff_rad}; 321 | 322 | return result; 323 | 324 | } // SCManager::detectLoopClosureIDBetweenSession 325 | 326 | 327 | std::pair SCManager::detectLoopClosureID ( void ) 328 | { 329 | int loop_id { -1 }; // init with -1, -1 means no loop (== LeGO-LOAM's variable "closestHistoryFrameID") 330 | 331 | auto curr_key = polarcontext_invkeys_mat_.back(); // current observation (query) 332 | auto curr_desc = polarcontexts_.back(); // current observation (query) 333 | 334 | /* 335 | * step 1: candidates from ringkey tree_ 336 | */ 337 | if( (int)polarcontext_invkeys_mat_.size() < NUM_EXCLUDE_RECENT + 1) 338 | { 339 | std::pair result {loop_id, 0.0}; 340 | return result; // Early return 341 | } 342 | 343 | // tree_ reconstruction (not mandatory to make everytime) 344 | if( tree_making_period_conter % TREE_MAKING_PERIOD_ == 0) // to save computation cost 345 | { 346 | TicToc t_tree_construction; 347 | 348 | polarcontext_invkeys_to_search_.clear(); 349 | polarcontext_invkeys_to_search_.assign( polarcontext_invkeys_mat_.begin(), polarcontext_invkeys_mat_.end() - NUM_EXCLUDE_RECENT ) ; 350 | 351 | polarcontext_tree_.reset(); 352 | polarcontext_tree_ = std::make_unique(PC_NUM_RING /* dim */, polarcontext_invkeys_to_search_, 10 /* max leaf */ ); 353 | // tree_ptr_->index->buildIndex(); // inernally called in the constructor of InvKeyTree (for detail, refer the nanoflann and KDtreeVectorOfVectorsAdaptor) 354 | t_tree_construction.toc("Tree construction"); 355 | } 356 | tree_making_period_conter = tree_making_period_conter + 1; 357 | 358 | double min_dist = 10000000; // init with somthing large 359 | int nn_align = 0; 360 | int nn_idx = 0; 361 | 362 | // knn search 363 | std::vector candidate_indexes( NUM_CANDIDATES_FROM_TREE ); 364 | std::vector out_dists_sqr( NUM_CANDIDATES_FROM_TREE ); 365 | 366 | TicToc t_tree_search; 367 | nanoflann::KNNResultSet knnsearch_result( NUM_CANDIDATES_FROM_TREE ); 368 | knnsearch_result.init( &candidate_indexes[0], &out_dists_sqr[0] ); 369 | polarcontext_tree_->index->findNeighbors( knnsearch_result, &curr_key[0] /* query */, nanoflann::SearchParams(10) ); 370 | t_tree_search.toc("Tree search"); 371 | 372 | /* 373 | * step 2: pairwise distance (find optimal columnwise best-fit using cosine distance) 374 | */ 375 | TicToc t_calc_dist; 376 | for ( int candidate_iter_idx = 0; candidate_iter_idx < NUM_CANDIDATES_FROM_TREE; candidate_iter_idx++ ) 377 | { 378 | MatrixXd polarcontext_candidate = polarcontexts_[ candidate_indexes[candidate_iter_idx] ]; 379 | std::pair sc_dist_result = distanceBtnScanContext( curr_desc, polarcontext_candidate ); 380 | 381 | double candidate_dist = sc_dist_result.first; 382 | int candidate_align = sc_dist_result.second; 383 | 384 | if( candidate_dist < min_dist ) 385 | { 386 | min_dist = candidate_dist; 387 | nn_align = candidate_align; 388 | 389 | nn_idx = candidate_indexes[candidate_iter_idx]; 390 | } 391 | } 392 | t_calc_dist.toc("Distance calc"); 393 | 394 | /* 395 | * loop threshold check 396 | */ 397 | if( min_dist < SC_DIST_THRES ) 398 | { 399 | loop_id = nn_idx; 400 | 401 | // std::cout.precision(3); 402 | cout << "[Loop found] Nearest distance: " << min_dist << " btn " << polarcontexts_.size()-1 << " and " << nn_idx << "." << endl; 403 | cout << "[Loop found] yaw diff: " << nn_align * PC_UNIT_SECTORANGLE << " deg." << endl; 404 | } 405 | else 406 | { 407 | std::cout.precision(3); 408 | cout << "[Not loop] Nearest distance: " << min_dist << " btn " << polarcontexts_.size()-1 << " and " << nn_idx << "." << endl; 409 | cout << "[Not loop] yaw diff: " << nn_align * PC_UNIT_SECTORANGLE << " deg." << endl; 410 | } 411 | 412 | // To do: return also nn_align (i.e., yaw diff) 413 | float yaw_diff_rad = deg2rad(nn_align * PC_UNIT_SECTORANGLE); 414 | std::pair result {loop_id, yaw_diff_rad}; 415 | 416 | return result; 417 | 418 | } // SCManager::detectLoopClosureID 419 | 420 | // } // namespace SC2 -------------------------------------------------------------------------------- /ltslam/src/Session.cpp: -------------------------------------------------------------------------------- 1 | #include "ltslam/Session.h" 2 | 3 | Session::Session() 4 | { 5 | 6 | } 7 | 8 | Session::Session(int _idx, std::string _name, std::string _session_dir_path, bool _is_base_session) 9 | : index_(_idx), name_(_name), session_dir_path_(_session_dir_path), is_base_session_(_is_base_session) 10 | { 11 | allocateMemory(); 12 | 13 | loadSessionGraph(); 14 | 15 | loadSessionScanContextDescriptors(); 16 | loadSessionKeyframePointclouds(); 17 | 18 | const float kICPFilterSize = 0.3; // TODO move to yaml 19 | downSizeFilterICP.setLeafSize(kICPFilterSize, kICPFilterSize, kICPFilterSize); 20 | 21 | } // ctor 22 | 23 | 24 | void Session::allocateMemory() 25 | { 26 | cloudKeyPoses6D.reset(new pcl::PointCloud()); 27 | originPoses6D.reset(new pcl::PointCloud()); 28 | } 29 | 30 | 31 | void Session::initKeyPoses(void) 32 | { 33 | for(auto & _node_info: nodes_) 34 | { 35 | PointTypePose thisPose6D; 36 | 37 | int node_idx = _node_info.first; 38 | Node node = _node_info.second; 39 | gtsam::Pose3 pose = node.initial; 40 | 41 | thisPose6D.x = pose.translation().x(); 42 | thisPose6D.y = pose.translation().x(); 43 | thisPose6D.z = pose.translation().x(); 44 | thisPose6D.intensity = node_idx; // TODO 45 | thisPose6D.roll = pose.rotation().roll(); 46 | thisPose6D.pitch = pose.rotation().pitch(); 47 | thisPose6D.yaw = pose.rotation().yaw(); 48 | thisPose6D.time = 0.0; // TODO 49 | 50 | cloudKeyPoses6D->push_back(thisPose6D); 51 | } 52 | 53 | PointTypePose thisPose6D; 54 | thisPose6D.x = 0.0; 55 | thisPose6D.y = 0.0; 56 | thisPose6D.z = 0.0; 57 | thisPose6D.intensity = 0.0; 58 | thisPose6D.roll = 0.0; 59 | thisPose6D.pitch = 0.0; 60 | thisPose6D.yaw = 0.0; 61 | thisPose6D.time = 0.0; 62 | originPoses6D->push_back( thisPose6D ); 63 | } // initKeyPoses 64 | 65 | 66 | void Session::updateKeyPoses(const gtsam::ISAM2 * _isam, const gtsam::Pose3& _anchor_transform) 67 | { 68 | using gtsam::Pose3; 69 | 70 | gtsam::Values isamCurrentEstimate = _isam->calculateEstimate(); 71 | 72 | int numPoses = cloudKeyFrames.size(); 73 | for (int node_idx_in_sess = 0; node_idx_in_sess < numPoses; ++node_idx_in_sess) 74 | { 75 | int node_idx_in_global = genGlobalNodeIdx(index_, node_idx_in_sess); 76 | // cout << "update the session " << index_ << "'s node: " << node_idx_in_sess << " (global idx: " << node_idx_in_global << ")" << endl; 77 | 78 | gtsam::Pose3 pose_self_coord = isamCurrentEstimate.at(node_idx_in_global); 79 | gtsam::Pose3 pose_central_coord = _anchor_transform * pose_self_coord; 80 | 81 | cloudKeyPoses6D->points[node_idx_in_sess].x = pose_central_coord.translation().x(); 82 | cloudKeyPoses6D->points[node_idx_in_sess].y = pose_central_coord.translation().y(); 83 | cloudKeyPoses6D->points[node_idx_in_sess].z = pose_central_coord.translation().z(); 84 | cloudKeyPoses6D->points[node_idx_in_sess].roll = pose_central_coord.rotation().roll(); 85 | cloudKeyPoses6D->points[node_idx_in_sess].pitch = pose_central_coord.rotation().pitch(); 86 | cloudKeyPoses6D->points[node_idx_in_sess].yaw = pose_central_coord.rotation().yaw(); 87 | } 88 | } // updateKeyPoses 89 | 90 | 91 | void Session::loopFindNearKeyframesCentralCoord( 92 | pcl::PointCloud::Ptr& nearKeyframes, 93 | const int& key, const int& searchNum) 94 | { 95 | // extract near keyframes 96 | nearKeyframes->clear(); 97 | int cloudSize = cloudKeyPoses6D->size(); 98 | for (int i = -searchNum; i <= searchNum; ++i) 99 | { 100 | int keyNear = key + i; 101 | if (keyNear < 0 || keyNear >= cloudSize ) 102 | continue; 103 | *nearKeyframes += *transformPointCloud(cloudKeyFrames[keyNear], &cloudKeyPoses6D->points[keyNear]); 104 | } 105 | 106 | if (nearKeyframes->empty()) 107 | return; 108 | 109 | // downsample near keyframes 110 | pcl::PointCloud::Ptr cloud_temp(new pcl::PointCloud()); 111 | downSizeFilterICP.setInputCloud(nearKeyframes); 112 | downSizeFilterICP.filter(*cloud_temp); 113 | nearKeyframes->clear(); // redundant? 114 | *nearKeyframes = *cloud_temp; 115 | } // loopFindNearKeyframesCentralCoord 116 | 117 | 118 | void Session::loopFindNearKeyframesLocalCoord( 119 | pcl::PointCloud::Ptr& nearKeyframes, 120 | const int& key, const int& searchNum) 121 | { 122 | // extract near keyframes 123 | nearKeyframes->clear(); 124 | int cloudSize = cloudKeyPoses6D->size(); 125 | for (int i = -searchNum; i <= searchNum; ++i) 126 | { 127 | int keyNear = key + i; 128 | if (keyNear < 0 || keyNear >= cloudSize ) 129 | continue; 130 | *nearKeyframes += *transformPointCloud(cloudKeyFrames[keyNear], &originPoses6D->points[0]); 131 | } 132 | 133 | if (nearKeyframes->empty()) 134 | return; 135 | 136 | // downsample near keyframes 137 | pcl::PointCloud::Ptr cloud_temp(new pcl::PointCloud()); 138 | downSizeFilterICP.setInputCloud(nearKeyframes); 139 | downSizeFilterICP.filter(*cloud_temp); 140 | nearKeyframes->clear(); // redundant? 141 | *nearKeyframes = *cloud_temp; 142 | } // loopFindNearKeyframesLocalCoord 143 | 144 | 145 | void Session::loadSessionKeyframePointclouds() 146 | { 147 | std::string pcd_dir = session_dir_path_ + "/Scans/"; 148 | 149 | // parse names (sorted) 150 | std::map pcd_names; // for auto-sort (because scManager should register SCDs in the right node order.) 151 | for(auto& _pcd : fs::directory_iterator(pcd_dir)) 152 | { 153 | std::string pcd_name = _pcd.path().filename(); 154 | 155 | std::stringstream pcd_name_stream {pcd_name}; 156 | std::string pcd_idx_str; 157 | getline(pcd_name_stream, pcd_idx_str, ','); 158 | int pcd_idx = std::stoi(pcd_idx_str); 159 | std::string pcd_name_filepath = _pcd.path(); 160 | 161 | pcd_names.insert(std::make_pair(pcd_idx, pcd_name_filepath)); 162 | } 163 | 164 | // load PCDs 165 | int num_pcd_loaded = 0; 166 | for (auto const& _pcd_name: pcd_names) 167 | { 168 | // cout << " load " << _pcd_name.second << endl; 169 | pcl::PointCloud::Ptr thisKeyFrame(new pcl::PointCloud()); 170 | pcl::io::loadPCDFile (_pcd_name.second, *thisKeyFrame); 171 | cloudKeyFrames.push_back(thisKeyFrame); 172 | 173 | num_pcd_loaded++; 174 | if(num_pcd_loaded >= nodes_.size()) { 175 | break; 176 | } 177 | } 178 | cout << "PCDs are loaded (" << name_ << ")" << endl; 179 | } 180 | 181 | 182 | void Session::loadSessionScanContextDescriptors() 183 | { 184 | std::string scd_dir = session_dir_path_ + "/SCDs/"; 185 | 186 | // parse names (sorted) 187 | std::map scd_names; // for auto-sort (because scManager should register SCDs in the right node order.) 188 | for(auto& _scd : fs::directory_iterator(scd_dir)) 189 | { 190 | std::string scd_name = _scd.path().filename(); 191 | 192 | std::stringstream scd_name_stream {scd_name}; 193 | std::string scd_idx_str; 194 | getline(scd_name_stream, scd_idx_str, ','); 195 | int scd_idx = std::stoi(scd_idx_str); 196 | std::string scd_name_filepath = _scd.path(); 197 | 198 | scd_names.insert(std::make_pair(scd_idx, scd_name_filepath)); 199 | } 200 | 201 | // load SCDs 202 | int num_scd_loaded = 0; 203 | for (auto const& _scd_name: scd_names) 204 | { 205 | // std::cout << "load a SCD: " << _scd_name.second << endl; 206 | Eigen::MatrixXd scd = readSCD(_scd_name.second); 207 | scManager.saveScancontextAndKeys(scd); 208 | 209 | num_scd_loaded++; 210 | if(num_scd_loaded >= nodes_.size()) { 211 | break; 212 | } 213 | } 214 | cout << "SCDs are loaded (" << name_ << ")" << endl; 215 | 216 | } // loadSessionScanContextDescriptors 217 | 218 | 219 | void Session::loadSessionGraph() 220 | { 221 | std::string posefile_path = session_dir_path_ + "/singlesession_posegraph.g2o"; 222 | 223 | std::ifstream posefile_handle (posefile_path); 224 | std::string strOneLine; 225 | while (getline(posefile_handle, strOneLine)) 226 | { 227 | G2oLineInfo line_info = splitG2oFileLine(strOneLine); 228 | 229 | // save variables (nodes) 230 | if( isTwoStringSame(line_info.type, G2oLineInfo::kVertexTypeName) ) { 231 | Node this_node { line_info.curr_idx, gtsam::Pose3( 232 | gtsam::Rot3(gtsam::Quaternion(line_info.quat[3], line_info.quat[0], line_info.quat[1], line_info.quat[2])), // xyzw to wxyz 233 | gtsam::Point3(line_info.trans[0], line_info.trans[1], line_info.trans[2])) }; 234 | nodes_.insert(std::pair(line_info.curr_idx, this_node)); // giseop for multimap 235 | } 236 | 237 | // save edges 238 | if( isTwoStringSame(line_info.type, G2oLineInfo::kEdgeTypeName) ) { 239 | Edge this_edge { line_info.prev_idx, line_info.curr_idx, gtsam::Pose3( 240 | gtsam::Rot3(gtsam::Quaternion(line_info.quat[3], line_info.quat[0], line_info.quat[1], line_info.quat[2])), // xyzw to wxyz 241 | gtsam::Point3(line_info.trans[0], line_info.trans[1], line_info.trans[2])) }; 242 | edges_.insert(std::pair(line_info.prev_idx, this_edge)); // giseop for multimap 243 | } 244 | } 245 | 246 | initKeyPoses(); 247 | 248 | // 249 | ROS_INFO_STREAM("\033[1;32m Session loaded: " << posefile_path << "\033[0m"); 250 | ROS_INFO_STREAM("\033[1;32m - num nodes: " << nodes_.size() << "\033[0m"); 251 | } // loadSessionGraph 252 | 253 | -------------------------------------------------------------------------------- /ltslam/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "ltslam/LTslam.h" 2 | 3 | using namespace gtsam; 4 | 5 | int main(int argc, char** argv) 6 | { 7 | ros::init(argc, argv, "main"); 8 | ROS_INFO("\033[1;32m----> LTslam starts.\033[0m"); 9 | 10 | LTslam ltslam3d; 11 | ltslam3d.run(); 12 | 13 | ROS_INFO("\033[1;32m----> LTslam done.\033[0m"); 14 | ros::spin(); 15 | return 0; 16 | } 17 | -------------------------------------------------------------------------------- /ltslam/src/utility.cpp: -------------------------------------------------------------------------------- 1 | #include "ltslam/utility.h" 2 | 3 | bool cout_debug = false; 4 | 5 | 6 | void readBin(std::string _bin_path, pcl::PointCloud::Ptr _pcd_ptr) 7 | { 8 | std::fstream input(_bin_path.c_str(), ios::in | ios::binary); 9 | if(!input.good()){ 10 | cerr << "Could not read file: " << _bin_path << endl; 11 | exit(EXIT_FAILURE); 12 | } 13 | input.seekg(0, ios::beg); 14 | 15 | for (int ii=0; input.good() && !input.eof(); ii++) { 16 | PointType point; 17 | 18 | input.read((char *) &point.x, sizeof(float)); 19 | input.read((char *) &point.y, sizeof(float)); 20 | input.read((char *) &point.z, sizeof(float)); 21 | input.read((char *) &point.intensity, sizeof(float)); 22 | 23 | _pcd_ptr->push_back(point); 24 | } 25 | input.close(); 26 | } 27 | 28 | 29 | namespace LTslamParam { 30 | // NOTE: kSessionStartIdxOffset in utility.h 31 | 32 | int ungenGlobalNodeIdx (const int& _session_idx, const int& _idx_in_graph) 33 | { 34 | return (_idx_in_graph - 1) / (_session_idx * kSessionStartIdxOffset); 35 | } // ungenGlobalNodeIdx 36 | 37 | int genGlobalNodeIdx (const int& _session_idx, const int& _node_offset) 38 | { 39 | // return (_session_idx * LTslam::kSessionStartIdxOffset) + _node_offset + 1; 40 | return (_session_idx * kSessionStartIdxOffset) + _node_offset + 1; 41 | } // genGlobalNodeIdx 42 | 43 | int genAnchorNodeIdx (const int& _session_idx) 44 | { 45 | return (_session_idx * kSessionStartIdxOffset); 46 | // return (_session_idx * LTslam::kSessionStartIdxOffset) + (LTslam::kSessionStartIdxOffset - 1); 47 | } // genAnchorNodeIdx 48 | 49 | } // namespace LTslamParam 50 | 51 | 52 | inline float rad2deg(float radians) 53 | { 54 | return radians * 180.0 / M_PI; 55 | } 56 | 57 | inline float deg2rad(float degrees) 58 | { 59 | return degrees * M_PI / 180.0; 60 | } 61 | 62 | Eigen::Affine3f pclPointToAffine3f(PointTypePose thisPoint) 63 | { 64 | return pcl::getTransformation(thisPoint.x, thisPoint.y, thisPoint.z, thisPoint.roll, thisPoint.pitch, thisPoint.yaw); 65 | } 66 | 67 | gtsam::Pose3 pclPointTogtsamPose3(PointTypePose thisPoint) 68 | { 69 | return gtsam::Pose3(gtsam::Rot3::RzRyRx(double(thisPoint.roll), double(thisPoint.pitch), double(thisPoint.yaw)), 70 | gtsam::Point3(double(thisPoint.x), double(thisPoint.y), double(thisPoint.z))); 71 | } 72 | 73 | void fsmkdir(std::string _path) 74 | { 75 | if (!fs::is_directory(_path) || !fs::exists(_path)) 76 | fs::create_directories(_path); // create src folder 77 | } //fsmkdir 78 | 79 | 80 | pcl::PointCloud::Ptr transformPointCloud(pcl::PointCloud::Ptr cloudIn, PointTypePose* transformIn) 81 | { 82 | pcl::PointCloud::Ptr cloudOut(new pcl::PointCloud()); 83 | 84 | PointType *pointFrom; 85 | 86 | int cloudSize = cloudIn->size(); 87 | cloudOut->resize(cloudSize); 88 | 89 | Eigen::Affine3f transCur = pcl::getTransformation(transformIn->x, transformIn->y, transformIn->z, 90 | transformIn->roll, transformIn->pitch, transformIn->yaw); 91 | 92 | int numberOfCores = 8; // TODO move to yaml 93 | #pragma omp parallel for num_threads(numberOfCores) 94 | for (int i = 0; i < cloudSize; ++i) 95 | { 96 | pointFrom = &cloudIn->points[i]; 97 | cloudOut->points[i].x = transCur(0,0) * pointFrom->x + transCur(0,1) * pointFrom->y + transCur(0,2) * pointFrom->z + transCur(0,3); 98 | cloudOut->points[i].y = transCur(1,0) * pointFrom->x + transCur(1,1) * pointFrom->y + transCur(1,2) * pointFrom->z + transCur(1,3); 99 | cloudOut->points[i].z = transCur(2,0) * pointFrom->x + transCur(2,1) * pointFrom->y + transCur(2,2) * pointFrom->z + transCur(2,3); 100 | cloudOut->points[i].intensity = pointFrom->intensity; 101 | } 102 | return cloudOut; 103 | } 104 | 105 | std::vector> sortVecWithIdx(const std::vector& arr) 106 | { 107 | std::vector > vp; 108 | for (int i = 0; i < arr.size(); ++i) 109 | vp.push_back(std::make_pair(arr[i], i)); 110 | 111 | std::sort(vp.begin(), vp.end(), std::greater<>()); 112 | return vp; 113 | } 114 | 115 | sensor_msgs::PointCloud2 publishCloud(ros::Publisher *thisPub, pcl::PointCloud::Ptr thisCloud, ros::Time thisStamp, std::string thisFrame) 116 | { 117 | sensor_msgs::PointCloud2 tempCloud; 118 | pcl::toROSMsg(*thisCloud, tempCloud); 119 | tempCloud.header.stamp = thisStamp; 120 | tempCloud.header.frame_id = thisFrame; 121 | if (thisPub->getNumSubscribers() != 0) 122 | thisPub->publish(tempCloud); 123 | return tempCloud; 124 | } 125 | 126 | 127 | std::vector splitPoseLine(std::string _str_line, char _delimiter) { 128 | std::vector parsed; 129 | std::stringstream ss(_str_line); 130 | std::string temp; 131 | while (getline(ss, temp, _delimiter)) { 132 | parsed.push_back(std::stod(temp)); // convert string to "double" 133 | } 134 | return parsed; 135 | } 136 | 137 | G2oLineInfo splitG2oFileLine(std::string _str_line) { 138 | 139 | std::stringstream ss(_str_line); 140 | 141 | std::vector parsed_elms ; 142 | std::string elm; 143 | char delimiter = ' '; 144 | while (getline(ss, elm, delimiter)) { 145 | parsed_elms.push_back(elm); // convert string to "double" 146 | } 147 | 148 | G2oLineInfo parsed; 149 | if( isTwoStringSame(parsed_elms.at(0), G2oLineInfo::kVertexTypeName) ) 150 | { 151 | parsed.type = parsed_elms.at(0); 152 | parsed.curr_idx = std::stoi(parsed_elms.at(1)); 153 | parsed.trans.push_back(std::stod(parsed_elms.at(2))); 154 | parsed.trans.push_back(std::stod(parsed_elms.at(3))); 155 | parsed.trans.push_back(std::stod(parsed_elms.at(4))); 156 | parsed.quat.push_back(std::stod(parsed_elms.at(5))); 157 | parsed.quat.push_back(std::stod(parsed_elms.at(6))); 158 | parsed.quat.push_back(std::stod(parsed_elms.at(7))); 159 | parsed.quat.push_back(std::stod(parsed_elms.at(8))); 160 | } 161 | if( isTwoStringSame(parsed_elms.at(0), G2oLineInfo::kEdgeTypeName) ) 162 | { 163 | parsed.type = parsed_elms.at(0); 164 | parsed.prev_idx = std::stoi(parsed_elms.at(1)); 165 | parsed.curr_idx = std::stoi(parsed_elms.at(2)); 166 | parsed.trans.push_back(std::stod(parsed_elms.at(3))); 167 | parsed.trans.push_back(std::stod(parsed_elms.at(4))); 168 | parsed.trans.push_back(std::stod(parsed_elms.at(5))); 169 | parsed.quat.push_back(std::stod(parsed_elms.at(6))); 170 | parsed.quat.push_back(std::stod(parsed_elms.at(7))); 171 | parsed.quat.push_back(std::stod(parsed_elms.at(8))); 172 | parsed.quat.push_back(std::stod(parsed_elms.at(9))); 173 | } 174 | 175 | return parsed; 176 | } 177 | 178 | bool isTwoStringSame(std::string _str1, std::string _str2) 179 | { 180 | return !(_str1.compare(_str2)); 181 | } 182 | 183 | void collect_digits(std::vector& digits, int num) { 184 | if (num > 9) { 185 | collect_digits(digits, num / 10); 186 | } 187 | digits.push_back(num % 10); 188 | } 189 | 190 | void writePose3ToStream(std::fstream& _stream, gtsam::Pose3 _pose) 191 | { 192 | gtsam::Point3 t = _pose.translation(); 193 | gtsam::Rot3 R = _pose.rotation(); 194 | 195 | // r1 means column 1 (see https://gtsam.org/doxygen/a02759.html) 196 | std::string sep = " "; // separator 197 | _stream << R.r1().x() << sep << R.r2().x() << sep << R.r3().x() << sep << t.x() << sep 198 | << R.r1().y() << sep << R.r2().y() << sep << R.r3().y() << sep << t.y() << sep 199 | << R.r1().z() << sep << R.r2().z() << sep << R.r3().z() << sep << t.z() << std::endl; 200 | } 201 | 202 | std::vector linspace(int a, int b, int N) { 203 | int h = (b - a) / static_cast(N-1); 204 | std::vector xs(N); 205 | typename std::vector::iterator x; 206 | int val; 207 | for (x = xs.begin(), val = a; x != xs.end(); ++x, val += h) 208 | *x = val; 209 | return xs; 210 | } 211 | 212 | void saveSCD(std::string fileName, Eigen::MatrixXd matrix, std::string delimiter = " ") 213 | { 214 | // delimiter: ", " or " " etc. 215 | 216 | int precision = 3; // or Eigen::FullPrecision, but SCD does not require such accruate precisions so 3 is enough. 217 | const static Eigen::IOFormat the_format(precision, Eigen::DontAlignCols, delimiter, "\n"); 218 | 219 | std::ofstream file(fileName); 220 | if (file.is_open()) 221 | { 222 | file << matrix.format(the_format); 223 | file.close(); 224 | } 225 | } 226 | 227 | Eigen::MatrixXd readSCD(std::string fileToOpen) 228 | { 229 | // ref: https://aleksandarhaber.com/eigen-matrix-library-c-tutorial-saving-and-loading-data-in-from-a-csv-file/ 230 | using namespace Eigen; 231 | 232 | std::vector matrixEntries; 233 | std::ifstream matrixDataFile(fileToOpen); 234 | std::string matrixRowString; 235 | std::string matrixEntry; 236 | 237 | int matrixRowNumber = 0; 238 | while (getline(matrixDataFile, matrixRowString)) { 239 | std::stringstream matrixRowStringStream(matrixRowString); //convert matrixRowString that is a string to a stream variable. 240 | while (getline(matrixRowStringStream, matrixEntry, ' ')) // here we read pieces of the stream matrixRowStringStream until every comma, and store the resulting character into the matrixEntry 241 | matrixEntries.push_back(stod(matrixEntry)); //here we convert the string to double and fill in the row vector storing all the matrix entries 242 | 243 | matrixRowNumber++; //update the column numbers 244 | } 245 | return Map>(matrixEntries.data(), matrixRowNumber, matrixEntries.size() / matrixRowNumber); 246 | } 247 | 248 | float poseDistance(const gtsam::Pose3& p1, const gtsam::Pose3& p2) 249 | { 250 | auto p1x = p1.translation().x(); 251 | auto p1y = p1.translation().y(); 252 | auto p1z = p1.translation().z(); 253 | auto p2x = p2.translation().x(); 254 | auto p2y = p2.translation().y(); 255 | auto p2z = p2.translation().z(); 256 | 257 | return sqrt((p1x-p2x)*(p1x-p2x) + (p1y-p2y)*(p1y-p2y) + (p1z-p2z)*(p1z-p2z)); 258 | } 259 | 260 | 261 | 262 | 263 | // SphericalPoint cart2sph(const PointType & _cp) 264 | // { // _cp means cartesian point 265 | 266 | // if(cout_debug){ 267 | // cout << "Cartesian Point [x, y, z]: [" << _cp.x << ", " << _cp.y << ", " << _cp.z << endl; 268 | // } 269 | 270 | // SphericalPoint sph_point { 271 | // std::atan2(_cp.y, _cp.x), 272 | // std::atan2(_cp.z, std::sqrt(_cp.x*_cp.x + _cp.y*_cp.y)), 273 | // std::sqrt(_cp.x*_cp.x + _cp.y*_cp.y + _cp.z*_cp.z) 274 | // }; 275 | // return sph_point; 276 | // } 277 | 278 | 279 | // std::pair resetRimgSize(const std::pair _fov, const float _resize_ratio) 280 | // { 281 | // // default is 1 deg x 1 deg 282 | // float alpha_vfov = _resize_ratio; 283 | // float alpha_hfov = _resize_ratio; 284 | 285 | // float V_FOV = _fov.first; 286 | // float H_FOV = _fov.second; 287 | 288 | // int NUM_RANGE_IMG_ROW = std::round(V_FOV*alpha_vfov); 289 | // int NUM_RANGE_IMG_COL = std::round(H_FOV*alpha_hfov); 290 | 291 | // std::pair rimg {NUM_RANGE_IMG_ROW, NUM_RANGE_IMG_COL}; 292 | // return rimg; 293 | // } 294 | 295 | 296 | // std::set convertIntVecToSet(const std::vector & v) 297 | // { 298 | // std::set s; 299 | // for (int x : v) { 300 | // s.insert(x); 301 | // } 302 | // return s; 303 | // } 304 | 305 | // void pubRangeImg(cv::Mat& _rimg, 306 | // sensor_msgs::ImagePtr& _msg, 307 | // image_transport::Publisher& _publiser, 308 | // std::pair _caxis) 309 | // { 310 | // cv::Mat scan_rimg_viz = convertColorMappedImg(_rimg, _caxis); 311 | // _msg = cvmat2msg(scan_rimg_viz); 312 | // _publiser.publish(_msg); 313 | // } // pubRangeImg 314 | 315 | 316 | // sensor_msgs::ImagePtr cvmat2msg(const cv::Mat &_img) 317 | // { 318 | // sensor_msgs::ImagePtr msg = cv_bridge::CvImage(std_msgs::Header(), "bgr8", _img).toImageMsg(); 319 | // return msg; 320 | // } 321 | 322 | 323 | // void publishPointcloud2FromPCLptr(const ros::Publisher& _scan_publisher, const pcl::PointCloud::Ptr _scan) 324 | // { 325 | // sensor_msgs::PointCloud2 tempCloud; 326 | // pcl::toROSMsg(*_scan, tempCloud); 327 | // tempCloud.header.stamp = ros::Time::now(); 328 | // tempCloud.header.frame_id = "ltmapper"; 329 | // _scan_publisher.publish(tempCloud); 330 | // } // publishPointcloud2FromPCLptr 331 | 332 | 333 | // sensor_msgs::PointCloud2 publishCloud(ros::Publisher *thisPub, pcl::PointCloud::Ptr thisCloud, ros::Time thisStamp, std::string thisFrame) 334 | // { 335 | // sensor_msgs::PointCloud2 tempCloud; 336 | // pcl::toROSMsg(*thisCloud, tempCloud); 337 | // tempCloud.header.stamp = thisStamp; 338 | // tempCloud.header.frame_id = thisFrame; 339 | // if (thisPub->getNumSubscribers() != 0) 340 | // thisPub->publish(tempCloud); 341 | // return tempCloud; 342 | // } 343 | 344 | // float pointDistance(PointType p) 345 | // { 346 | // return sqrt(p.x*p.x + p.y*p.y + p.z*p.z); 347 | // } 348 | 349 | // float pointDistance(PointType p1, PointType p2) 350 | // { 351 | // return sqrt((p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y) + (p1.z-p2.z)*(p1.z-p2.z)); 352 | // } 353 | 354 | --------------------------------------------------------------------------------