├── .gitignore
├── CMakeLists.txt
├── README.MD
├── config
├── config_indoor.yaml
└── config_outdoor.yaml
├── example
└── place_recognition.cpp
├── figs
├── system_pipeline_btc.png
└── video_cover.jpg
├── include
├── CustomMsg.h
├── CustomPoint.h
├── btc.h
└── utils.h
├── launch
└── place_recognition.launch
├── package.xml
├── poses
├── kitti00.txt
├── kitti02.txt
├── kitti05.txt
├── kitti06.txt
├── kitti07.txt
└── kitti08.txt
├── rviz_cfg
└── loop.rviz
├── src
├── btc.cpp
└── utils.cpp
└── supply
└── Supplementary_Material_for_BTC.pdf
/.gitignore:
--------------------------------------------------------------------------------
1 | # Edit at https://www.toptal.com/developers/gitignore?templates=python
2 |
3 | ### Python ###
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | share/python-wheels/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 | MANIFEST
31 |
32 | # PyInstaller
33 | # Usually these files are written by a python script from a template
34 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
35 | *.manifest
36 | *.spec
37 |
38 | # Installer logs
39 | pip-log.txt
40 | pip-delete-this-directory.txt
41 |
42 | # Unit test / coverage reports
43 | htmlcov/
44 | .tox/
45 | .nox/
46 | .coverage
47 | .coverage.*
48 | .cache
49 | nosetests.xml
50 | coverage.xml
51 | *.cover
52 | *.py,cover
53 | .hypothesis/
54 | .pytest_cache/
55 | cover/
56 |
57 | # Translations
58 | *.mo
59 | *.pot
60 |
61 | # Django stuff:
62 | *.log
63 | local_settings.py
64 | db.sqlite3
65 | db.sqlite3-journal
66 |
67 | # Flask stuff:
68 | instance/
69 | .webassets-cache
70 |
71 | # Scrapy stuff:
72 | .scrapy
73 |
74 | # Sphinx documentation
75 | docs/_build/
76 |
77 | # PyBuilder
78 | .pybuilder/
79 | target/
80 |
81 | # Jupyter Notebook
82 | .ipynb_checkpoints
83 |
84 | # IPython
85 | profile_default/
86 | ipython_config.py
87 |
88 | # pyenv
89 | # For a library or package, you might want to ignore these files since the code is
90 | # intended to run in multiple environments; otherwise, check them in:
91 | # .python-version
92 |
93 | # pipenv
94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
97 | # install all needed dependencies.
98 | #Pipfile.lock
99 |
100 | # poetry
101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
102 | # This is especially recommended for binary packages to ensure reproducibility, and is more
103 | # commonly ignored for libraries.
104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
105 | #poetry.lock
106 |
107 | # pdm
108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
109 | #pdm.lock
110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
111 | # in version control.
112 | # https://pdm.fming.dev/#use-with-ide
113 | .pdm.toml
114 |
115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
116 | __pypackages__/
117 | _skbuild/
118 |
119 | # Celery stuff
120 | celerybeat-schedule
121 | celerybeat.pid
122 |
123 | # SageMath parsed files
124 | *.sage.py
125 |
126 | # Environments
127 | .env
128 | .venv
129 | env/
130 | venv/
131 | ENV/
132 | env.bak/
133 | venv.bak/
134 |
135 | # Spyder project settings
136 | .spyderproject
137 | .spyproject
138 |
139 | # Rope project settings
140 | .ropeproject
141 |
142 | # mkdocs documentation
143 | /site
144 |
145 | # mypy
146 | .mypy_cache/
147 | .dmypy.json
148 | dmypy.json
149 |
150 | # Pyre type checker
151 | .pyre/
152 |
153 | # pytype static type analyzer
154 | .pytype/
155 |
156 | # Cython debug symbols
157 | cython_debug/
158 |
159 | # PyCharm
160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162 | # and can be added to the global gitignore or merged into this file. For a more nuclear
163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
164 | #.idea/
165 |
166 | ### Python Patch ###
167 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
168 | poetry.toml
169 |
170 | # ruff
171 | .ruff_cache/
172 |
173 | # LSP config files
174 | pyrightconfig.json
175 |
176 | # End of https://www.toptal.com/developers/gitignore/api/python
177 | # Created by https://www.toptal.com/developers/gitignore/api/cmake
178 | # Edit at https://www.toptal.com/developers/gitignore?templates=cmake
179 |
180 | ### CMake ###
181 | CMakeLists.txt.user
182 | CMakeCache.txt
183 | CMakeFiles
184 | CMakeScripts
185 | Testing
186 | Makefile
187 | cmake_install.cmake
188 | install_manifest.txt
189 | compile_commands.json
190 | CTestTestfile.cmake
191 | _deps
192 |
193 | ### CMake Patch ###
194 | # External projects
195 | *-prefix/
196 |
197 | # End of https://www.toptal.com/developers/gitignore/api/cmake
198 | n# Created by https://www.toptal.com/developers/gitignore/api/c++
199 | # Edit at https://www.toptal.com/developers/gitignore?templates=c++
200 |
201 | ### C++ ###
202 | # Prerequisites
203 | *.d
204 |
205 | # Compiled Object files
206 | *.slo
207 | *.lo
208 | *.o
209 | *.obj
210 |
211 | # Precompiled Headers
212 | *.gch
213 | *.pch
214 |
215 | # Compiled Dynamic libraries
216 | *.so
217 | *.dylib
218 | *.dll
219 |
220 | # Fortran module files
221 | *.mod
222 | *.smod
223 |
224 | # Compiled Static libraries
225 | *.lai
226 | *.la
227 | *.a
228 | *.lib
229 |
230 | # Executables
231 | *.exe
232 | *.out
233 | *.app
234 |
235 | # End of https://www.toptal.com/developers/gitignore/api/c++
236 |
237 | .vscode/# Created by https://www.toptal.com/developers/gitignore/api/ros
238 | # Edit at https://www.toptal.com/developers/gitignore?templates=ros
239 |
240 | ### ROS ###
241 | devel/
242 | logs/
243 | build/
244 | bin/
245 | lib/
246 | msg_gen/
247 | srv_gen/
248 | msg/*Action.msg
249 | msg/*ActionFeedback.msg
250 | msg/*ActionGoal.msg
251 | msg/*ActionResult.msg
252 | msg/*Feedback.msg
253 | msg/*Goal.msg
254 | msg/*Result.msg
255 | msg/_*.py
256 | build_isolated/
257 | devel_isolated/
258 |
259 | # Generated by dynamic reconfigure
260 | *.cfgc
261 | /cfg/cpp/
262 | /cfg/*.py
263 |
264 | # Ignore generated docs
265 | *.dox
266 | *.wikidoc
267 |
268 | # eclipse stuff
269 | .project
270 | .cproject
271 |
272 | # qcreator stuff
273 | CMakeLists.txt.user
274 |
275 | srv/_*.py
276 | *.pcd
277 | *.pyc
278 | qtcreator-*
279 | *.user
280 |
281 | /planning/cfg
282 | /planning/docs
283 | /planning/src
284 |
285 | *~
286 |
287 | # Emacs
288 | .#*
289 |
290 | # Catkin custom files
291 | CATKIN_IGNORE
292 |
293 | # End of https://www.toptal.com/developers/gitignore/api/ros
294 | n
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.0.2)
2 | project(btc_desc)
3 |
4 | ## Compile as C++11, supported in ROS Kinetic and newer
5 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
6 | set(CMAKE_CXX_STANDARD_REQUIRED ON)
7 | set(CMAKE_CXX_EXTENSIONS OFF)
8 | set(CMAKE_CXX_STANDARD 17)
9 | SET(CMAKE_BUILD_TYPE "Release")
10 | #SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")
11 | #SET(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")
12 | ## Find catkin macros and libraries
13 | ## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz)
14 | ## is used, also find other catkin packages
15 | find_package(catkin REQUIRED COMPONENTS
16 | cv_bridge
17 | pcl_conversions
18 | pcl_ros
19 | roscpp
20 | rospy
21 | sensor_msgs
22 | std_msgs
23 | tf_conversions
24 | )
25 |
26 | ## System dependencies are found with CMake's conventions
27 | # find_package(Boost REQUIRED COMPONENTS system)
28 | find_package(PCL REQUIRED)
29 | find_package(OpenCV REQUIRED)
30 | find_package(Threads)
31 | find_package(Ceres REQUIRED)
32 | find_package(GTSAM REQUIRED QUIET)
33 |
34 | set(CMAKE_AUTOMOC ON)
35 | set(CMAKE_AUTOUIC ON)
36 | set(CMAKE_AUTORCC ON)
37 | set(CMAKE_INCLUDE_CURRENT_DIR ON)
38 |
39 | catkin_package(
40 | CATKIN_DEPENDS roscpp rospy std_msgs
41 | )
42 |
43 | include_directories(
44 | # include
45 | ${catkin_INCLUDE_DIRS}
46 | ${PCL_INCLUDE_DIRS}
47 | ${OpenCV_INCLUDE_DIRS}
48 | )
49 |
50 | add_library(utils_lib src/utils.cpp)
51 |
52 |
53 | add_executable(btc_place_recognition example/place_recognition.cpp src/btc.cpp)
54 | target_link_libraries(btc_place_recognition
55 | ${catkin_LIBRARIES}
56 | ${PCL_LIBRARIES}
57 | ${OpenCV_LIBS}
58 | ${CERES_LIBRARIES}
59 | tbb
60 | utils_lib)
61 |
62 |
63 |
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
1 | # BTC
2 | ## BTC: A Binary and Triangle Combined Descriptor for 3D Place Recognition
3 |
4 | ## Introduction
5 | **BTC** is a novel global and local combined descriptor for 3D place recognition. To achieve viewpoint invariance, we devise a global triangle descriptor composed of three side lengths of a triangle formed by three keypoints extracted from the accumulated point cloud. The triangle descriptor inherently exhibits viewpoint invariance, as the side lengths remain unchanged regardless of viewpoint or direction. While the triangle descriptor captures the global appearance of the point cloud, to further enhance its descriptive and discriminative capabilities, we develop a local descriptor called the binary descriptor that encodes the local geometric information of each keypoint forming the triangle. This combination of global and local descriptors allows for accurate and robust place recognition, even in large-scale unstructured environments.
6 |
7 |
8 |

9 |
System overview of the Binary Triangle Combined (BTC) descriptor for place recognition in a SLAM system.
10 |
11 |
12 | ### Developers:
13 | [Chongjian Yuan 袁崇健](https://github.com/ChongjianYUAN), [Jiarong Lin 林家荣](https://github.com/ziv-lin)
14 |
15 | ### Related Paper
16 | The related paper will be available on **arxiv** soon.
17 |
18 | ### Related Video
19 | Our accompanying video is now available on **YouTube**.
20 |
21 |

22 |
23 |
24 | ## Parameters Explanation
25 |
26 | ### Binary Descriptor Parameters
27 | - `useful_corner_num`: Maximum number of useful corners.
28 | - `plane_detection_thre`: Threshold for plane detection.
29 | - `plane_merge_normal_thre`: Normal threshold for plane merging.
30 | - `plane_merge_dis_thre`: Distance threshold for plane merging.
31 | - `voxel_size`: Size of each voxel.
32 | - `voxel_init_num`: Minimum number of points in a voxel for extraction.
33 | - `proj_plane_num`: Number of projection planes.
34 | - `proj_image_resolution`: Resolution of the projected image in meters.
35 | - `proj_image_high_inc`: Height increment for the projected image in meters.
36 | - `proj_dis_min`: Minimum distance to the projection plane.
37 | - `proj_dis_max`: Maximum distance to the projection plane.
38 | - `summary_min_thre`: Minimum number of points in one image grid.
39 | - `line_filter_enable`: Enable line point filtering; turn off in environments lacking features (indoor and outdoor), otherwise enable to reduce computation
40 |
41 | ### Stable Triangle Descriptor Parameters
42 | - `descriptor_near_num`: Number of neighboring points to save for each point when generating triangles.
43 | - `descriptor_min_len`: Minimum edge length.
44 | - `descriptor_max_len`: Maximum edge length.
45 | - `max_constrait_dis`: Neighborhood range for non-maximum suppression.
46 | - `triangle_resolution`: Edge length amplification coefficient.
47 |
48 | ### Candidate Search Parameters
49 | - `skip_near_num`: Number of frames to skip.
50 | - `candidate_num`: Maximum number of reference frames.
51 | - `similarity_threshold`: BTC similarity coefficient.
52 | - `rough_dis_threshold`: Threshold coefficient for BTC edge length distance.
53 | - `normal_threshold`: Normal Threshold for plane-to-plane validation.
54 | - `dis_threshold`: Distance threshold for plane-to-plane validation.
55 | - `icp_threshold`: Threshold for plane-to-plane ICP to triggle loop.
56 |
57 | ## Example
58 |
59 | ### Place Recognition
60 | To perform place recognition using the BTC descriptor, you need to prepare the data and then launch the ROS node with the following command:
61 |
62 | 1. **Data Preparation**:
63 | - Prepare the submap LiDAR point cloud directories containing the point cloud files in either `.bin` or `.pcd` format.
64 | - File naming format should follow this convention:
65 | - For PCD files: `000000.pcd`, `000001.pcd`, ..., up to your maximum submap count.
66 | - For BIN files: similarly named as `000000.bin`, `000001.bin`, etc.
67 | - Prepare a `pose.txt` file with the following format:
68 | ```
69 | Submap_id pos_x pos_y pos_z quat_x quat_y quat_z quat_w
70 | ```
71 | where:
72 | - `Submap_id` is the identifier for the submap.
73 | - `pos_x`, `pos_y`, `pos_z` are the coordinates of the submap's position.
74 | - `quat_x`, `quat_y`, `quat_z`, `quat_w` are the quaternion components representing the orientation.
75 |
76 | 2. **Launch the Node**:
77 | Run the following command to start the place recognition process:
78 |
79 | ```bash
80 | roslaunch btc_desc place_recognition.launch
81 | ```
82 |
83 | ### More Examples
84 | More examples to be released soon:
85 | - **Multi-Session Place Recognition**
86 | - **Global Relocalization**
87 | - **Pose Graph Optimization (PGO)**
88 | - **Multi-Map Merge**
89 |
--------------------------------------------------------------------------------
/config/config_indoor.yaml:
--------------------------------------------------------------------------------
1 | %YAML:1.0
2 |
3 | # binary descriptor
4 | useful_corner_num: 500 # Maximum number of triangle vertices
5 | plane_detection_thre: 0.01 # Maximum allowed value for the minimum eigenvalue of the covariance matrix to consider as a plane
6 | plane_merge_normal_thre: 0.1 # Normal threshold for plane merging
7 | plane_merge_dis_thre: 0.3 # Distance threshold for plane merging
8 | voxel_size: 0.5 # Side length of each voxel during plane extraction
9 | voxel_init_num: 10 # Minimum number of points in the voxel for plane extraction
10 | proj_plane_num: 2 # Number of projection planes
11 | proj_image_resolution: 0.2 # In meters
12 | proj_image_high_inc: 0.1 # In meters
13 | proj_dis_min: -1.0 # Minimum distance to the projection plane
14 | proj_dis_max: 4.0 # Maximum distance to the projection plane
15 | summary_min_thre: 6 # Minimum number of points in one image grid
16 | line_filter_enable: 0 # Enable line point filtering; turn off in environments lacking features (indoor and outdoor), otherwise enable to reduce computation
17 |
18 | # std descriptor
19 | descriptor_near_num: 15 # Number of neighboring points to save for each point when generating triangles
20 | descriptor_min_len: 1 # Minimum edge length
21 | descriptor_max_len: 30 # Maximum edge length
22 | max_constrait_dis: 1 # Neighborhood range for non-maximum suppression
23 | triangle_resolution: 0.2 # Edge length amplification coefficient
24 |
25 | # candidate search
26 | skip_near_num: 100 # Number of frames to skip; set to a positive value for intra-machine loops, and -1000000 for inter-machine loops
27 | candidate_num: 50 # Maximum number of reference frames
28 | similarity_threshold: 0.7 # BTC similarity coefficient
29 | rough_dis_threshold: 0.01 # Threshold coefficient for BTC edge length distance
30 | normal_threshold: 0.2 # Threshold for plane ICP and geometric validation: point-to-plane distance
31 | dis_threshold: 0.5 # Distance threshold for point-to-plane validation
32 | icp_threshold: 0.2 # Threshold for plane-to-plane ICP to triggle loop.
33 |
--------------------------------------------------------------------------------
/config/config_outdoor.yaml:
--------------------------------------------------------------------------------
1 | %YAML:1.0
2 |
3 | # binary descriptor
4 | useful_corner_num: 500 # Maximum number of useful corners
5 | plane_detection_thre: 0.01 # Threshold for plane detection
6 | plane_merge_normal_thre: 0.1 # Normal threshold for plane merging
7 | plane_merge_dis_thre: 0.3 # Distance threshold for plane merging
8 | voxel_size: 2 # Size of each voxel
9 | voxel_init_num: 10 # Minimum number of points in a voxel for extraction
10 | proj_plane_num: 1 # Number of projection planes
11 | proj_image_resolution: 0.5 # Resolution of the projected image in meters
12 | proj_image_high_inc: 0.1 # Height increment for the projected image in meters
13 | proj_dis_min: -1.0 # Minimum distance to the projection plane
14 | proj_dis_max: 4.0 # Maximum distance to the projection plane
15 | summary_min_thre: 10 # Minimum number of points in one image grid
16 | line_filter_enable: 1 # Enable line point filtering (1: enabled, 0: disabled)
17 |
18 | # std descriptor
19 | descriptor_near_num: 10 # Number of neighboring points to save for each point when generating triangles
20 | descriptor_min_len: 2 # Minimum edge length
21 | descriptor_max_len: 50 # Maximum edge length
22 | max_constrait_dis: 2 # Neighborhood range for non-maximum suppression
23 | triangle_resolution: 0.2 # Edge length amplification coefficient
24 |
25 | # candidate search
26 | skip_near_num: 100 # Number of frames to skip
27 | candidate_num: 50 # Maximum number of reference frames
28 | similarity_threshold: 0.7 # BTC similarity coefficient
29 | rough_dis_threshold: 0.01 # Threshold coefficient for BTC edge length distance
30 | normal_threshold: 0.2 # Threshold for plane ICP and geometric validation: point-to-plane distance
31 | dis_threshold: 0.5 # Distance threshold for plane-to-plane validation.
32 | icp_threshold: 0.2 # Threshold for plane-to-plane ICP to triggle loop.
33 |
--------------------------------------------------------------------------------
/example/place_recognition.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 |
7 | #include
8 |
9 | #include "include/btc.h"
10 | #include "include/utils.h"
11 |
12 | // Read KITTI data
13 | std::vector read_lidar_data(const std::string lidar_data_path) {
14 | std::ifstream lidar_data_file;
15 | lidar_data_file.open(lidar_data_path,
16 | std::ifstream::in | std::ifstream::binary);
17 | if (!lidar_data_file) {
18 | std::cout << "Read End..." << std::endl;
19 | std::vector nan_data;
20 | return nan_data;
21 | // exit(-1);
22 | }
23 | lidar_data_file.seekg(0, std::ios::end);
24 | const size_t num_elements = lidar_data_file.tellg() / sizeof(float);
25 | lidar_data_file.seekg(0, std::ios::beg);
26 |
27 | std::vector lidar_data_buffer(num_elements);
28 | lidar_data_file.read(reinterpret_cast(&lidar_data_buffer[0]),
29 | num_elements * sizeof(float));
30 | return lidar_data_buffer;
31 | }
32 |
33 | int main(int argc, char **argv) {
34 | ros::init(argc, argv, "btc_place_recognition");
35 | ros::NodeHandle nh;
36 | std::string setting_path = "";
37 | std::string pcds_dir = "";
38 | std::string pose_file = "";
39 | std::string result_file = "";
40 | double cloud_overlap_thr = 0.5;
41 | bool calc_gt_enable = false;
42 | bool read_bin = true;
43 | nh.param("cloud_overlap_thr", cloud_overlap_thr, 0.5);
44 | nh.param("setting_path", setting_path, "");
45 | nh.param("pcds_dir", pcds_dir, "");
46 | nh.param("pose_file", pose_file, "");
47 | nh.param("read_bin", read_bin, true);
48 |
49 | ros::Publisher pubOdomAftMapped =
50 | nh.advertise("/aft_mapped_to_init", 10);
51 | ros::Publisher pubCureentCloud =
52 | nh.advertise("/cloud_current", 100);
53 | ros::Publisher pubCurrentBinary =
54 | nh.advertise("/cloud_key_points", 100);
55 | ros::Publisher pubPath =
56 | nh.advertise("descriptor_line", 10);
57 | ros::Publisher pubCurrentPose =
58 | nh.advertise("/current_pose", 10);
59 | ros::Publisher pubMatchedPose =
60 | nh.advertise("/matched_pose", 10);
61 | ros::Publisher pubMatchedCloud =
62 | nh.advertise("/cloud_matched", 100);
63 | ros::Publisher pubMatchedBinary =
64 | nh.advertise("/cloud_matched_key_points", 100);
65 | ros::Publisher pubLoopStatus =
66 | nh.advertise("/loop_status", 100);
67 | ros::Publisher pubBTC =
68 | nh.advertise("descriptor_line", 10);
69 |
70 | std_msgs::ColorRGBA color_tp;
71 | std_msgs::ColorRGBA color_fp;
72 | std_msgs::ColorRGBA color_path;
73 | double scale_tp = 4.0;
74 | double scale_fp = 5.0;
75 | double scale_path = 3.0;
76 | color_tp.a = 1.0;
77 | color_tp.r = 0.0 / 255.0;
78 | color_tp.g = 255.0 / 255.0;
79 | color_tp.b = 0.0 / 255.0;
80 |
81 | color_fp.a = 1.0;
82 | color_fp.r = 1.0;
83 | color_fp.g = 0.0;
84 | color_fp.b = 0.0;
85 |
86 | color_path.a = 0.8;
87 | color_path.r = 255.0 / 255.0;
88 | color_path.g = 255.0 / 255.0;
89 | color_path.b = 255.0 / 255.0;
90 |
91 | ros::Rate loop(50000);
92 | ros::Rate slow_loop(1000);
93 |
94 | ConfigSetting config_setting;
95 | load_config_setting(setting_path, config_setting);
96 |
97 | // pose is only for visulization and gt overlap calculation
98 | std::vector> pose_list;
99 | std::vector time_list;
100 | load_evo_pose_with_time(pose_file, pose_list, time_list);
101 | std::string print_msg = "Successfully load pose file:" + pose_file +
102 | ". pose size:" + std::to_string(time_list.size());
103 | ROS_INFO_STREAM(print_msg.c_str());
104 |
105 | BtcDescManager *btc_manager = new BtcDescManager(config_setting);
106 | btc_manager->print_debug_info_ = false;
107 | pcl::PointCloud::Ptr temp_cloud(
108 | new pcl::PointCloud());
109 |
110 | std::vector descriptor_time;
111 | std::vector querying_time;
112 | std::vector update_time;
113 | int triggle_loop_num = 0;
114 | int true_loop_num = 0;
115 | bool finish = false;
116 |
117 | pcl::PCDReader reader;
118 |
119 | while (ros::ok() && !finish) {
120 | for (size_t submap_id = 0; submap_id < pose_list.size(); ++submap_id) {
121 | pcl::PointCloud::Ptr cloud(
122 | new pcl::PointCloud());
123 | pcl::PointCloud transform_cloud;
124 | cloud->reserve(1000000);
125 | transform_cloud.reserve(1000000);
126 | // Get pose information, only for gt overlap calculation
127 | Eigen::Vector3d translation = pose_list[submap_id].first;
128 | Eigen::Matrix3d rotation = pose_list[submap_id].second;
129 | if (read_bin) {
130 | std::stringstream ss;
131 | ss << pcds_dir << "/" << std::setfill('0') << std::setw(6) << submap_id
132 | << ".bin";
133 | std::string pcd_file = ss.str();
134 | auto t_load_start = std::chrono::high_resolution_clock::now();
135 | std::vector lidar_data = read_lidar_data(ss.str());
136 | if (lidar_data.size() == 0) {
137 | break;
138 | }
139 |
140 | for (std::size_t i = 0; i < lidar_data.size(); i += 4) {
141 | pcl::PointXYZI point;
142 | point.x = lidar_data[i];
143 | point.y = lidar_data[i + 1];
144 | point.z = lidar_data[i + 2];
145 | point.intensity = lidar_data[i + 3];
146 | Eigen::Vector3d pv = point2vec(point);
147 | pv = rotation * pv + translation;
148 | cloud->points.push_back(point);
149 | point = vec2point(pv);
150 | transform_cloud.points.push_back(vec2point(pv));
151 | }
152 | auto t_load_end = std::chrono::high_resolution_clock::now();
153 | std::cout << "[Time] load cloud: " << time_inc(t_load_end, t_load_start)
154 | << "ms, " << std::endl;
155 | } else {
156 | // Load point cloud from pcd file
157 | std::stringstream ss;
158 | ss << pcds_dir << "/" << std::setfill('0') << std::setw(6) << submap_id
159 | << ".pcd";
160 | std::string pcd_file = ss.str();
161 |
162 | auto t_load_start = std::chrono::high_resolution_clock::now();
163 | // pcl::io::loadPCDFile(pcd_file, *cloud)
164 | if (reader.read(pcd_file, *cloud) == -1) {
165 | ROS_ERROR_STREAM("Couldn't read file " << pcd_file);
166 | continue;
167 | }
168 | auto t_load_end = std::chrono::high_resolution_clock::now();
169 | std::cout << "[Time] load cloud: " << time_inc(t_load_end, t_load_start)
170 | << "ms, " << std::endl;
171 | transform_cloud = *cloud;
172 |
173 | for (size_t j = 0; j < transform_cloud.size(); j++) {
174 | Eigen::Vector3d pv = point2vec(transform_cloud.points[j]);
175 | pv = rotation * pv + translation;
176 | transform_cloud.points[j] = vec2point(pv);
177 | }
178 | }
179 |
180 | // step1. Descriptor Extraction
181 | std::cout << "[Description] submap id:" << submap_id << std::endl;
182 | auto t_descriptor_begin = std::chrono::high_resolution_clock::now();
183 | std::vector btcs_vec;
184 | btc_manager->GenerateBtcDescs(transform_cloud.makeShared(), submap_id,
185 | btcs_vec);
186 | auto t_descriptor_end = std::chrono::high_resolution_clock::now();
187 | descriptor_time.push_back(time_inc(t_descriptor_end, t_descriptor_begin));
188 | // step2. Searching Loop
189 | auto t_query_begin = std::chrono::high_resolution_clock::now();
190 | std::pair search_result(-1, 0);
191 | std::pair loop_transform;
192 | loop_transform.first << 0, 0, 0;
193 | loop_transform.second = Eigen::Matrix3d::Identity();
194 | std::vector> loop_std_pair;
195 | if (submap_id > config_setting.skip_near_num_) {
196 | if (btcs_vec.size() == 0) {
197 | // getchar();
198 | }
199 | btc_manager->SearchLoop(btcs_vec, search_result, loop_transform,
200 | loop_std_pair);
201 | }
202 | if (search_result.first > 0) {
203 | std::cout << "[Loop Detection] triggle loop: " << submap_id << "--"
204 | << search_result.first << ", score:" << search_result.second
205 | << std::endl;
206 | }
207 | auto t_query_end = std::chrono::high_resolution_clock::now();
208 | querying_time.push_back(time_inc(t_query_end, t_query_begin));
209 |
210 | // step3. Add descriptors to the database
211 | auto t_map_update_begin = std::chrono::high_resolution_clock::now();
212 | btc_manager->AddBtcDescs(btcs_vec);
213 | auto t_map_update_end = std::chrono::high_resolution_clock::now();
214 | update_time.push_back(time_inc(t_map_update_end, t_map_update_begin));
215 | std::cout << "[Time] descriptor extraction: "
216 | << time_inc(t_descriptor_end, t_descriptor_begin) << "ms, "
217 | << "query: " << time_inc(t_query_end, t_query_begin) << "ms, "
218 | << "update map:"
219 | << time_inc(t_map_update_end, t_map_update_begin) << "ms"
220 | << std::endl;
221 | std::cout << std::endl;
222 |
223 | // down sample to save memory
224 | down_sampling_voxel(transform_cloud, 0.5);
225 | btc_manager->key_cloud_vec_.push_back(transform_cloud.makeShared());
226 |
227 | // visuliazaion
228 | sensor_msgs::PointCloud2 pub_cloud;
229 | pcl::toROSMsg(transform_cloud, pub_cloud);
230 | pub_cloud.header.frame_id = "camera_init";
231 | pubCureentCloud.publish(pub_cloud);
232 |
233 | pcl::PointCloud key_points_cloud;
234 | for (auto var : btc_manager->history_binary_list_.back()) {
235 | pcl::PointXYZ pi;
236 | pi.x = var.location_[0];
237 | pi.y = var.location_[1];
238 | pi.z = var.location_[2];
239 | key_points_cloud.push_back(pi);
240 | }
241 | pcl::toROSMsg(key_points_cloud, pub_cloud);
242 | pub_cloud.header.frame_id = "camera_init";
243 | pubCurrentBinary.publish(pub_cloud);
244 |
245 | visualization_msgs::MarkerArray marker_array;
246 | visualization_msgs::Marker marker;
247 | marker.header.frame_id = "camera_init";
248 | marker.ns = "colored_path";
249 | marker.id = submap_id;
250 | marker.type = visualization_msgs::Marker::LINE_LIST;
251 | marker.action = visualization_msgs::Marker::ADD;
252 | marker.pose.orientation.w = 1.0;
253 | if (search_result.first >= 0) {
254 | triggle_loop_num++;
255 | Eigen::Matrix4d transform1 = Eigen::Matrix4d::Identity();
256 | Eigen::Matrix4d transform2 = Eigen::Matrix4d::Identity();
257 | publish_std(loop_std_pair, transform1, transform2, pubBTC);
258 | slow_loop.sleep();
259 | double cloud_overlap =
260 | calc_overlap(transform_cloud.makeShared(),
261 | btc_manager->key_cloud_vec_[search_result.first], 0.5);
262 | pcl::PointCloud match_key_points_cloud;
263 | for (auto var :
264 | btc_manager->history_binary_list_[search_result.first]) {
265 | pcl::PointXYZ pi;
266 | pi.x = var.location_[0];
267 | pi.y = var.location_[1];
268 | pi.z = var.location_[2];
269 | match_key_points_cloud.push_back(pi);
270 | }
271 | pcl::toROSMsg(match_key_points_cloud, pub_cloud);
272 | pub_cloud.header.frame_id = "camera_init";
273 | pubMatchedBinary.publish(pub_cloud);
274 | // true positive
275 | if (cloud_overlap >= cloud_overlap_thr) {
276 | true_loop_num++;
277 | pcl::PointCloud matched_cloud;
278 | matched_cloud.resize(
279 | btc_manager->key_cloud_vec_[search_result.first]->size());
280 | for (size_t i = 0;
281 | i < btc_manager->key_cloud_vec_[search_result.first]->size();
282 | i++) {
283 | pcl::PointXYZRGB pi;
284 | pi.x =
285 | btc_manager->key_cloud_vec_[search_result.first]->points[i].x;
286 | pi.y =
287 | btc_manager->key_cloud_vec_[search_result.first]->points[i].y;
288 | pi.z =
289 | btc_manager->key_cloud_vec_[search_result.first]->points[i].z;
290 | pi.r = 0;
291 | pi.g = 255;
292 | pi.b = 0;
293 | matched_cloud.points[i] = pi;
294 | }
295 | pcl::toROSMsg(matched_cloud, pub_cloud);
296 | pub_cloud.header.frame_id = "camera_init";
297 | pubMatchedCloud.publish(pub_cloud);
298 | slow_loop.sleep();
299 |
300 | marker.scale.x = scale_tp;
301 | marker.color = color_tp;
302 | geometry_msgs::Point point1;
303 | point1.x = pose_list[submap_id - 1].first[0];
304 | point1.y = pose_list[submap_id - 1].first[1];
305 | point1.z = pose_list[submap_id - 1].first[2];
306 | geometry_msgs::Point point2;
307 | point2.x = pose_list[submap_id].first[0];
308 | point2.y = pose_list[submap_id].first[1];
309 | point2.z = pose_list[submap_id].first[2];
310 | marker.points.push_back(point1);
311 | marker.points.push_back(point2);
312 |
313 | } else {
314 | pcl::PointCloud matched_cloud;
315 | matched_cloud.resize(
316 | btc_manager->key_cloud_vec_[search_result.first]->size());
317 | for (size_t i = 0;
318 | i < btc_manager->key_cloud_vec_[search_result.first]->size();
319 | i++) {
320 | pcl::PointXYZRGB pi;
321 | pi.x =
322 | btc_manager->key_cloud_vec_[search_result.first]->points[i].x;
323 | pi.y =
324 | btc_manager->key_cloud_vec_[search_result.first]->points[i].y;
325 | pi.z =
326 | btc_manager->key_cloud_vec_[search_result.first]->points[i].z;
327 | pi.r = 255;
328 | pi.g = 0;
329 | pi.b = 0;
330 | matched_cloud.points[i] = pi;
331 | }
332 | pcl::toROSMsg(matched_cloud, pub_cloud);
333 | pub_cloud.header.frame_id = "camera_init";
334 | pubMatchedCloud.publish(pub_cloud);
335 | slow_loop.sleep();
336 | marker.scale.x = scale_fp;
337 | marker.color = color_fp;
338 | geometry_msgs::Point point1;
339 | point1.x = pose_list[submap_id - 1].first[0];
340 | point1.y = pose_list[submap_id - 1].first[1];
341 | point1.z = pose_list[submap_id - 1].first[2];
342 | geometry_msgs::Point point2;
343 | point2.x = pose_list[submap_id].first[0];
344 | point2.y = pose_list[submap_id].first[1];
345 | point2.z = pose_list[submap_id].first[2];
346 | marker.points.push_back(point1);
347 | marker.points.push_back(point2);
348 | }
349 |
350 | } else {
351 | if (submap_id > 0) {
352 | marker.scale.x = scale_path;
353 | marker.color = color_path;
354 | geometry_msgs::Point point1;
355 | point1.x = pose_list[submap_id - 1].first[0];
356 | point1.y = pose_list[submap_id - 1].first[1];
357 | point1.z = pose_list[submap_id - 1].first[2];
358 | geometry_msgs::Point point2;
359 | point2.x = pose_list[submap_id].first[0];
360 | point2.y = pose_list[submap_id].first[1];
361 | point2.z = pose_list[submap_id].first[2];
362 | marker.points.push_back(point1);
363 | marker.points.push_back(point2);
364 | }
365 | }
366 | marker_array.markers.push_back(marker);
367 | pubLoopStatus.publish(marker_array);
368 | loop.sleep();
369 | }
370 | finish = true;
371 | }
372 |
373 | double mean_descriptor_time =
374 | std::accumulate(descriptor_time.begin(), descriptor_time.end(), 0) * 1.0 /
375 | descriptor_time.size();
376 | double mean_query_time =
377 | std::accumulate(querying_time.begin(), querying_time.end(), 0) * 1.0 /
378 | querying_time.size();
379 | double mean_update_time =
380 | std::accumulate(update_time.begin(), update_time.end(), 0) * 1.0 /
381 | update_time.size();
382 | std::cout << "Total submap number:" << pose_list.size()
383 | << ", triggle loop number:" << triggle_loop_num
384 | << ", true loop number:" << true_loop_num << std::endl;
385 | std::cout << "Mean time for descriptor extraction: " << mean_descriptor_time
386 | << "ms, query: " << mean_query_time
387 | << "ms, update: " << mean_update_time << "ms, total: "
388 | << mean_descriptor_time + mean_query_time + mean_update_time << "ms"
389 | << std::endl;
390 | return 0;
391 | }
--------------------------------------------------------------------------------
/figs/system_pipeline_btc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hku-mars/btc_descriptor/742af157036144edad9a8350330c0158ea1a40d5/figs/system_pipeline_btc.png
--------------------------------------------------------------------------------
/figs/video_cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hku-mars/btc_descriptor/742af157036144edad9a8350330c0158ea1a40d5/figs/video_cover.jpg
--------------------------------------------------------------------------------
/include/CustomMsg.h:
--------------------------------------------------------------------------------
1 | // Generated by gencpp from file livox_ros_driver/CustomMsg.msg
2 | // DO NOT EDIT!
3 |
4 |
5 | #ifndef LIVOX_ROS_DRIVER_MESSAGE_CUSTOMMSG_H
6 | #define LIVOX_ROS_DRIVER_MESSAGE_CUSTOMMSG_H
7 |
8 |
9 | #include
10 | #include
11 | #include