├── CMakeLists.txt ├── README.md ├── docker ├── Dockerfile ├── Makefile └── run.sh ├── include ├── aloam_velodyne │ ├── common.h │ └── tic_toc.h └── scancontext │ ├── KDTreeVectorOfVectorsAdaptor.h │ ├── Scancontext.cpp │ ├── Scancontext.h │ └── nanoflann.hpp ├── launch ├── aloam_mulran.launch ├── aloam_velodyne_HDL_32.launch ├── aloam_velodyne_HDL_64.launch ├── aloam_velodyne_VLP_16.launch └── kitti_helper.launch ├── package.xml ├── picture ├── anypipe.png ├── indoor.png ├── kaist-03-1.png ├── kaist-03-2.png ├── kaist-03-3.png ├── kaist-03-4.png ├── kaist-03-5.png ├── kaist-03-merged.png ├── kitti.png ├── kitti05.png ├── kitti_gif.gif ├── riverside01.png └── scfastlio.png ├── rviz_cfg └── aloam_velodyne.rviz ├── src ├── kittiHelper.cpp ├── laserMapping.cpp ├── laserOdometry.cpp ├── laserPosegraphOptimization.cpp ├── lidarFactor.hpp └── scanRegistration.cpp └── utils ├── python ├── __pycache__ │ └── pypcdMyUtils.cpython-36.pyc ├── bone_table.npy ├── jet_table.npy ├── makeMergedMap.py └── pypcdMyUtils.py └── sample_data ├── KAIST03 ├── Scans │ ├── 000000.pcd │ ├── 000001.pcd │ ├── 000002.pcd │ ├── 000003.pcd │ ├── 000004.pcd │ ├── 000005.pcd │ ├── 000006.pcd │ ├── 000007.pcd │ ├── 000008.pcd │ ├── 000009.pcd │ ├── 000010.pcd │ ├── 000011.pcd │ ├── 000012.pcd │ ├── 000013.pcd │ ├── 000014.pcd │ ├── 000015.pcd │ ├── 000016.pcd │ ├── 000017.pcd │ ├── 000018.pcd │ ├── 000019.pcd │ └── 000020.pcd ├── optimized_poses.txt └── times.txt └── Seosan01 ├── Scans ├── 000000.pcd ├── 000001.pcd ├── 000002.pcd ├── 000003.pcd ├── 000004.pcd ├── 000005.pcd ├── 000006.pcd ├── 000007.pcd ├── 000008.pcd ├── 000009.pcd ├── 000010.pcd ├── 000011.pcd ├── 000012.pcd ├── 000013.pcd ├── 000014.pcd ├── 000015.pcd ├── 000016.pcd ├── 000017.pcd ├── 000018.pcd ├── 000019.pcd └── 000020.pcd ├── optimized_poses.txt └── times.txt /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.3) 2 | project(aloam_velodyne) 3 | 4 | set(CMAKE_BUILD_TYPE "Release") 5 | # set(CMAKE_CXX_FLAGS "-std=c++11") 6 | set(CMAKE_CXX_STANDARD 17) 7 | set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Wall -g") 8 | 9 | find_package(catkin REQUIRED COMPONENTS 10 | geometry_msgs 11 | nav_msgs 12 | sensor_msgs 13 | roscpp 14 | rospy 15 | rosbag 16 | std_msgs 17 | image_transport 18 | cv_bridge 19 | tf 20 | ) 21 | 22 | #find_package(Eigen3 REQUIRED) 23 | find_package(PCL REQUIRED) 24 | find_package(OpenCV REQUIRED) 25 | find_package(Ceres REQUIRED) 26 | 27 | find_package(OpenMP REQUIRED) 28 | find_package(GTSAM REQUIRED QUIET) 29 | 30 | include_directories( 31 | include 32 | ${catkin_INCLUDE_DIRS} 33 | ${PCL_INCLUDE_DIRS} 34 | ${CERES_INCLUDE_DIRS} 35 | ${OpenCV_INCLUDE_DIRS} 36 | ${GTSAM_INCLUDE_DIR} 37 | ) 38 | 39 | catkin_package( 40 | CATKIN_DEPENDS geometry_msgs nav_msgs roscpp rospy std_msgs 41 | DEPENDS EIGEN3 PCL 42 | INCLUDE_DIRS include 43 | ) 44 | 45 | 46 | add_executable(ascanRegistration src/scanRegistration.cpp) 47 | target_link_libraries(ascanRegistration ${catkin_LIBRARIES} ${PCL_LIBRARIES}) 48 | 49 | add_executable(alaserOdometry src/laserOdometry.cpp) 50 | target_link_libraries(alaserOdometry ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${CERES_LIBRARIES}) 51 | 52 | add_executable(alaserMapping src/laserMapping.cpp) 53 | target_link_libraries(alaserMapping ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${CERES_LIBRARIES}) 54 | 55 | add_executable(alaserPGO 56 | src/laserPosegraphOptimization.cpp 57 | include/scancontext/Scancontext.cpp 58 | ) 59 | target_compile_options(alaserPGO 60 | PRIVATE ${OpenMP_CXX_FLAGS} 61 | ) 62 | target_link_libraries(alaserPGO 63 | ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${CERES_LIBRARIES} 64 | ${OpenMP_CXX_FLAGS} 65 | gtsam 66 | ) 67 | 68 | add_executable(kittiHelper src/kittiHelper.cpp) 69 | target_link_libraries(kittiHelper ${catkin_LIBRARIES} ${PCL_LIBRARIES} ${OpenCV_LIBS}) 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SC-A-LOAM 2 | 3 | ## News 4 | - ``2021-07-16``: This repository's easy-to-use plug-and-play loop detection and pose graph optimization module (named [SC-PGO](https://github.com/gisbi-kim/SC-A-LOAM/blob/main/src/laserPosegraphOptimization.cpp)) is also integrated with FAST-LIO2! see [FAST_LIO_SLAM](https://github.com/gisbi-kim/FAST_LIO_SLAM). 5 | 6 | ## What is SC-A-LOAM? 7 | - A real-time LiDAR SLAM package that integrates A-LOAM and ScanContext. 8 | - **A-LOAM** for odometry (i.e., consecutive motion estimation) 9 | - **ScanContext** for coarse global localization that can deal with big drifts (i.e., place recognition as kidnapped robot problem without initial pose) 10 | - and iSAM2 of GTSAM is used for pose-graph optimization. 11 | - This package aims to show ScanContext's handy applicability. 12 | - The only things a user should do is just to include `Scancontext.h`, call `makeAndSaveScancontextAndKeys` and `detectLoopClosureID`. 13 | 14 | ## Features 15 | 1. A strong place recognition and loop closing 16 | - We integrated ScanContext as a loop detector into A-LOAM, and ISAM2-based pose-graph optimization is followed. (see https://youtu.be/okML_zNadhY?t=313 to enjoy the drift-closing moment) 17 | 2. A modular implementation 18 | - The only difference from A-LOAM is the addition of the `laserPosegraphOptimization.cpp` file. In the new file, we subscribe the point cloud topic and odometry topic (as a result of A-LOAM, published from `laserMapping.cpp`). That is, our implementation is generic to any front-end odometry methods. Thus, our pose-graph optimization module (i.e., `laserPosegraphOptimization.cpp`) can easily be integrated with any odometry algorithms such as non-LOAM family or even other sensors (e.g., visual odometry). 19 | -

20 | 3. (optional) Altitude stabilization using consumer-level GPS 21 | - To make a result more trustworthy, we supports GPS (consumer-level price, such as U-Blox EVK-7P)-based altitude stabilization. The LOAM family of methods are known to be susceptible to z-errors in outdoors. We used the robust loss for only the altitude term. For the details, see the variable `robustGPSNoise` in the `laserPosegraphOptimization.cpp` file. 22 | 23 | ## Prerequisites (dependencies) 24 | - We mainly depend on ROS, Ceres (for A-LOAM), and GTSAM (for pose-graph optimization). 25 | - For the details to install the prerequisites, please follow the A-LOAM and LIO-SAM repositiory. 26 | - The below examples are done under ROS melodic (ubuntu 18) and GTSAM version 4.x. 27 | 28 | ## How to use? 29 | - First, install the abovementioned dependencies, and follow below lines. 30 | ``` 31 | mkdir -p ~/catkin_scaloam_ws/src 32 | cd ~/catkin_scaloam_ws/src 33 | git clone https://github.com/gisbi-kim/SC-A-LOAM.git 34 | cd ../ 35 | catkin_make 36 | source ~/catkin_scaloam_ws/devel/setup.bash 37 | roslaunch aloam_velodyne aloam_mulran.launch # for MulRan dataset setting 38 | ``` 39 | 40 | ## Example Results 41 | 42 | ### Riverside 01, MulRan dataset 43 | - The MulRan dataset provides lidar scans (Ouster OS1-64, horizontally mounted, 10Hz) and consumer level gps (U-Blox EVK-7P, 4Hz) data. 44 | - About how to use (publishing data) data: see here https://github.com/irapkaist/file_player_mulran 45 | - example videos on Riverside 01 sequence. 46 | 1. with consumer level GPS-based altitude stabilization: https://youtu.be/FwAVX5TVm04 47 | 2. without the z stabilization: https://youtu.be/okML_zNadhY 48 | - example result: 49 | 50 |

51 | 52 | ### KITTI 05 53 | - For KITTI (HDL-64 sensor), run using the command 54 | ``` 55 | roslaunch aloam_velodyne aloam_velodyne_HDL_64.launch # for KITTI dataset setting 56 | ``` 57 | - To publish KITTI scans, you can use mini-kitti publisher, a simple python script: https://github.com/gisbi-kim/mini-kitti-publisher 58 | - example video (no GPS used here): https://youtu.be/hk3Xx8SKkv4 59 | - example result: 60 | 61 |

62 | 63 | ### Indoor 64 | - ScanContext also works at indoor environments (use smaller sc_max_radius value). 65 | - example video: https://youtu.be/Uv6_BRmxJho 66 | - example result: 67 |

68 | 69 | ### For Livox LiDAR 70 | - Scan Context also works for Livox LiDAR data 71 | - In this example, Scan Context is integrated with FAST-LIO (https://github.com/hku-mars/FAST_LIO). 72 | - Note: No additional integration effort is required. A user just run seperately FAST-LIO node and SC-A-LOAM's posegraphoptimization.cpp node! 73 | - example video (tutoial and results): https://youtu.be/Fw9S6D6HozA 74 | - example result: 75 |

76 | 77 | ### For Navtech Radar 78 | - Scan Context also works for Navtech Radar data! 79 | - For the details, please see 80 | - https://github.com/gisbi-kim/navtech-radar-slam 81 | - used the pose-graph optimization node of this repository (SC-A-LOAM) 82 | - [example video](https://www.youtube.com/watch?v=avtIQ8fesgU&t=128s) 83 | 84 | ## Utilities 85 | 86 | ### Data saver and Map construction 87 | - Similar to the [SC-LIO-SAM's saver utility](https://github.com/gisbi-kim/SC-LIO-SAM#applications), we support pose and scan saver per keyframes. Using these saved data, the map (within ROI) can be constructed offline. See the `utils/python/makeMergedMap.py` and [this tutorial](https://youtu.be/jmR3DH_A4Co). 88 | - Below is the example results of MulRan dataset KAIST 03's merged map, visualized using CloudCompare ([download the map data here](https://www.dropbox.com/sh/96jrpx3x6hh316j/AACb07kGbocnQWMIpksmU6MQa?dl=0)). 89 | 90 |

91 | 92 | - A user also can remove dynamic points using these saved keyframe poses and scans. See [this tutorial](https://www.youtube.com/watch?v=UiYYrPMcIRU) and our [Removert project](https://github.com/irapkaist/removert). 93 | 94 | ## Acknowledgements 95 | - Thanks to LOAM, A-LOAM, and LIO-SAM code authors. The major codes in this repository are borrowed from their efforts. 96 | 97 | ## Maintainer 98 | - please contact me through `paulgkim@kaist.ac.kr` 99 | 100 | ## TODO 101 | - Delayed RS loop closings 102 | - SLAM with multi-session localization 103 | - More examples on other datasets (KITTI, complex urban dataset, etc.) 104 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ros:kinetic-perception 2 | 3 | ENV CERES_VERSION="1.12.0" 4 | ENV PCL_VERSION="1.8.0" 5 | ENV CATKIN_WS=/root/catkin_ws 6 | 7 | # setup processors number used to compile library 8 | RUN if [ "x$(nproc)" = "x1" ] ; then export USE_PROC=1 ; else export USE_PROC=$(($(nproc)/2)) ; fi && \ 9 | # Install dependencies 10 | apt-get update && apt-get install -y \ 11 | cmake \ 12 | libatlas-base-dev \ 13 | libeigen3-dev \ 14 | libgoogle-glog-dev \ 15 | libsuitesparse-dev \ 16 | python-catkin-tools \ 17 | ros-${ROS_DISTRO}-cv-bridge \ 18 | ros-${ROS_DISTRO}-image-transport \ 19 | ros-${ROS_DISTRO}-message-filters \ 20 | ros-${ROS_DISTRO}-tf && \ 21 | rm -rf /var/lib/apt/lists/* && \ 22 | # Build and install Ceres 23 | git clone https://ceres-solver.googlesource.com/ceres-solver && \ 24 | cd ceres-solver && \ 25 | git checkout tags/${CERES_VERSION} && \ 26 | mkdir build && cd build && \ 27 | cmake .. && \ 28 | make -j${USE_PROC} install && \ 29 | cd ../.. && \ 30 | rm -rf ./ceres-solver && \ 31 | # Build and install pcl 32 | git clone https://github.com/PointCloudLibrary/pcl.git && \ 33 | cd pcl && \ 34 | git checkout tags/pcl-${PCL_VERSION} && \ 35 | mkdir build && cd build && \ 36 | cmake .. && \ 37 | make -j${USE_PROC} install && \ 38 | cd ../.. && \ 39 | rm -rf ./pcl && \ 40 | # Setup catkin workspace 41 | mkdir -p $CATKIN_WS/src/A-LOAM/ 42 | 43 | # WORKDIR $CATKIN_WS/src 44 | 45 | # Copy A-LOAM 46 | COPY ./ $CATKIN_WS/src/A-LOAM/ 47 | # use the following line if you only have this dockerfile 48 | # RUN git clone https://github.com/HKUST-Aerial-Robotics/A-LOAM.git 49 | 50 | # Build A-LOAM 51 | WORKDIR $CATKIN_WS 52 | ENV TERM xterm 53 | ENV PYTHONIOENCODING UTF-8 54 | RUN catkin config \ 55 | --extend /opt/ros/$ROS_DISTRO \ 56 | --cmake-args \ 57 | -DCMAKE_BUILD_TYPE=Release && \ 58 | catkin build && \ 59 | sed -i '/exec "$@"/i \ 60 | source "/root/catkin_ws/devel/setup.bash"' /ros_entrypoint.sh 61 | -------------------------------------------------------------------------------- /docker/Makefile: -------------------------------------------------------------------------------- 1 | all: help 2 | 3 | help: 4 | @echo "" 5 | @echo "-- Help Menu" 6 | @echo "" 7 | @echo " 1. make build - build all images" 8 | # @echo " 1. make pull - pull all images" 9 | @echo " 1. make clean - remove all images" 10 | @echo "" 11 | 12 | build: 13 | @docker build --tag ros:aloam -f ./Dockerfile .. 14 | 15 | clean: 16 | @docker rmi -f ros:aloam 17 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | trap : SIGTERM SIGINT 3 | 4 | function abspath() { 5 | # generate absolute path from relative path 6 | # $1 : relative filename 7 | # return : absolute path 8 | if [ -d "$1" ]; then 9 | # dir 10 | (cd "$1"; pwd) 11 | elif [ -f "$1" ]; then 12 | # file 13 | if [[ $1 = /* ]]; then 14 | echo "$1" 15 | elif [[ $1 == */* ]]; then 16 | echo "$(cd "${1%/*}"; pwd)/${1##*/}" 17 | else 18 | echo "$(pwd)/$1" 19 | fi 20 | fi 21 | } 22 | 23 | if [ "$#" -ne 1 ]; then 24 | echo "Usage: $0 LIDAR_SCAN_NUMBER" >&2 25 | exit 1 26 | fi 27 | 28 | roscore & 29 | ROSCORE_PID=$! 30 | sleep 1 31 | 32 | rviz -d ../rviz_cfg/aloam_velodyne.rviz & 33 | RVIZ_PID=$! 34 | 35 | A_LOAM_DIR=$(abspath "..") 36 | 37 | if [ "$1" -eq 16 ]; then 38 | docker run \ 39 | -it \ 40 | --rm \ 41 | --net=host \ 42 | -v ${A_LOAM_DIR}:/root/catkin_ws/src/A-LOAM/ \ 43 | ros:aloam \ 44 | /bin/bash -c \ 45 | "cd /root/catkin_ws/; \ 46 | catkin config \ 47 | --cmake-args \ 48 | -DCMAKE_BUILD_TYPE=Release; \ 49 | catkin build; \ 50 | source devel/setup.bash; \ 51 | roslaunch aloam_velodyne aloam_velodyne_VLP_16.launch rviz:=false" 52 | elif [ "$1" -eq "32" ]; then 53 | docker run \ 54 | -it \ 55 | --rm \ 56 | --net=host \ 57 | -v ${A_LOAM_DIR}:/root/catkin_ws/src/A-LOAM/ \ 58 | ros:aloam \ 59 | /bin/bash -c \ 60 | "cd /root/catkin_ws/; \ 61 | catkin config \ 62 | --cmake-args \ 63 | -DCMAKE_BUILD_TYPE=Release; \ 64 | catkin build; \ 65 | source devel/setup.bash; \ 66 | roslaunch aloam_velodyne aloam_velodyne_HDL_32.launch rviz:=false" 67 | elif [ "$1" -eq "64" ]; then 68 | docker run \ 69 | -it \ 70 | --rm \ 71 | --net=host \ 72 | -v ${A_LOAM_DIR}:/root/catkin_ws/src/A-LOAM/ \ 73 | ros:aloam \ 74 | /bin/bash -c \ 75 | "cd /root/catkin_ws/; \ 76 | catkin config \ 77 | --cmake-args \ 78 | -DCMAKE_BUILD_TYPE=Release; \ 79 | catkin build; \ 80 | source devel/setup.bash; \ 81 | roslaunch aloam_velodyne aloam_velodyne_HDL_64.launch rviz:=false" 82 | fi 83 | 84 | wait $ROSCORE_PID 85 | wait $RVIZ_PID 86 | 87 | if [[ $? -gt 128 ]] 88 | then 89 | kill $ROSCORE_PID 90 | kill $RVIZ_PID 91 | fi -------------------------------------------------------------------------------- /include/aloam_velodyne/common.h: -------------------------------------------------------------------------------- 1 | // This is an advanced implementation of the algorithm described in the following paper: 2 | // J. Zhang and S. Singh. LOAM: Lidar Odometry and Mapping in Real-time. 3 | // Robotics: Science and Systems Conference (RSS). Berkeley, CA, July 2014. 4 | 5 | // Modifier: Tong Qin qintonguav@gmail.com 6 | // Shaozu Cao saozu.cao@connect.ust.hk 7 | 8 | 9 | // Copyright 2013, Ji Zhang, Carnegie Mellon University 10 | // Further contributions copyright (c) 2016, Southwest Research Institute 11 | // All rights reserved. 12 | // 13 | // Redistribution and use in source and binary forms, with or without 14 | // modification, are permitted provided that the following conditions are met: 15 | // 16 | // 1. Redistributions of source code must retain the above copyright notice, 17 | // this list of conditions and the following disclaimer. 18 | // 2. Redistributions in binary form must reproduce the above copyright notice, 19 | // this list of conditions and the following disclaimer in the documentation 20 | // and/or other materials provided with the distribution. 21 | // 3. Neither the name of the copyright holder nor the names of its 22 | // contributors may be used to endorse or promote products derived from this 23 | // software without specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 26 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 27 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 28 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 29 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 30 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 31 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 32 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 33 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 34 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 35 | // POSSIBILITY OF SUCH DAMAGE. 36 | 37 | #pragma once 38 | 39 | #include 40 | 41 | #include 42 | 43 | typedef pcl::PointXYZI PointType; 44 | 45 | inline double rad2deg(double radians) 46 | { 47 | return radians * 180.0 / M_PI; 48 | } 49 | 50 | inline double deg2rad(double degrees) 51 | { 52 | return degrees * M_PI / 180.0; 53 | } 54 | 55 | struct Pose6D { 56 | double x; 57 | double y; 58 | double z; 59 | double roll; 60 | double pitch; 61 | double yaw; 62 | }; -------------------------------------------------------------------------------- /include/aloam_velodyne/tic_toc.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 | 10 | class TicToc 11 | { 12 | public: 13 | TicToc() 14 | { 15 | tic(); 16 | } 17 | 18 | void tic() 19 | { 20 | start = std::chrono::system_clock::now(); 21 | } 22 | 23 | double toc() 24 | { 25 | end = std::chrono::system_clock::now(); 26 | std::chrono::duration elapsed_seconds = end - start; 27 | return elapsed_seconds.count() * 1000; 28 | } 29 | 30 | private: 31 | std::chrono::time_point start, end; 32 | }; 33 | 34 | class TicTocV2 35 | { 36 | public: 37 | TicTocV2() 38 | { 39 | tic(); 40 | } 41 | 42 | TicTocV2( bool _disp ) 43 | { 44 | disp_ = _disp; 45 | tic(); 46 | } 47 | 48 | void tic() 49 | { 50 | start = std::chrono::system_clock::now(); 51 | } 52 | 53 | void toc( std::string _about_task ) 54 | { 55 | end = std::chrono::system_clock::now(); 56 | std::chrono::duration elapsed_seconds = end - start; 57 | double elapsed_ms = elapsed_seconds.count() * 1000; 58 | 59 | if( disp_ ) 60 | { 61 | std::cout.precision(3); // 10 for sec, 3 for ms 62 | std::cout << _about_task << ": " << elapsed_ms << " msec." << std::endl; 63 | } 64 | } 65 | 66 | private: 67 | std::chrono::time_point start, end; 68 | bool disp_ = false; 69 | }; -------------------------------------------------------------------------------- /include/scancontext/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 "scancontext/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 | -------------------------------------------------------------------------------- /include/scancontext/Scancontext.cpp: -------------------------------------------------------------------------------- 1 | #include "scancontext/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 | TicTocV2 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 | void SCManager::setSCdistThres(double _new_thres) 263 | { 264 | SC_DIST_THRES = _new_thres; 265 | } // SCManager::setThres 266 | 267 | void SCManager::setMaximumRadius(double _max_r) 268 | { 269 | PC_MAX_RADIUS = _max_r; 270 | } // SCManager::setMaximumRadius 271 | 272 | std::pair SCManager::detectLoopClosureIDBetweenSession (std::vector& _curr_key, Eigen::MatrixXd& _curr_desc) 273 | { 274 | int loop_id { -1 }; // init with -1, -1 means no loop (== LeGO-LOAM's variable "closestHistoryFrameID") 275 | 276 | auto& curr_key = _curr_key; 277 | auto& curr_desc = _curr_desc; // current observation (query) 278 | 279 | // step 0: if first, construct the tree in batch 280 | if( ! is_tree_batch_made ) // run only once 281 | { 282 | polarcontext_invkeys_to_search_.clear(); 283 | polarcontext_invkeys_to_search_.assign( polarcontext_invkeys_mat_.begin(), polarcontext_invkeys_mat_.end() ) ; 284 | 285 | polarcontext_tree_batch_.reset(); 286 | polarcontext_tree_batch_ = std::make_unique(PC_NUM_RING /* dim */, polarcontext_invkeys_to_search_, 10 /* max leaf */ ); 287 | 288 | is_tree_batch_made = true; // for running this block only once 289 | } 290 | 291 | double min_dist = 10000000; // init with somthing large 292 | int nn_align = 0; 293 | int nn_idx = 0; 294 | 295 | // step 1: knn search 296 | std::vector candidate_indexes( NUM_CANDIDATES_FROM_TREE ); 297 | std::vector out_dists_sqr( NUM_CANDIDATES_FROM_TREE ); 298 | 299 | nanoflann::KNNResultSet knnsearch_result( NUM_CANDIDATES_FROM_TREE ); 300 | knnsearch_result.init( &candidate_indexes[0], &out_dists_sqr[0] ); 301 | polarcontext_tree_batch_->index->findNeighbors( knnsearch_result, &curr_key[0] /* query */, nanoflann::SearchParams(10) ); // error here 302 | 303 | // step 2: pairwise distance (find optimal columnwise best-fit using cosine distance) 304 | TicTocV2 t_calc_dist; 305 | for ( int candidate_iter_idx = 0; candidate_iter_idx < NUM_CANDIDATES_FROM_TREE; candidate_iter_idx++ ) 306 | { 307 | MatrixXd polarcontext_candidate = polarcontexts_[ candidate_indexes[candidate_iter_idx] ]; 308 | std::pair sc_dist_result = distanceBtnScanContext( curr_desc, polarcontext_candidate ); 309 | 310 | double candidate_dist = sc_dist_result.first; 311 | int candidate_align = sc_dist_result.second; 312 | 313 | if( candidate_dist < min_dist ) 314 | { 315 | min_dist = candidate_dist; 316 | nn_align = candidate_align; 317 | 318 | nn_idx = candidate_indexes[candidate_iter_idx]; 319 | } 320 | } 321 | t_calc_dist.toc("Distance calc"); 322 | 323 | // step 3: similarity threshold 324 | if( min_dist < SC_DIST_THRES ) 325 | loop_id = nn_idx; 326 | 327 | // To do: return also nn_align (i.e., yaw diff) 328 | float yaw_diff_rad = deg2rad(nn_align * PC_UNIT_SECTORANGLE); 329 | std::pair result {loop_id, yaw_diff_rad}; 330 | 331 | return result; 332 | 333 | } // SCManager::detectLoopClosureIDBetweenSession 334 | 335 | 336 | std::pair SCManager::detectLoopClosureID ( void ) 337 | { 338 | int loop_id { -1 }; // init with -1, -1 means no loop (== LeGO-LOAM's variable "closestHistoryFrameID") 339 | 340 | auto curr_key = polarcontext_invkeys_mat_.back(); // current observation (query) 341 | auto curr_desc = polarcontexts_.back(); // current observation (query) 342 | 343 | /* 344 | * step 1: candidates from ringkey tree_ 345 | */ 346 | if( (int)polarcontext_invkeys_mat_.size() < NUM_EXCLUDE_RECENT + 1) 347 | { 348 | std::pair result {loop_id, 0.0}; 349 | return result; // Early return 350 | } 351 | 352 | // tree_ reconstruction (not mandatory to make everytime) 353 | if( tree_making_period_conter % TREE_MAKING_PERIOD_ == 0) // to save computation cost 354 | { 355 | TicTocV2 t_tree_construction; 356 | 357 | polarcontext_invkeys_to_search_.clear(); 358 | polarcontext_invkeys_to_search_.assign( polarcontext_invkeys_mat_.begin(), polarcontext_invkeys_mat_.end() - NUM_EXCLUDE_RECENT ) ; 359 | 360 | polarcontext_tree_.reset(); 361 | polarcontext_tree_ = std::make_unique(PC_NUM_RING /* dim */, polarcontext_invkeys_to_search_, 10 /* max leaf */ ); 362 | // tree_ptr_->index->buildIndex(); // inernally called in the constructor of InvKeyTree (for detail, refer the nanoflann and KDtreeVectorOfVectorsAdaptor) 363 | t_tree_construction.toc("Tree construction"); 364 | } 365 | tree_making_period_conter = tree_making_period_conter + 1; 366 | 367 | double min_dist = 10000000; // init with somthing large 368 | int nn_align = 0; 369 | int nn_idx = 0; 370 | 371 | // knn search 372 | std::vector candidate_indexes( NUM_CANDIDATES_FROM_TREE ); 373 | std::vector out_dists_sqr( NUM_CANDIDATES_FROM_TREE ); 374 | 375 | TicTocV2 t_tree_search; 376 | nanoflann::KNNResultSet knnsearch_result( NUM_CANDIDATES_FROM_TREE ); 377 | knnsearch_result.init( &candidate_indexes[0], &out_dists_sqr[0] ); 378 | polarcontext_tree_->index->findNeighbors( knnsearch_result, &curr_key[0] /* query */, nanoflann::SearchParams(10) ); 379 | t_tree_search.toc("Tree search"); 380 | 381 | /* 382 | * step 2: pairwise distance (find optimal columnwise best-fit using cosine distance) 383 | */ 384 | TicTocV2 t_calc_dist; 385 | for ( int candidate_iter_idx = 0; candidate_iter_idx < NUM_CANDIDATES_FROM_TREE; candidate_iter_idx++ ) 386 | { 387 | MatrixXd polarcontext_candidate = polarcontexts_[ candidate_indexes[candidate_iter_idx] ]; 388 | std::pair sc_dist_result = distanceBtnScanContext( curr_desc, polarcontext_candidate ); 389 | 390 | double candidate_dist = sc_dist_result.first; 391 | int candidate_align = sc_dist_result.second; 392 | 393 | if( candidate_dist < min_dist ) 394 | { 395 | min_dist = candidate_dist; 396 | nn_align = candidate_align; 397 | 398 | nn_idx = candidate_indexes[candidate_iter_idx]; 399 | } 400 | } 401 | t_calc_dist.toc("Distance calc"); 402 | 403 | /* 404 | * loop threshold check 405 | */ 406 | if( min_dist < SC_DIST_THRES ) 407 | { 408 | loop_id = nn_idx; 409 | 410 | // std::cout.precision(3); 411 | cout << "[Loop found] Nearest distance: " << min_dist << " btn " << polarcontexts_.size()-1 << " and " << nn_idx << "." << endl; 412 | // cout << "[Loop found] yaw diff: " << nn_align * PC_UNIT_SECTORANGLE << " deg." << endl; 413 | } 414 | else 415 | { 416 | std::cout.precision(3); 417 | cout << "[Not loop] Nearest distance: " << min_dist << " btn " << polarcontexts_.size()-1 << " and " << nn_idx << "." << endl; 418 | // cout << "[Not loop] yaw diff: " << nn_align * PC_UNIT_SECTORANGLE << " deg." << endl; 419 | } 420 | 421 | // To do: return also nn_align (i.e., yaw diff) 422 | float yaw_diff_rad = deg2rad(nn_align * PC_UNIT_SECTORANGLE); 423 | std::pair result {loop_id, yaw_diff_rad}; 424 | 425 | return result; 426 | 427 | } // SCManager::detectLoopClosureID 428 | 429 | // } // namespace SC2 -------------------------------------------------------------------------------- /include/scancontext/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 "scancontext/nanoflann.hpp" 26 | #include "scancontext/KDTreeVectorOfVectorsAdaptor.h" 27 | #include "aloam_velodyne/tic_toc.h" 28 | 29 | using namespace Eigen; 30 | using namespace nanoflann; 31 | 32 | using std::cout; 33 | using std::endl; 34 | using std::make_pair; 35 | 36 | using std::atan2; 37 | using std::cos; 38 | using std::sin; 39 | 40 | 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) 41 | using KeyMat = std::vector >; 42 | using InvKeyTree = KDTreeVectorOfVectorsAdaptor< KeyMat, float >; 43 | 44 | 45 | // namespace SC2 46 | // { 47 | 48 | void coreImportTest ( void ); 49 | 50 | 51 | // sc param-independent helper functions 52 | float xy2theta( const float & _x, const float & _y ); 53 | MatrixXd circshift( MatrixXd &_mat, int _num_shift ); 54 | std::vector eig2stdvec( MatrixXd _eigmat ); 55 | 56 | 57 | class SCManager 58 | { 59 | public: 60 | SCManager( ) = default; // reserving data space (of std::vector) could be considered. but the descriptor is lightweight so don't care. 61 | 62 | Eigen::MatrixXd makeScancontext( pcl::PointCloud & _scan_down ); 63 | Eigen::MatrixXd makeRingkeyFromScancontext( Eigen::MatrixXd &_desc ); 64 | Eigen::MatrixXd makeSectorkeyFromScancontext( Eigen::MatrixXd &_desc ); 65 | 66 | int fastAlignUsingVkey ( MatrixXd & _vkey1, MatrixXd & _vkey2 ); 67 | double distDirectSC ( MatrixXd &_sc1, MatrixXd &_sc2 ); // "d" (eq 5) in the original paper (IROS 18) 68 | std::pair distanceBtnScanContext ( MatrixXd &_sc1, MatrixXd &_sc2 ); // "D" (eq 6) in the original paper (IROS 18) 69 | 70 | // User-side API 71 | void makeAndSaveScancontextAndKeys( pcl::PointCloud & _scan_down ); 72 | std::pair detectLoopClosureID( void ); // int: nearest node index, float: relative yaw 73 | 74 | // for ltslam 75 | // User-side API for multi-session 76 | void saveScancontextAndKeys( Eigen::MatrixXd _scd ); 77 | std::pair detectLoopClosureIDBetweenSession ( std::vector& curr_key, Eigen::MatrixXd& curr_desc); 78 | 79 | const Eigen::MatrixXd& getConstRefRecentSCD(void); 80 | 81 | public: 82 | // hyper parameters () 83 | 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. 84 | 85 | const int PC_NUM_RING = 20; // 20 in the original paper (IROS 18) 86 | const int PC_NUM_SECTOR = 60; // 60 in the original paper (IROS 18) 87 | double PC_MAX_RADIUS = 80.0; // 80 meter max in the original paper (IROS 18) 88 | const double PC_UNIT_SECTORANGLE = 360.0 / double(PC_NUM_SECTOR); 89 | const double PC_UNIT_RINGGAP = PC_MAX_RADIUS / double(PC_NUM_RING); 90 | 91 | // tree 92 | const int NUM_EXCLUDE_RECENT = 30; // simply just keyframe gap (related with loopClosureFrequency in yaml), but node position distance-based exclusion is ok. 93 | const int NUM_CANDIDATES_FROM_TREE = 3; // 10 is enough. (refer the IROS 18 paper) 94 | 95 | // loop thres 96 | 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. 97 | // 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) 98 | 99 | double SC_DIST_THRES = 0.2; // 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_ = 30; // 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 | // setter 107 | void setSCdistThres(double _new_thres); 108 | void setMaximumRadius(double _max_r); 109 | 110 | // data 111 | std::vector polarcontexts_timestamp_; // optional. 112 | std::vector polarcontexts_; 113 | std::vector polarcontext_invkeys_; 114 | std::vector polarcontext_vkeys_; 115 | 116 | KeyMat polarcontext_invkeys_mat_; 117 | KeyMat polarcontext_invkeys_to_search_; 118 | std::unique_ptr polarcontext_tree_; 119 | 120 | bool is_tree_batch_made = false; 121 | std::unique_ptr polarcontext_tree_batch_; 122 | 123 | }; // SCManager 124 | 125 | // } // namespace SC2 126 | -------------------------------------------------------------------------------- /launch/aloam_mulran.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /launch/aloam_velodyne_HDL_32.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /launch/aloam_velodyne_HDL_64.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /launch/aloam_velodyne_VLP_16.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /launch/kitti_helper.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | aloam_velodyne 4 | 0.0.0 5 | 6 | 7 | This is an advanced implentation of LOAM. 8 | LOAM is described in the following paper: 9 | J. Zhang and S. Singh. LOAM: Lidar Odometry and Mapping in Real-time. 10 | Robotics: Science and Systems Conference (RSS). Berkeley, CA, July 2014. 11 | 12 | 13 | qintong 14 | 15 | BSD 16 | 17 | Ji Zhang 18 | 19 | catkin 20 | geometry_msgs 21 | nav_msgs 22 | roscpp 23 | rospy 24 | std_msgs 25 | rosbag 26 | sensor_msgs 27 | tf 28 | image_transport 29 | 30 | geometry_msgs 31 | nav_msgs 32 | sensor_msgs 33 | roscpp 34 | rospy 35 | std_msgs 36 | rosbag 37 | tf 38 | image_transport 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /picture/anypipe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/anypipe.png -------------------------------------------------------------------------------- /picture/indoor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/indoor.png -------------------------------------------------------------------------------- /picture/kaist-03-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/kaist-03-1.png -------------------------------------------------------------------------------- /picture/kaist-03-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/kaist-03-2.png -------------------------------------------------------------------------------- /picture/kaist-03-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/kaist-03-3.png -------------------------------------------------------------------------------- /picture/kaist-03-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/kaist-03-4.png -------------------------------------------------------------------------------- /picture/kaist-03-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/kaist-03-5.png -------------------------------------------------------------------------------- /picture/kaist-03-merged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/kaist-03-merged.png -------------------------------------------------------------------------------- /picture/kitti.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/kitti.png -------------------------------------------------------------------------------- /picture/kitti05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/kitti05.png -------------------------------------------------------------------------------- /picture/kitti_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/kitti_gif.gif -------------------------------------------------------------------------------- /picture/riverside01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/riverside01.png -------------------------------------------------------------------------------- /picture/scfastlio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/picture/scfastlio.png -------------------------------------------------------------------------------- /rviz_cfg/aloam_velodyne.rviz: -------------------------------------------------------------------------------- 1 | Panels: 2 | - Class: rviz/Displays 3 | Help Height: 78 4 | Name: Displays 5 | Property Tree Widget: 6 | Expanded: 7 | - /odometry1/odomPath1 8 | - /mapping1 9 | - /pgoOdom1/Shape1 10 | Splitter Ratio: 0.49117645621299744 11 | Tree Height: 746 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: pgoMap 31 | - Class: rviz/Views 32 | Expanded: 33 | - /Current View1 34 | Name: pgo Views 35 | Splitter Ratio: 0.5 36 | - Class: rviz/Views 37 | Expanded: 38 | - /Current View1 39 | Name: Views 40 | Splitter Ratio: 0.5 41 | Preferences: 42 | PromptSaveOnExit: true 43 | Toolbars: 44 | toolButtonStyle: 2 45 | Visualization Manager: 46 | Class: "" 47 | Displays: 48 | - Alpha: 0.5 49 | Cell Size: 1 50 | Class: rviz/Grid 51 | Color: 160; 160; 164 52 | Enabled: false 53 | Line Style: 54 | Line Width: 0.029999999329447746 55 | Value: Lines 56 | Name: Grid 57 | Normal Cell Count: 0 58 | Offset: 59 | X: 0 60 | Y: 0 61 | Z: 0 62 | Plane: XY 63 | Plane Cell Count: 160 64 | Reference Frame: 65 | Value: false 66 | - Class: rviz/Axes 67 | Enabled: true 68 | Length: 1 69 | Name: Axes 70 | Radius: 0.10000000149011612 71 | Reference Frame: camera_init 72 | Value: true 73 | - Class: rviz/Group 74 | Displays: 75 | - Alpha: 1 76 | Buffer Length: 1 77 | Class: rviz/Path 78 | Color: 255; 170; 0 79 | Enabled: false 80 | Head Diameter: 0.30000001192092896 81 | Head Length: 0.20000000298023224 82 | Length: 0.30000001192092896 83 | Line Style: Lines 84 | Line Width: 0.029999999329447746 85 | Name: gtPathlc 86 | Offset: 87 | X: 0 88 | Y: 0 89 | Z: 0 90 | Pose Color: 255; 85; 255 91 | Pose Style: None 92 | Radius: 0.029999999329447746 93 | Shaft Diameter: 0.10000000149011612 94 | Shaft Length: 0.10000000149011612 95 | Topic: /path_gt 96 | Unreliable: false 97 | Value: false 98 | - Alpha: 1 99 | Buffer Length: 1 100 | Class: rviz/Path 101 | Color: 255; 170; 0 102 | Enabled: true 103 | Head Diameter: 0.30000001192092896 104 | Head Length: 0.20000000298023224 105 | Length: 0.30000001192092896 106 | Line Style: Lines 107 | Line Width: 0.029999999329447746 108 | Name: gtPathLidar 109 | Offset: 110 | X: 0 111 | Y: 0 112 | Z: 0 113 | Pose Color: 255; 85; 255 114 | Pose Style: None 115 | Radius: 0.029999999329447746 116 | Shaft Diameter: 0.10000000149011612 117 | Shaft Length: 0.10000000149011612 118 | Topic: /path_gt_lidar 119 | Unreliable: false 120 | Value: true 121 | Enabled: false 122 | Name: GT 123 | - Class: rviz/Group 124 | Displays: 125 | - Alpha: 1 126 | Buffer Length: 1 127 | Class: rviz/Path 128 | Color: 204; 0; 0 129 | Enabled: true 130 | Head Diameter: 0.30000001192092896 131 | Head Length: 0.20000000298023224 132 | Length: 0.30000001192092896 133 | Line Style: Billboards 134 | Line Width: 0.20000000298023224 135 | Name: odomPath 136 | Offset: 137 | X: 0 138 | Y: 0 139 | Z: 0 140 | Pose Color: 255; 85; 255 141 | Pose Style: None 142 | Radius: 0.029999999329447746 143 | Shaft Diameter: 0.10000000149011612 144 | Shaft Length: 0.10000000149011612 145 | Topic: /laser_odom_path 146 | Unreliable: false 147 | Value: true 148 | - Alpha: 1 149 | Autocompute Intensity Bounds: true 150 | Autocompute Value Bounds: 151 | Max Value: 10 152 | Min Value: -10 153 | Value: true 154 | Axis: Z 155 | Channel Name: intensity 156 | Class: rviz/PointCloud2 157 | Color: 255; 255; 255 158 | Color Transformer: FlatColor 159 | Decay Time: 0 160 | Enabled: false 161 | Invert Rainbow: false 162 | Max Color: 255; 255; 255 163 | Max Intensity: 63.10047912597656 164 | Min Color: 0; 0; 0 165 | Min Intensity: -0.0067862942814826965 166 | Name: PointCloud2 167 | Position Transformer: XYZ 168 | Queue Size: 10 169 | Selectable: true 170 | Size (Pixels): 3 171 | Size (m): 0.05000000074505806 172 | Style: Flat Squares 173 | Topic: /velodyne_cloud_2 174 | Unreliable: false 175 | Use Fixed Frame: true 176 | Use rainbow: true 177 | Value: false 178 | Enabled: true 179 | Name: odometry 180 | - Class: rviz/Group 181 | Displays: 182 | - Alpha: 1 183 | Buffer Length: 1 184 | Class: rviz/Path 185 | Color: 4; 69; 246 186 | Enabled: true 187 | Head Diameter: 0.30000001192092896 188 | Head Length: 0.20000000298023224 189 | Length: 0.30000001192092896 190 | Line Style: Billboards 191 | Line Width: 0.30000001192092896 192 | Name: mappingPath 193 | Offset: 194 | X: 0 195 | Y: 0 196 | Z: 0 197 | Pose Color: 255; 85; 255 198 | Pose Style: None 199 | Radius: 0.029999999329447746 200 | Shaft Diameter: 0.10000000149011612 201 | Shaft Length: 0.10000000149011612 202 | Topic: /aft_mapped_path 203 | Unreliable: false 204 | Value: true 205 | - Alpha: 1 206 | Autocompute Intensity Bounds: true 207 | Autocompute Value Bounds: 208 | Max Value: 10 209 | Min Value: -10 210 | Value: true 211 | Axis: Z 212 | Channel Name: intensity 213 | Class: rviz/PointCloud2 214 | Color: 255; 255; 255 215 | Color Transformer: Intensity 216 | Decay Time: 0 217 | Enabled: false 218 | Invert Rainbow: false 219 | Max Color: 255; 255; 255 220 | Max Intensity: 50.14332962036133 221 | Min Color: 0; 0; 0 222 | Min Intensity: -0.04392780363559723 223 | Name: allMapCloud 224 | Position Transformer: XYZ 225 | Queue Size: 10 226 | Selectable: true 227 | Size (Pixels): 4 228 | Size (m): 0.20000000298023224 229 | Style: Points 230 | Topic: /laser_cloud_map 231 | Unreliable: false 232 | Use Fixed Frame: true 233 | Use rainbow: false 234 | Value: false 235 | - Alpha: 1 236 | Autocompute Intensity Bounds: true 237 | Autocompute Value Bounds: 238 | Max Value: 10 239 | Min Value: -10 240 | Value: true 241 | Axis: Z 242 | Channel Name: intensity 243 | Class: rviz/PointCloud2 244 | Color: 255; 255; 255 245 | Color Transformer: FlatColor 246 | Decay Time: 0 247 | Enabled: false 248 | Invert Rainbow: false 249 | Max Color: 255; 255; 255 250 | Max Intensity: 15 251 | Min Color: 0; 0; 0 252 | Min Intensity: 0 253 | Name: surround 254 | Position Transformer: XYZ 255 | Queue Size: 1 256 | Selectable: true 257 | Size (Pixels): 1 258 | Size (m): 0.05000000074505806 259 | Style: Squares 260 | Topic: /laser_cloud_surround 261 | Unreliable: false 262 | Use Fixed Frame: true 263 | Use rainbow: true 264 | Value: false 265 | - Alpha: 1 266 | Autocompute Intensity Bounds: true 267 | Autocompute Value Bounds: 268 | Max Value: 10 269 | Min Value: -10 270 | Value: true 271 | Axis: Z 272 | Channel Name: intensity 273 | Class: rviz/PointCloud2 274 | Color: 255; 255; 255 275 | Color Transformer: Intensity 276 | Decay Time: 0 277 | Enabled: false 278 | Invert Rainbow: false 279 | Max Color: 255; 255; 255 280 | Max Intensity: 50.141441345214844 281 | Min Color: 0; 0; 0 282 | Min Intensity: -0.024373823776841164 283 | Name: currPoints 284 | Position Transformer: XYZ 285 | Queue Size: 10 286 | Selectable: true 287 | Size (Pixels): 1.5 288 | Size (m): 0.009999999776482582 289 | Style: Points 290 | Topic: /velodyne_cloud_registered 291 | Unreliable: false 292 | Use Fixed Frame: true 293 | Use rainbow: true 294 | Value: false 295 | - Angle Tolerance: 0.10000000149011612 296 | Class: rviz/Odometry 297 | Covariance: 298 | Orientation: 299 | Alpha: 0.5 300 | Color: 255; 255; 127 301 | Color Style: Unique 302 | Frame: Local 303 | Offset: 1 304 | Scale: 1 305 | Value: true 306 | Position: 307 | Alpha: 0.30000001192092896 308 | Color: 204; 51; 204 309 | Scale: 1 310 | Value: true 311 | Value: true 312 | Enabled: false 313 | Keep: 1 314 | Name: Odometry 315 | Position Tolerance: 0.10000000149011612 316 | Shape: 317 | Alpha: 1 318 | Axes Length: 1.5 319 | Axes Radius: 0.5 320 | Color: 255; 25; 0 321 | Head Length: 0.10000000149011612 322 | Head Radius: 2 323 | Shaft Length: 0.10000000149011612 324 | Shaft Radius: 20 325 | Value: Axes 326 | Topic: /aft_mapped_to_init 327 | Unreliable: false 328 | Value: false 329 | - Class: rviz/TF 330 | Enabled: false 331 | Frame Timeout: 15 332 | Frames: 333 | All Enabled: true 334 | Marker Scale: 1 335 | Name: TF 336 | Show Arrows: true 337 | Show Axes: true 338 | Show Names: true 339 | Tree: 340 | {} 341 | Update Interval: 0 342 | Value: false 343 | Enabled: true 344 | Name: mapping 345 | - Alpha: 1 346 | Buffer Length: 1 347 | Class: rviz/Path 348 | Color: 25; 255; 0 349 | Enabled: true 350 | Head Diameter: 0.30000001192092896 351 | Head Length: 0.20000000298023224 352 | Length: 1 353 | Line Style: Billboards 354 | Line Width: 0.4000000059604645 355 | Name: pgoPath 356 | Offset: 357 | X: 0 358 | Y: 0 359 | Z: 0 360 | Pose Color: 255; 85; 255 361 | Pose Style: Axes 362 | Radius: 0.30000001192092896 363 | Shaft Diameter: 0.10000000149011612 364 | Shaft Length: 0.10000000149011612 365 | Topic: /aft_pgo_path 366 | Unreliable: false 367 | Value: true 368 | - Angle Tolerance: 0.10000000149011612 369 | Class: rviz/Odometry 370 | Covariance: 371 | Orientation: 372 | Alpha: 0.5 373 | Color: 255; 255; 127 374 | Color Style: Unique 375 | Frame: Local 376 | Offset: 1 377 | Scale: 1 378 | Value: true 379 | Position: 380 | Alpha: 0.30000001192092896 381 | Color: 204; 51; 204 382 | Scale: 1 383 | Value: true 384 | Value: true 385 | Enabled: false 386 | Keep: 1 387 | Name: pgoOdom 388 | Position Tolerance: 0.10000000149011612 389 | Shape: 390 | Alpha: 1 391 | Axes Length: 5 392 | Axes Radius: 1 393 | Color: 255; 25; 0 394 | Head Length: 0.30000001192092896 395 | Head Radius: 0.10000000149011612 396 | Shaft Length: 1 397 | Shaft Radius: 0.05000000074505806 398 | Value: Axes 399 | Topic: /aft_pgo_odom 400 | Unreliable: false 401 | Value: false 402 | - Alpha: 0.5 403 | Autocompute Intensity Bounds: true 404 | Autocompute Value Bounds: 405 | Max Value: 10 406 | Min Value: -10 407 | Value: true 408 | Axis: Z 409 | Channel Name: intensity 410 | Class: rviz/PointCloud2 411 | Color: 255; 255; 255 412 | Color Transformer: Intensity 413 | Decay Time: 0 414 | Enabled: true 415 | Invert Rainbow: false 416 | Max Color: 255; 255; 255 417 | Max Intensity: 15.099973678588867 418 | Min Color: 0; 0; 0 419 | Min Intensity: 0.00020467743161134422 420 | Name: pgoMap 421 | Position Transformer: XYZ 422 | Queue Size: 1 423 | Selectable: true 424 | Size (Pixels): 3 425 | Size (m): 0.10000000149011612 426 | Style: Points 427 | Topic: /aft_pgo_map 428 | Unreliable: false 429 | Use Fixed Frame: true 430 | Use rainbow: true 431 | Value: true 432 | - Alpha: 1 433 | Autocompute Intensity Bounds: true 434 | Autocompute Value Bounds: 435 | Max Value: 10 436 | Min Value: -10 437 | Value: true 438 | Axis: Z 439 | Channel Name: intensity 440 | Class: rviz/PointCloud2 441 | Color: 255; 255; 255 442 | Color Transformer: FlatColor 443 | Decay Time: 0 444 | Enabled: false 445 | Invert Rainbow: false 446 | Max Color: 255; 255; 255 447 | Max Intensity: 20.06364631652832 448 | Min Color: 0; 0; 0 449 | Min Intensity: 3.003571033477783 450 | Name: loop scan 451 | Position Transformer: XYZ 452 | Queue Size: 10 453 | Selectable: true 454 | Size (Pixels): 3 455 | Size (m): 0.5 456 | Style: Spheres 457 | Topic: /loop_scan_local 458 | Unreliable: false 459 | Use Fixed Frame: true 460 | Use rainbow: true 461 | Value: false 462 | - Alpha: 1 463 | Autocompute Intensity Bounds: true 464 | Autocompute Value Bounds: 465 | Max Value: 32.84956741333008 466 | Min Value: -16.756437301635742 467 | Value: true 468 | Axis: Z 469 | Channel Name: intensity 470 | Class: rviz/PointCloud2 471 | Color: 255; 255; 255 472 | Color Transformer: Intensity 473 | Decay Time: 0 474 | Enabled: false 475 | Invert Rainbow: false 476 | Max Color: 255; 255; 255 477 | Max Intensity: 20.100271224975586 478 | Min Color: 0; 0; 0 479 | Min Intensity: 3.0008726119995117 480 | Name: loop submap 481 | Position Transformer: XYZ 482 | Queue Size: 10 483 | Selectable: true 484 | Size (Pixels): 2 485 | Size (m): 0.20000000298023224 486 | Style: Spheres 487 | Topic: /loop_submap_local 488 | Unreliable: false 489 | Use Fixed Frame: true 490 | Use rainbow: true 491 | Value: false 492 | Enabled: true 493 | Global Options: 494 | Background Color: 0; 0; 0 495 | Default Light: true 496 | Fixed Frame: camera_init 497 | Frame Rate: 30 498 | Name: root 499 | Tools: 500 | - Class: rviz/Interact 501 | Hide Inactive Objects: true 502 | - Class: rviz/MoveCamera 503 | - Class: rviz/Select 504 | - Class: rviz/FocusCamera 505 | - Class: rviz/Measure 506 | - Class: rviz/SetInitialPose 507 | Theta std deviation: 0.2617993950843811 508 | Topic: /initialpose 509 | X std deviation: 0.5 510 | Y std deviation: 0.5 511 | - Class: rviz/SetGoal 512 | Topic: /move_base_simple/goal 513 | - Class: rviz/PublishPoint 514 | Single click: true 515 | Topic: /clicked_point 516 | Value: true 517 | Views: 518 | Current: 519 | Class: rviz/Orbit 520 | Distance: 10 521 | Enable Stereo Rendering: 522 | Stereo Eye Separation: 0.05999999865889549 523 | Stereo Focal Distance: 1 524 | Swap Stereo Eyes: false 525 | Value: false 526 | Focal Point: 527 | X: 0 528 | Y: 0 529 | Z: 0 530 | Focal Shape Fixed Size: false 531 | Focal Shape Size: 0.05000000074505806 532 | Invert Z Axis: false 533 | Name: Current View 534 | Near Clip Distance: 0.009999999776482582 535 | Pitch: 0.7853981852531433 536 | Target Frame: aft_pgo 537 | Value: Orbit (rviz) 538 | Yaw: 0.7853981852531433 539 | Saved: ~ 540 | Window Geometry: 541 | Displays: 542 | collapsed: false 543 | Height: 1043 544 | Hide Left Dock: false 545 | Hide Right Dock: false 546 | QMainWindow State: 000000ff00000000fd00000004000000000000015600000375fc020000000afb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003d00000375000000c900fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb0000000a0049006d00610067006500000002ef000000c30000000000000000fb0000001200700067006f00200056006900650077007300000002db000000d7000000a400ffffff000000010000010f00000375fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fc0000003d00000375000000c10100001cfa000000000100000002fb0000000a005600690065007700730100000000ffffffff0000010000fffffffb0000000a0056006900650077007301000006710000010f0000010000fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000007800000003efc0100000002fb0000000800540069006d0065010000000000000780000002eb00fffffffb0000000800540069006d006501000000000000045000000000000000000000050f0000037500000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 547 | Selection: 548 | collapsed: false 549 | Time: 550 | collapsed: false 551 | Tool Properties: 552 | collapsed: false 553 | Views: 554 | collapsed: false 555 | Width: 1920 556 | X: 3840 557 | Y: 0 558 | pgo Views: 559 | collapsed: false 560 | -------------------------------------------------------------------------------- /src/kittiHelper.cpp: -------------------------------------------------------------------------------- 1 | // Author: Tong Qin qintonguav@gmail.com 2 | // Shaozu Cao saozu.cao@connect.ust.hk 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | std::vector read_lidar_data(const std::string lidar_data_path) 26 | { 27 | std::ifstream lidar_data_file(lidar_data_path, std::ifstream::in | std::ifstream::binary); 28 | lidar_data_file.seekg(0, std::ios::end); 29 | const size_t num_elements = lidar_data_file.tellg() / sizeof(float); 30 | lidar_data_file.seekg(0, std::ios::beg); 31 | 32 | std::vector lidar_data_buffer(num_elements); 33 | lidar_data_file.read(reinterpret_cast(&lidar_data_buffer[0]), num_elements*sizeof(float)); 34 | return lidar_data_buffer; 35 | } 36 | 37 | int main(int argc, char** argv) 38 | { 39 | ros::init(argc, argv, "kitti_helper"); 40 | ros::NodeHandle n("~"); 41 | std::string dataset_folder, sequence_number, output_bag_file; 42 | n.getParam("dataset_folder", dataset_folder); 43 | n.getParam("sequence_number", sequence_number); 44 | std::cout << "Reading sequence " << sequence_number << " from " << dataset_folder << '\n'; 45 | bool to_bag; 46 | n.getParam("to_bag", to_bag); 47 | if (to_bag) 48 | n.getParam("output_bag_file", output_bag_file); 49 | int publish_delay; 50 | n.getParam("publish_delay", publish_delay); 51 | publish_delay = publish_delay <= 0 ? 1 : publish_delay; 52 | 53 | ros::Publisher pub_laser_cloud = n.advertise("/velodyne_points", 2); 54 | 55 | image_transport::ImageTransport it(n); 56 | image_transport::Publisher pub_image_left = it.advertise("/image_left", 2); 57 | image_transport::Publisher pub_image_right = it.advertise("/image_right", 2); 58 | 59 | ros::Publisher pubOdomGT = n.advertise ("/odometry_gt", 5); 60 | nav_msgs::Odometry odomGT; 61 | odomGT.header.frame_id = "/camera_init"; 62 | odomGT.child_frame_id = "/ground_truth"; 63 | 64 | ros::Publisher pubPathGT = n.advertise ("/path_gt", 5); 65 | nav_msgs::Path pathGT; 66 | pathGT.header.frame_id = "/camera_init"; 67 | 68 | std::string timestamp_path = "sequences/" + sequence_number + "/times.txt"; 69 | std::ifstream timestamp_file(dataset_folder + timestamp_path, std::ifstream::in); 70 | 71 | std::string ground_truth_path = "results/" + sequence_number + ".txt"; 72 | std::ifstream ground_truth_file(dataset_folder + ground_truth_path, std::ifstream::in); 73 | 74 | rosbag::Bag bag_out; 75 | if (to_bag) 76 | bag_out.open(output_bag_file, rosbag::bagmode::Write); 77 | 78 | Eigen::Matrix3d R_transform; 79 | R_transform << 0, 0, 1, -1, 0, 0, 0, -1, 0; 80 | Eigen::Quaterniond q_transform(R_transform); 81 | 82 | std::string line; 83 | std::size_t line_num = 0; 84 | 85 | ros::Rate r(10.0 / publish_delay); 86 | while (std::getline(timestamp_file, line) && ros::ok()) 87 | { 88 | float timestamp = stof(line); 89 | std::stringstream left_image_path, right_image_path; 90 | left_image_path << dataset_folder << "sequences/" + sequence_number + "/image_0/" << std::setfill('0') << std::setw(6) << line_num << ".png"; 91 | cv::Mat left_image = cv::imread(left_image_path.str(), CV_LOAD_IMAGE_GRAYSCALE); 92 | right_image_path << dataset_folder << "sequences/" + sequence_number + "/image_1/" << std::setfill('0') << std::setw(6) << line_num << ".png"; 93 | cv::Mat right_image = cv::imread(left_image_path.str(), CV_LOAD_IMAGE_GRAYSCALE); 94 | 95 | std::getline(ground_truth_file, line); 96 | std::stringstream pose_stream(line); 97 | std::string s; 98 | Eigen::Matrix gt_pose; 99 | for (std::size_t i = 0; i < 3; ++i) 100 | { 101 | for (std::size_t j = 0; j < 4; ++j) 102 | { 103 | std::getline(pose_stream, s, ' '); 104 | gt_pose(i, j) = stof(s); 105 | } 106 | } 107 | 108 | Eigen::Quaterniond q_w_i(gt_pose.topLeftCorner<3, 3>()); 109 | Eigen::Quaterniond q = q_transform * q_w_i; 110 | q.normalize(); 111 | Eigen::Vector3d t = q_transform * gt_pose.topRightCorner<3, 1>(); 112 | 113 | odomGT.header.stamp = ros::Time().fromSec(timestamp); 114 | odomGT.pose.pose.orientation.x = q.x(); 115 | odomGT.pose.pose.orientation.y = q.y(); 116 | odomGT.pose.pose.orientation.z = q.z(); 117 | odomGT.pose.pose.orientation.w = q.w(); 118 | odomGT.pose.pose.position.x = t(0); 119 | odomGT.pose.pose.position.y = t(1); 120 | odomGT.pose.pose.position.z = t(2); 121 | pubOdomGT.publish(odomGT); 122 | 123 | geometry_msgs::PoseStamped poseGT; 124 | poseGT.header = odomGT.header; 125 | poseGT.pose = odomGT.pose.pose; 126 | pathGT.header.stamp = odomGT.header.stamp; 127 | pathGT.poses.push_back(poseGT); 128 | pubPathGT.publish(pathGT); 129 | 130 | // read lidar point cloud 131 | std::stringstream lidar_data_path; 132 | lidar_data_path << dataset_folder << "velodyne/sequences/" + sequence_number + "/velodyne/" 133 | << std::setfill('0') << std::setw(6) << line_num << ".bin"; 134 | std::vector lidar_data = read_lidar_data(lidar_data_path.str()); 135 | std::cout << "totally " << lidar_data.size() / 4.0 << " points in this lidar frame \n"; 136 | 137 | std::vector lidar_points; 138 | std::vector lidar_intensities; 139 | pcl::PointCloud laser_cloud; 140 | for (std::size_t i = 0; i < lidar_data.size(); i += 4) 141 | { 142 | lidar_points.emplace_back(lidar_data[i], lidar_data[i+1], lidar_data[i+2]); 143 | lidar_intensities.push_back(lidar_data[i+3]); 144 | 145 | pcl::PointXYZI point; 146 | point.x = lidar_data[i]; 147 | point.y = lidar_data[i + 1]; 148 | point.z = lidar_data[i + 2]; 149 | point.intensity = lidar_data[i + 3]; 150 | laser_cloud.push_back(point); 151 | } 152 | 153 | sensor_msgs::PointCloud2 laser_cloud_msg; 154 | pcl::toROSMsg(laser_cloud, laser_cloud_msg); 155 | laser_cloud_msg.header.stamp = ros::Time().fromSec(timestamp); 156 | laser_cloud_msg.header.frame_id = "/camera_init"; 157 | pub_laser_cloud.publish(laser_cloud_msg); 158 | 159 | sensor_msgs::ImagePtr image_left_msg = cv_bridge::CvImage(laser_cloud_msg.header, "mono8", left_image).toImageMsg(); 160 | sensor_msgs::ImagePtr image_right_msg = cv_bridge::CvImage(laser_cloud_msg.header, "mono8", right_image).toImageMsg(); 161 | pub_image_left.publish(image_left_msg); 162 | pub_image_right.publish(image_right_msg); 163 | 164 | if (to_bag) 165 | { 166 | bag_out.write("/image_left", ros::Time::now(), image_left_msg); 167 | bag_out.write("/image_right", ros::Time::now(), image_right_msg); 168 | bag_out.write("/velodyne_points", ros::Time::now(), laser_cloud_msg); 169 | bag_out.write("/path_gt", ros::Time::now(), pathGT); 170 | bag_out.write("/odometry_gt", ros::Time::now(), odomGT); 171 | } 172 | 173 | line_num ++; 174 | r.sleep(); 175 | } 176 | bag_out.close(); 177 | std::cout << "Done \n"; 178 | 179 | 180 | return 0; 181 | } -------------------------------------------------------------------------------- /src/laserMapping.cpp: -------------------------------------------------------------------------------- 1 | // This is an advanced implementation of the algorithm described in the following paper: 2 | // J. Zhang and S. Singh. LOAM: Lidar Odometry and Mapping in Real-time. 3 | // Robotics: Science and Systems Conference (RSS). Berkeley, CA, July 2014. 4 | 5 | // Modifier: Tong Qin qintonguav@gmail.com 6 | // Shaozu Cao saozu.cao@connect.ust.hk 7 | 8 | 9 | // Copyright 2013, Ji Zhang, Carnegie Mellon University 10 | // Further contributions copyright (c) 2016, Southwest Research Institute 11 | // All rights reserved. 12 | // 13 | // Redistribution and use in source and binary forms, with or without 14 | // modification, are permitted provided that the following conditions are met: 15 | // 16 | // 1. Redistributions of source code must retain the above copyright notice, 17 | // this list of conditions and the following disclaimer. 18 | // 2. Redistributions in binary form must reproduce the above copyright notice, 19 | // this list of conditions and the following disclaimer in the documentation 20 | // and/or other materials provided with the distribution. 21 | // 3. Neither the name of the copyright holder nor the names of its 22 | // contributors may be used to endorse or promote products derived from this 23 | // software without specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 26 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 27 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 28 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 29 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 30 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 31 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 32 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 33 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 34 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 35 | // POSSIBILITY OF SUCH DAMAGE. 36 | 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | #include 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include 60 | 61 | #include "lidarFactor.hpp" 62 | #include "aloam_velodyne/common.h" 63 | #include "aloam_velodyne/tic_toc.h" 64 | 65 | 66 | int frameCount = 0; 67 | 68 | double timeLaserCloudCornerLast = 0; 69 | double timeLaserCloudSurfLast = 0; 70 | double timeLaserCloudFullRes = 0; 71 | double timeLaserOdometry = 0; 72 | 73 | 74 | int laserCloudCenWidth = 10; 75 | int laserCloudCenHeight = 10; 76 | int laserCloudCenDepth = 5; 77 | const int laserCloudWidth = 21; 78 | const int laserCloudHeight = 21; 79 | const int laserCloudDepth = 11; 80 | 81 | 82 | const int laserCloudNum = laserCloudWidth * laserCloudHeight * laserCloudDepth; //4851 83 | 84 | 85 | int laserCloudValidInd[125]; 86 | int laserCloudSurroundInd[125]; 87 | 88 | // input: from odom 89 | pcl::PointCloud::Ptr laserCloudCornerLast(new pcl::PointCloud()); 90 | pcl::PointCloud::Ptr laserCloudSurfLast(new pcl::PointCloud()); 91 | 92 | // ouput: all visualble cube points 93 | pcl::PointCloud::Ptr laserCloudSurround(new pcl::PointCloud()); 94 | 95 | // surround points in map to build tree 96 | pcl::PointCloud::Ptr laserCloudCornerFromMap(new pcl::PointCloud()); 97 | pcl::PointCloud::Ptr laserCloudSurfFromMap(new pcl::PointCloud()); 98 | 99 | //input & output: points in one frame. local --> global 100 | pcl::PointCloud::Ptr laserCloudFullRes(new pcl::PointCloud()); 101 | 102 | // points in every cube 103 | pcl::PointCloud::Ptr laserCloudCornerArray[laserCloudNum]; 104 | pcl::PointCloud::Ptr laserCloudSurfArray[laserCloudNum]; 105 | 106 | //kd-tree 107 | pcl::KdTreeFLANN::Ptr kdtreeCornerFromMap(new pcl::KdTreeFLANN()); 108 | pcl::KdTreeFLANN::Ptr kdtreeSurfFromMap(new pcl::KdTreeFLANN()); 109 | 110 | double parameters[7] = {0, 0, 0, 1, 0, 0, 0}; 111 | Eigen::Map q_w_curr(parameters); 112 | Eigen::Map t_w_curr(parameters + 4); 113 | 114 | // wmap_T_odom * odom_T_curr = wmap_T_curr; 115 | // transformation between odom's world and map's world frame 116 | Eigen::Quaterniond q_wmap_wodom(1, 0, 0, 0); 117 | Eigen::Vector3d t_wmap_wodom(0, 0, 0); 118 | 119 | Eigen::Quaterniond q_wodom_curr(1, 0, 0, 0); 120 | Eigen::Vector3d t_wodom_curr(0, 0, 0); 121 | 122 | 123 | std::queue cornerLastBuf; 124 | std::queue surfLastBuf; 125 | std::queue fullResBuf; 126 | std::queue odometryBuf; 127 | std::mutex mBuf; 128 | 129 | pcl::VoxelGrid downSizeFilterCorner; 130 | pcl::VoxelGrid downSizeFilterSurf; 131 | 132 | std::vector pointSearchInd; 133 | std::vector pointSearchSqDis; 134 | 135 | PointType pointOri, pointSel; 136 | 137 | ros::Publisher pubLaserCloudSurround, pubLaserCloudMap, pubLaserCloudFullRes, pubOdomAftMapped, pubOdomAftMappedHighFrec, pubLaserAfterMappedPath; 138 | ros::Publisher pubLaserCloudFullResLocal; 139 | 140 | nav_msgs::Path laserAfterMappedPath; 141 | 142 | // set initial guess 143 | void transformAssociateToMap() 144 | { 145 | q_w_curr = q_wmap_wodom * q_wodom_curr; 146 | t_w_curr = q_wmap_wodom * t_wodom_curr + t_wmap_wodom; 147 | } 148 | 149 | void transformUpdate() 150 | { 151 | q_wmap_wodom = q_w_curr * q_wodom_curr.inverse(); 152 | t_wmap_wodom = t_w_curr - q_wmap_wodom * t_wodom_curr; 153 | } 154 | 155 | void pointAssociateToMap(PointType const *const pi, PointType *const po) 156 | { 157 | Eigen::Vector3d point_curr(pi->x, pi->y, pi->z); 158 | Eigen::Vector3d point_w = q_w_curr * point_curr + t_w_curr; 159 | po->x = point_w.x(); 160 | po->y = point_w.y(); 161 | po->z = point_w.z(); 162 | po->intensity = pi->intensity; 163 | //po->intensity = 1.0; 164 | } 165 | 166 | void pointAssociateTobeMapped(PointType const *const pi, PointType *const po) 167 | { 168 | Eigen::Vector3d point_w(pi->x, pi->y, pi->z); 169 | Eigen::Vector3d point_curr = q_w_curr.inverse() * (point_w - t_w_curr); 170 | po->x = point_curr.x(); 171 | po->y = point_curr.y(); 172 | po->z = point_curr.z(); 173 | po->intensity = pi->intensity; 174 | } 175 | 176 | void laserCloudCornerLastHandler(const sensor_msgs::PointCloud2ConstPtr &laserCloudCornerLast2) 177 | { 178 | mBuf.lock(); 179 | cornerLastBuf.push(laserCloudCornerLast2); 180 | mBuf.unlock(); 181 | } 182 | 183 | void laserCloudSurfLastHandler(const sensor_msgs::PointCloud2ConstPtr &laserCloudSurfLast2) 184 | { 185 | mBuf.lock(); 186 | surfLastBuf.push(laserCloudSurfLast2); 187 | mBuf.unlock(); 188 | } 189 | 190 | void laserCloudFullResHandler(const sensor_msgs::PointCloud2ConstPtr &laserCloudFullRes2) 191 | { 192 | mBuf.lock(); 193 | fullResBuf.push(laserCloudFullRes2); 194 | mBuf.unlock(); 195 | } 196 | 197 | //receive odomtry 198 | void laserOdometryHandler(const nav_msgs::Odometry::ConstPtr &laserOdometry) 199 | { 200 | mBuf.lock(); 201 | odometryBuf.push(laserOdometry); 202 | mBuf.unlock(); 203 | 204 | // high frequence publish 205 | Eigen::Quaterniond q_wodom_curr; 206 | Eigen::Vector3d t_wodom_curr; 207 | q_wodom_curr.x() = laserOdometry->pose.pose.orientation.x; 208 | q_wodom_curr.y() = laserOdometry->pose.pose.orientation.y; 209 | q_wodom_curr.z() = laserOdometry->pose.pose.orientation.z; 210 | q_wodom_curr.w() = laserOdometry->pose.pose.orientation.w; 211 | t_wodom_curr.x() = laserOdometry->pose.pose.position.x; 212 | t_wodom_curr.y() = laserOdometry->pose.pose.position.y; 213 | t_wodom_curr.z() = laserOdometry->pose.pose.position.z; 214 | 215 | Eigen::Quaterniond q_w_curr = q_wmap_wodom * q_wodom_curr; 216 | Eigen::Vector3d t_w_curr = q_wmap_wodom * t_wodom_curr + t_wmap_wodom; 217 | 218 | nav_msgs::Odometry odomAftMapped; 219 | odomAftMapped.header.frame_id = "/camera_init"; 220 | odomAftMapped.child_frame_id = "/aft_mapped"; 221 | odomAftMapped.header.stamp = laserOdometry->header.stamp; 222 | odomAftMapped.pose.pose.orientation.x = q_w_curr.x(); 223 | odomAftMapped.pose.pose.orientation.y = q_w_curr.y(); 224 | odomAftMapped.pose.pose.orientation.z = q_w_curr.z(); 225 | odomAftMapped.pose.pose.orientation.w = q_w_curr.w(); 226 | odomAftMapped.pose.pose.position.x = t_w_curr.x(); 227 | odomAftMapped.pose.pose.position.y = t_w_curr.y(); 228 | odomAftMapped.pose.pose.position.z = t_w_curr.z(); 229 | pubOdomAftMappedHighFrec.publish(odomAftMapped); 230 | } 231 | 232 | void process() 233 | { 234 | while(1) 235 | { 236 | while (!cornerLastBuf.empty() && !surfLastBuf.empty() && 237 | !fullResBuf.empty() && !odometryBuf.empty()) 238 | { 239 | mBuf.lock(); 240 | while (!odometryBuf.empty() && odometryBuf.front()->header.stamp.toSec() < cornerLastBuf.front()->header.stamp.toSec()) 241 | odometryBuf.pop(); 242 | if (odometryBuf.empty()) 243 | { 244 | mBuf.unlock(); 245 | break; 246 | } 247 | 248 | while (!surfLastBuf.empty() && surfLastBuf.front()->header.stamp.toSec() < cornerLastBuf.front()->header.stamp.toSec()) 249 | surfLastBuf.pop(); 250 | if (surfLastBuf.empty()) 251 | { 252 | mBuf.unlock(); 253 | break; 254 | } 255 | 256 | while (!fullResBuf.empty() && fullResBuf.front()->header.stamp.toSec() < cornerLastBuf.front()->header.stamp.toSec()) 257 | fullResBuf.pop(); 258 | if (fullResBuf.empty()) 259 | { 260 | mBuf.unlock(); 261 | break; 262 | } 263 | 264 | timeLaserCloudCornerLast = cornerLastBuf.front()->header.stamp.toSec(); 265 | timeLaserCloudSurfLast = surfLastBuf.front()->header.stamp.toSec(); 266 | timeLaserCloudFullRes = fullResBuf.front()->header.stamp.toSec(); 267 | timeLaserOdometry = odometryBuf.front()->header.stamp.toSec(); 268 | 269 | if (timeLaserCloudCornerLast != timeLaserOdometry || 270 | timeLaserCloudSurfLast != timeLaserOdometry || 271 | timeLaserCloudFullRes != timeLaserOdometry) 272 | { 273 | //printf("time corner %f surf %f full %f odom %f \n", timeLaserCloudCornerLast, timeLaserCloudSurfLast, timeLaserCloudFullRes, timeLaserOdometry); 274 | //printf("unsync messeage!"); 275 | mBuf.unlock(); 276 | break; 277 | } 278 | 279 | laserCloudCornerLast->clear(); 280 | pcl::fromROSMsg(*cornerLastBuf.front(), *laserCloudCornerLast); 281 | cornerLastBuf.pop(); 282 | 283 | laserCloudSurfLast->clear(); 284 | pcl::fromROSMsg(*surfLastBuf.front(), *laserCloudSurfLast); 285 | surfLastBuf.pop(); 286 | 287 | laserCloudFullRes->clear(); 288 | pcl::fromROSMsg(*fullResBuf.front(), *laserCloudFullRes); 289 | fullResBuf.pop(); 290 | 291 | q_wodom_curr.x() = odometryBuf.front()->pose.pose.orientation.x; 292 | q_wodom_curr.y() = odometryBuf.front()->pose.pose.orientation.y; 293 | q_wodom_curr.z() = odometryBuf.front()->pose.pose.orientation.z; 294 | q_wodom_curr.w() = odometryBuf.front()->pose.pose.orientation.w; 295 | t_wodom_curr.x() = odometryBuf.front()->pose.pose.position.x; 296 | t_wodom_curr.y() = odometryBuf.front()->pose.pose.position.y; 297 | t_wodom_curr.z() = odometryBuf.front()->pose.pose.position.z; 298 | odometryBuf.pop(); 299 | 300 | while(!cornerLastBuf.empty()) 301 | { 302 | cornerLastBuf.pop(); 303 | //printf("drop lidar frame in mapping for real time performance \n"); 304 | } 305 | 306 | mBuf.unlock(); 307 | 308 | TicToc t_whole; 309 | 310 | transformAssociateToMap(); 311 | 312 | TicToc t_shift; 313 | int centerCubeI = int((t_w_curr.x() + 25.0) / 50.0) + laserCloudCenWidth; 314 | int centerCubeJ = int((t_w_curr.y() + 25.0) / 50.0) + laserCloudCenHeight; 315 | int centerCubeK = int((t_w_curr.z() + 25.0) / 50.0) + laserCloudCenDepth; 316 | 317 | if (t_w_curr.x() + 25.0 < 0) 318 | centerCubeI--; 319 | if (t_w_curr.y() + 25.0 < 0) 320 | centerCubeJ--; 321 | if (t_w_curr.z() + 25.0 < 0) 322 | centerCubeK--; 323 | 324 | while (centerCubeI < 3) 325 | { 326 | for (int j = 0; j < laserCloudHeight; j++) 327 | { 328 | for (int k = 0; k < laserCloudDepth; k++) 329 | { 330 | int i = laserCloudWidth - 1; 331 | pcl::PointCloud::Ptr laserCloudCubeCornerPointer = 332 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 333 | pcl::PointCloud::Ptr laserCloudCubeSurfPointer = 334 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 335 | for (; i >= 1; i--) 336 | { 337 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 338 | laserCloudCornerArray[i - 1 + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 339 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 340 | laserCloudSurfArray[i - 1 + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 341 | } 342 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 343 | laserCloudCubeCornerPointer; 344 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 345 | laserCloudCubeSurfPointer; 346 | laserCloudCubeCornerPointer->clear(); 347 | laserCloudCubeSurfPointer->clear(); 348 | } 349 | } 350 | 351 | centerCubeI++; 352 | laserCloudCenWidth++; 353 | } 354 | 355 | while (centerCubeI >= laserCloudWidth - 3) 356 | { 357 | for (int j = 0; j < laserCloudHeight; j++) 358 | { 359 | for (int k = 0; k < laserCloudDepth; k++) 360 | { 361 | int i = 0; 362 | pcl::PointCloud::Ptr laserCloudCubeCornerPointer = 363 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 364 | pcl::PointCloud::Ptr laserCloudCubeSurfPointer = 365 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 366 | for (; i < laserCloudWidth - 1; i++) 367 | { 368 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 369 | laserCloudCornerArray[i + 1 + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 370 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 371 | laserCloudSurfArray[i + 1 + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 372 | } 373 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 374 | laserCloudCubeCornerPointer; 375 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 376 | laserCloudCubeSurfPointer; 377 | laserCloudCubeCornerPointer->clear(); 378 | laserCloudCubeSurfPointer->clear(); 379 | } 380 | } 381 | 382 | centerCubeI--; 383 | laserCloudCenWidth--; 384 | } 385 | 386 | while (centerCubeJ < 3) 387 | { 388 | for (int i = 0; i < laserCloudWidth; i++) 389 | { 390 | for (int k = 0; k < laserCloudDepth; k++) 391 | { 392 | int j = laserCloudHeight - 1; 393 | pcl::PointCloud::Ptr laserCloudCubeCornerPointer = 394 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 395 | pcl::PointCloud::Ptr laserCloudCubeSurfPointer = 396 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 397 | for (; j >= 1; j--) 398 | { 399 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 400 | laserCloudCornerArray[i + laserCloudWidth * (j - 1) + laserCloudWidth * laserCloudHeight * k]; 401 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 402 | laserCloudSurfArray[i + laserCloudWidth * (j - 1) + laserCloudWidth * laserCloudHeight * k]; 403 | } 404 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 405 | laserCloudCubeCornerPointer; 406 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 407 | laserCloudCubeSurfPointer; 408 | laserCloudCubeCornerPointer->clear(); 409 | laserCloudCubeSurfPointer->clear(); 410 | } 411 | } 412 | 413 | centerCubeJ++; 414 | laserCloudCenHeight++; 415 | } 416 | 417 | while (centerCubeJ >= laserCloudHeight - 3) 418 | { 419 | for (int i = 0; i < laserCloudWidth; i++) 420 | { 421 | for (int k = 0; k < laserCloudDepth; k++) 422 | { 423 | int j = 0; 424 | pcl::PointCloud::Ptr laserCloudCubeCornerPointer = 425 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 426 | pcl::PointCloud::Ptr laserCloudCubeSurfPointer = 427 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 428 | for (; j < laserCloudHeight - 1; j++) 429 | { 430 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 431 | laserCloudCornerArray[i + laserCloudWidth * (j + 1) + laserCloudWidth * laserCloudHeight * k]; 432 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 433 | laserCloudSurfArray[i + laserCloudWidth * (j + 1) + laserCloudWidth * laserCloudHeight * k]; 434 | } 435 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 436 | laserCloudCubeCornerPointer; 437 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 438 | laserCloudCubeSurfPointer; 439 | laserCloudCubeCornerPointer->clear(); 440 | laserCloudCubeSurfPointer->clear(); 441 | } 442 | } 443 | 444 | centerCubeJ--; 445 | laserCloudCenHeight--; 446 | } 447 | 448 | while (centerCubeK < 3) 449 | { 450 | for (int i = 0; i < laserCloudWidth; i++) 451 | { 452 | for (int j = 0; j < laserCloudHeight; j++) 453 | { 454 | int k = laserCloudDepth - 1; 455 | pcl::PointCloud::Ptr laserCloudCubeCornerPointer = 456 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 457 | pcl::PointCloud::Ptr laserCloudCubeSurfPointer = 458 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 459 | for (; k >= 1; k--) 460 | { 461 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 462 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * (k - 1)]; 463 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 464 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * (k - 1)]; 465 | } 466 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 467 | laserCloudCubeCornerPointer; 468 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 469 | laserCloudCubeSurfPointer; 470 | laserCloudCubeCornerPointer->clear(); 471 | laserCloudCubeSurfPointer->clear(); 472 | } 473 | } 474 | 475 | centerCubeK++; 476 | laserCloudCenDepth++; 477 | } 478 | 479 | while (centerCubeK >= laserCloudDepth - 3) 480 | { 481 | for (int i = 0; i < laserCloudWidth; i++) 482 | { 483 | for (int j = 0; j < laserCloudHeight; j++) 484 | { 485 | int k = 0; 486 | pcl::PointCloud::Ptr laserCloudCubeCornerPointer = 487 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 488 | pcl::PointCloud::Ptr laserCloudCubeSurfPointer = 489 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k]; 490 | for (; k < laserCloudDepth - 1; k++) 491 | { 492 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 493 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * (k + 1)]; 494 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 495 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * (k + 1)]; 496 | } 497 | laserCloudCornerArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 498 | laserCloudCubeCornerPointer; 499 | laserCloudSurfArray[i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k] = 500 | laserCloudCubeSurfPointer; 501 | laserCloudCubeCornerPointer->clear(); 502 | laserCloudCubeSurfPointer->clear(); 503 | } 504 | } 505 | 506 | centerCubeK--; 507 | laserCloudCenDepth--; 508 | } 509 | 510 | int laserCloudValidNum = 0; 511 | int laserCloudSurroundNum = 0; 512 | 513 | for (int i = centerCubeI - 2; i <= centerCubeI + 2; i++) 514 | { 515 | for (int j = centerCubeJ - 2; j <= centerCubeJ + 2; j++) 516 | { 517 | for (int k = centerCubeK - 1; k <= centerCubeK + 1; k++) 518 | { 519 | if (i >= 0 && i < laserCloudWidth && 520 | j >= 0 && j < laserCloudHeight && 521 | k >= 0 && k < laserCloudDepth) 522 | { 523 | laserCloudValidInd[laserCloudValidNum] = i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k; 524 | laserCloudValidNum++; 525 | laserCloudSurroundInd[laserCloudSurroundNum] = i + laserCloudWidth * j + laserCloudWidth * laserCloudHeight * k; 526 | laserCloudSurroundNum++; 527 | } 528 | } 529 | } 530 | } 531 | 532 | laserCloudCornerFromMap->clear(); 533 | laserCloudSurfFromMap->clear(); 534 | for (int i = 0; i < laserCloudValidNum; i++) 535 | { 536 | *laserCloudCornerFromMap += *laserCloudCornerArray[laserCloudValidInd[i]]; 537 | *laserCloudSurfFromMap += *laserCloudSurfArray[laserCloudValidInd[i]]; 538 | } 539 | int laserCloudCornerFromMapNum = laserCloudCornerFromMap->points.size(); 540 | int laserCloudSurfFromMapNum = laserCloudSurfFromMap->points.size(); 541 | 542 | 543 | pcl::PointCloud::Ptr laserCloudCornerStack(new pcl::PointCloud()); 544 | downSizeFilterCorner.setInputCloud(laserCloudCornerLast); 545 | downSizeFilterCorner.filter(*laserCloudCornerStack); 546 | int laserCloudCornerStackNum = laserCloudCornerStack->points.size(); 547 | 548 | pcl::PointCloud::Ptr laserCloudSurfStack(new pcl::PointCloud()); 549 | downSizeFilterSurf.setInputCloud(laserCloudSurfLast); 550 | downSizeFilterSurf.filter(*laserCloudSurfStack); 551 | int laserCloudSurfStackNum = laserCloudSurfStack->points.size(); 552 | 553 | //printf("map prepare time %f ms\n", t_shift.toc()); 554 | //printf("map corner num %d surf num %d \n", laserCloudCornerFromMapNum, laserCloudSurfFromMapNum); 555 | if (laserCloudCornerFromMapNum > 10 && laserCloudSurfFromMapNum > 50) 556 | { 557 | TicToc t_opt; 558 | TicToc t_tree; 559 | kdtreeCornerFromMap->setInputCloud(laserCloudCornerFromMap); 560 | kdtreeSurfFromMap->setInputCloud(laserCloudSurfFromMap); 561 | //printf("build tree time %f ms \n", t_tree.toc()); 562 | 563 | for (int iterCount = 0; iterCount < 2; iterCount++) 564 | { 565 | //ceres::LossFunction *loss_function = NULL; 566 | ceres::LossFunction *loss_function = new ceres::HuberLoss(0.1); 567 | ceres::LocalParameterization *q_parameterization = 568 | new ceres::EigenQuaternionParameterization(); 569 | ceres::Problem::Options problem_options; 570 | 571 | ceres::Problem problem(problem_options); 572 | problem.AddParameterBlock(parameters, 4, q_parameterization); 573 | problem.AddParameterBlock(parameters + 4, 3); 574 | 575 | TicToc t_data; 576 | int corner_num = 0; 577 | 578 | for (int i = 0; i < laserCloudCornerStackNum; i++) 579 | { 580 | pointOri = laserCloudCornerStack->points[i]; 581 | //double sqrtDis = pointOri.x * pointOri.x + pointOri.y * pointOri.y + pointOri.z * pointOri.z; 582 | pointAssociateToMap(&pointOri, &pointSel); 583 | kdtreeCornerFromMap->nearestKSearch(pointSel, 5, pointSearchInd, pointSearchSqDis); 584 | 585 | if (pointSearchSqDis[4] < 1.0) 586 | { 587 | std::vector nearCorners; 588 | Eigen::Vector3d center(0, 0, 0); 589 | for (int j = 0; j < 5; j++) 590 | { 591 | Eigen::Vector3d tmp(laserCloudCornerFromMap->points[pointSearchInd[j]].x, 592 | laserCloudCornerFromMap->points[pointSearchInd[j]].y, 593 | laserCloudCornerFromMap->points[pointSearchInd[j]].z); 594 | center = center + tmp; 595 | nearCorners.push_back(tmp); 596 | } 597 | center = center / 5.0; 598 | 599 | Eigen::Matrix3d covMat = Eigen::Matrix3d::Zero(); 600 | for (int j = 0; j < 5; j++) 601 | { 602 | Eigen::Matrix tmpZeroMean = nearCorners[j] - center; 603 | covMat = covMat + tmpZeroMean * tmpZeroMean.transpose(); 604 | } 605 | 606 | Eigen::SelfAdjointEigenSolver saes(covMat); 607 | 608 | // if is indeed line feature 609 | // note Eigen library sort eigenvalues in increasing order 610 | Eigen::Vector3d unit_direction = saes.eigenvectors().col(2); 611 | Eigen::Vector3d curr_point(pointOri.x, pointOri.y, pointOri.z); 612 | if (saes.eigenvalues()[2] > 3 * saes.eigenvalues()[1]) 613 | { 614 | Eigen::Vector3d point_on_line = center; 615 | Eigen::Vector3d point_a, point_b; 616 | point_a = 0.1 * unit_direction + point_on_line; 617 | point_b = -0.1 * unit_direction + point_on_line; 618 | 619 | ceres::CostFunction *cost_function = LidarEdgeFactor::Create(curr_point, point_a, point_b, 1.0); 620 | problem.AddResidualBlock(cost_function, loss_function, parameters, parameters + 4); 621 | corner_num++; 622 | } 623 | } 624 | /* 625 | else if(pointSearchSqDis[4] < 0.01 * sqrtDis) 626 | { 627 | Eigen::Vector3d center(0, 0, 0); 628 | for (int j = 0; j < 5; j++) 629 | { 630 | Eigen::Vector3d tmp(laserCloudCornerFromMap->points[pointSearchInd[j]].x, 631 | laserCloudCornerFromMap->points[pointSearchInd[j]].y, 632 | laserCloudCornerFromMap->points[pointSearchInd[j]].z); 633 | center = center + tmp; 634 | } 635 | center = center / 5.0; 636 | Eigen::Vector3d curr_point(pointOri.x, pointOri.y, pointOri.z); 637 | ceres::CostFunction *cost_function = LidarDistanceFactor::Create(curr_point, center); 638 | problem.AddResidualBlock(cost_function, loss_function, parameters, parameters + 4); 639 | } 640 | */ 641 | } 642 | 643 | int surf_num = 0; 644 | for (int i = 0; i < laserCloudSurfStackNum; i++) 645 | { 646 | pointOri = laserCloudSurfStack->points[i]; 647 | //double sqrtDis = pointOri.x * pointOri.x + pointOri.y * pointOri.y + pointOri.z * pointOri.z; 648 | pointAssociateToMap(&pointOri, &pointSel); 649 | kdtreeSurfFromMap->nearestKSearch(pointSel, 5, pointSearchInd, pointSearchSqDis); 650 | 651 | Eigen::Matrix matA0; 652 | Eigen::Matrix matB0 = -1 * Eigen::Matrix::Ones(); 653 | if (pointSearchSqDis[4] < 1.0) 654 | { 655 | 656 | for (int j = 0; j < 5; j++) 657 | { 658 | matA0(j, 0) = laserCloudSurfFromMap->points[pointSearchInd[j]].x; 659 | matA0(j, 1) = laserCloudSurfFromMap->points[pointSearchInd[j]].y; 660 | matA0(j, 2) = laserCloudSurfFromMap->points[pointSearchInd[j]].z; 661 | ////printf(" pts %f %f %f ", matA0(j, 0), matA0(j, 1), matA0(j, 2)); 662 | } 663 | // find the norm of plane 664 | Eigen::Vector3d norm = matA0.colPivHouseholderQr().solve(matB0); 665 | double negative_OA_dot_norm = 1 / norm.norm(); 666 | norm.normalize(); 667 | 668 | // Here n(pa, pb, pc) is unit norm of plane 669 | bool planeValid = true; 670 | for (int j = 0; j < 5; j++) 671 | { 672 | // if OX * n > 0.2, then plane is not fit well 673 | if (fabs(norm(0) * laserCloudSurfFromMap->points[pointSearchInd[j]].x + 674 | norm(1) * laserCloudSurfFromMap->points[pointSearchInd[j]].y + 675 | norm(2) * laserCloudSurfFromMap->points[pointSearchInd[j]].z + negative_OA_dot_norm) > 0.2) 676 | { 677 | planeValid = false; 678 | break; 679 | } 680 | } 681 | Eigen::Vector3d curr_point(pointOri.x, pointOri.y, pointOri.z); 682 | if (planeValid) 683 | { 684 | ceres::CostFunction *cost_function = LidarPlaneNormFactor::Create(curr_point, norm, negative_OA_dot_norm); 685 | problem.AddResidualBlock(cost_function, loss_function, parameters, parameters + 4); 686 | surf_num++; 687 | } 688 | } 689 | /* 690 | else if(pointSearchSqDis[4] < 0.01 * sqrtDis) 691 | { 692 | Eigen::Vector3d center(0, 0, 0); 693 | for (int j = 0; j < 5; j++) 694 | { 695 | Eigen::Vector3d tmp(laserCloudSurfFromMap->points[pointSearchInd[j]].x, 696 | laserCloudSurfFromMap->points[pointSearchInd[j]].y, 697 | laserCloudSurfFromMap->points[pointSearchInd[j]].z); 698 | center = center + tmp; 699 | } 700 | center = center / 5.0; 701 | Eigen::Vector3d curr_point(pointOri.x, pointOri.y, pointOri.z); 702 | ceres::CostFunction *cost_function = LidarDistanceFactor::Create(curr_point, center); 703 | problem.AddResidualBlock(cost_function, loss_function, parameters, parameters + 4); 704 | } 705 | */ 706 | } 707 | 708 | ////printf("corner num %d used corner num %d \n", laserCloudCornerStackNum, corner_num); 709 | ////printf("surf num %d used surf num %d \n", laserCloudSurfStackNum, surf_num); 710 | 711 | //printf("mapping data assosiation time %f ms \n", t_data.toc()); 712 | 713 | TicToc t_solver; 714 | ceres::Solver::Options options; 715 | options.linear_solver_type = ceres::DENSE_QR; 716 | options.max_num_iterations = 4; 717 | options.minimizer_progress_to_stdout = false; 718 | options.check_gradients = false; 719 | options.gradient_check_relative_precision = 1e-4; 720 | ceres::Solver::Summary summary; 721 | ceres::Solve(options, &problem, &summary); 722 | //printf("mapping solver time %f ms \n", t_solver.toc()); 723 | 724 | ////printf("time %f \n", timeLaserOdometry); 725 | ////printf("corner factor num %d surf factor num %d\n", corner_num, surf_num); 726 | ////printf("result q %f %f %f %f result t %f %f %f\n", parameters[3], parameters[0], parameters[1], parameters[2], 727 | // parameters[4], parameters[5], parameters[6]); 728 | } 729 | //printf("mapping optimization time %f \n", t_opt.toc()); 730 | } 731 | else 732 | { 733 | ROS_WARN("time Map corner and surf num are not enough"); 734 | } 735 | transformUpdate(); 736 | 737 | TicToc t_add; 738 | for (int i = 0; i < laserCloudCornerStackNum; i++) 739 | { 740 | pointAssociateToMap(&laserCloudCornerStack->points[i], &pointSel); 741 | 742 | int cubeI = int((pointSel.x + 25.0) / 50.0) + laserCloudCenWidth; 743 | int cubeJ = int((pointSel.y + 25.0) / 50.0) + laserCloudCenHeight; 744 | int cubeK = int((pointSel.z + 25.0) / 50.0) + laserCloudCenDepth; 745 | 746 | if (pointSel.x + 25.0 < 0) 747 | cubeI--; 748 | if (pointSel.y + 25.0 < 0) 749 | cubeJ--; 750 | if (pointSel.z + 25.0 < 0) 751 | cubeK--; 752 | 753 | if (cubeI >= 0 && cubeI < laserCloudWidth && 754 | cubeJ >= 0 && cubeJ < laserCloudHeight && 755 | cubeK >= 0 && cubeK < laserCloudDepth) 756 | { 757 | int cubeInd = cubeI + laserCloudWidth * cubeJ + laserCloudWidth * laserCloudHeight * cubeK; 758 | laserCloudCornerArray[cubeInd]->push_back(pointSel); 759 | } 760 | } 761 | 762 | for (int i = 0; i < laserCloudSurfStackNum; i++) 763 | { 764 | pointAssociateToMap(&laserCloudSurfStack->points[i], &pointSel); 765 | 766 | int cubeI = int((pointSel.x + 25.0) / 50.0) + laserCloudCenWidth; 767 | int cubeJ = int((pointSel.y + 25.0) / 50.0) + laserCloudCenHeight; 768 | int cubeK = int((pointSel.z + 25.0) / 50.0) + laserCloudCenDepth; 769 | 770 | if (pointSel.x + 25.0 < 0) 771 | cubeI--; 772 | if (pointSel.y + 25.0 < 0) 773 | cubeJ--; 774 | if (pointSel.z + 25.0 < 0) 775 | cubeK--; 776 | 777 | if (cubeI >= 0 && cubeI < laserCloudWidth && 778 | cubeJ >= 0 && cubeJ < laserCloudHeight && 779 | cubeK >= 0 && cubeK < laserCloudDepth) 780 | { 781 | int cubeInd = cubeI + laserCloudWidth * cubeJ + laserCloudWidth * laserCloudHeight * cubeK; 782 | laserCloudSurfArray[cubeInd]->push_back(pointSel); 783 | } 784 | } 785 | //printf("add points time %f ms\n", t_add.toc()); 786 | 787 | 788 | TicToc t_filter; 789 | for (int i = 0; i < laserCloudValidNum; i++) 790 | { 791 | int ind = laserCloudValidInd[i]; 792 | 793 | pcl::PointCloud::Ptr tmpCorner(new pcl::PointCloud()); 794 | downSizeFilterCorner.setInputCloud(laserCloudCornerArray[ind]); 795 | downSizeFilterCorner.filter(*tmpCorner); 796 | laserCloudCornerArray[ind] = tmpCorner; 797 | 798 | pcl::PointCloud::Ptr tmpSurf(new pcl::PointCloud()); 799 | downSizeFilterSurf.setInputCloud(laserCloudSurfArray[ind]); 800 | downSizeFilterSurf.filter(*tmpSurf); 801 | laserCloudSurfArray[ind] = tmpSurf; 802 | } 803 | //printf("filter time %f ms \n", t_filter.toc()); 804 | 805 | TicToc t_pub; 806 | //publish surround map for every 5 frame 807 | if (frameCount % 5 == 0) 808 | { 809 | laserCloudSurround->clear(); 810 | for (int i = 0; i < laserCloudSurroundNum; i++) 811 | { 812 | int ind = laserCloudSurroundInd[i]; 813 | *laserCloudSurround += *laserCloudCornerArray[ind]; 814 | *laserCloudSurround += *laserCloudSurfArray[ind]; 815 | } 816 | 817 | sensor_msgs::PointCloud2 laserCloudSurround3; 818 | pcl::toROSMsg(*laserCloudSurround, laserCloudSurround3); 819 | laserCloudSurround3.header.stamp = ros::Time().fromSec(timeLaserOdometry); 820 | laserCloudSurround3.header.frame_id = "/camera_init"; 821 | pubLaserCloudSurround.publish(laserCloudSurround3); 822 | } 823 | 824 | if (frameCount % 20 == 0) 825 | { 826 | pcl::PointCloud laserCloudMap; 827 | for (int i = 0; i < 4851; i++) 828 | { 829 | laserCloudMap += *laserCloudCornerArray[i]; 830 | laserCloudMap += *laserCloudSurfArray[i]; 831 | } 832 | sensor_msgs::PointCloud2 laserCloudMsg; 833 | pcl::toROSMsg(laserCloudMap, laserCloudMsg); 834 | laserCloudMsg.header.stamp = ros::Time().fromSec(timeLaserOdometry); 835 | laserCloudMsg.header.frame_id = "/camera_init"; 836 | pubLaserCloudMap.publish(laserCloudMsg); 837 | } 838 | 839 | sensor_msgs::PointCloud2 laserCloudFullRes3Local; 840 | pcl::toROSMsg(*laserCloudFullRes, laserCloudFullRes3Local); 841 | laserCloudFullRes3Local.header.stamp = ros::Time().fromSec(timeLaserOdometry); 842 | laserCloudFullRes3Local.header.frame_id = "/camera_init"; 843 | pubLaserCloudFullResLocal.publish(laserCloudFullRes3Local); 844 | 845 | int laserCloudFullResNum = laserCloudFullRes->points.size(); 846 | for (int i = 0; i < laserCloudFullResNum; i++) 847 | { 848 | pointAssociateToMap(&laserCloudFullRes->points[i], &laserCloudFullRes->points[i]); 849 | } 850 | 851 | sensor_msgs::PointCloud2 laserCloudFullRes3; 852 | pcl::toROSMsg(*laserCloudFullRes, laserCloudFullRes3); 853 | laserCloudFullRes3.header.stamp = ros::Time().fromSec(timeLaserOdometry); 854 | laserCloudFullRes3.header.frame_id = "/camera_init"; 855 | pubLaserCloudFullRes.publish(laserCloudFullRes3); 856 | 857 | //printf("mapping pub time %f ms \n", t_pub.toc()); 858 | 859 | //printf("whole mapping time %f ms ++++++++++\n", t_whole.toc()); 860 | 861 | nav_msgs::Odometry odomAftMapped; 862 | odomAftMapped.header.frame_id = "/camera_init"; 863 | odomAftMapped.child_frame_id = "/aft_mapped"; 864 | odomAftMapped.header.stamp = ros::Time().fromSec(timeLaserOdometry); 865 | odomAftMapped.pose.pose.orientation.x = q_w_curr.x(); 866 | odomAftMapped.pose.pose.orientation.y = q_w_curr.y(); 867 | odomAftMapped.pose.pose.orientation.z = q_w_curr.z(); 868 | odomAftMapped.pose.pose.orientation.w = q_w_curr.w(); 869 | odomAftMapped.pose.pose.position.x = t_w_curr.x(); 870 | odomAftMapped.pose.pose.position.y = t_w_curr.y(); 871 | odomAftMapped.pose.pose.position.z = t_w_curr.z(); 872 | pubOdomAftMapped.publish(odomAftMapped); 873 | 874 | // double roll, pitch, yaw; 875 | // geometry_msgs::Quaternion quat = odomAftMapped.pose.pose.orientation; 876 | // tf::Matrix3x3(tf::Quaternion(quat.x, quat.y, quat.z, quat.w)).getRPY(roll, pitch, yaw); 877 | // std::cout << "pub - roll, pitch, yaw (deg): " << rad2deg(roll) << ", " << rad2deg(pitch) << ", " << rad2deg(yaw) << std::endl; 878 | 879 | geometry_msgs::PoseStamped laserAfterMappedPose; 880 | laserAfterMappedPose.header = odomAftMapped.header; 881 | laserAfterMappedPose.pose = odomAftMapped.pose.pose; 882 | 883 | laserAfterMappedPath.header.stamp = odomAftMapped.header.stamp; 884 | laserAfterMappedPath.header.frame_id = "/camera_init"; 885 | laserAfterMappedPath.poses.push_back(laserAfterMappedPose); 886 | pubLaserAfterMappedPath.publish(laserAfterMappedPath); 887 | 888 | static tf::TransformBroadcaster br; 889 | tf::Transform transform; 890 | tf::Quaternion q; 891 | transform.setOrigin(tf::Vector3(t_w_curr(0), 892 | t_w_curr(1), 893 | t_w_curr(2))); 894 | q.setW(q_w_curr.w()); 895 | q.setX(q_w_curr.x()); 896 | q.setY(q_w_curr.y()); 897 | q.setZ(q_w_curr.z()); 898 | transform.setRotation(q); 899 | br.sendTransform(tf::StampedTransform(transform, odomAftMapped.header.stamp, "/camera_init", "/aft_mapped")); 900 | 901 | frameCount++; 902 | } 903 | std::chrono::milliseconds dura(2); 904 | std::this_thread::sleep_for(dura); 905 | } 906 | } 907 | 908 | int main(int argc, char **argv) 909 | { 910 | ros::init(argc, argv, "laserMapping"); 911 | ros::NodeHandle nh; 912 | 913 | float lineRes = 0; 914 | float planeRes = 0; 915 | nh.param("mapping_line_resolution", lineRes, 0.4); 916 | nh.param("mapping_plane_resolution", planeRes, 0.8); 917 | //printf("line resolution %f plane resolution %f \n", lineRes, planeRes); 918 | downSizeFilterCorner.setLeafSize(lineRes, lineRes,lineRes); 919 | downSizeFilterSurf.setLeafSize(planeRes, planeRes, planeRes); 920 | 921 | ros::Subscriber subLaserCloudCornerLast = nh.subscribe("/laser_cloud_corner_last", 100, laserCloudCornerLastHandler); 922 | 923 | ros::Subscriber subLaserCloudSurfLast = nh.subscribe("/laser_cloud_surf_last", 100, laserCloudSurfLastHandler); 924 | 925 | ros::Subscriber subLaserOdometry = nh.subscribe("/laser_odom_to_init", 100, laserOdometryHandler); 926 | 927 | ros::Subscriber subLaserCloudFullRes = nh.subscribe("/velodyne_cloud_3", 100, laserCloudFullResHandler); 928 | 929 | pubLaserCloudSurround = nh.advertise("/laser_cloud_surround", 100); 930 | 931 | pubLaserCloudMap = nh.advertise("/laser_cloud_map", 100); 932 | 933 | pubLaserCloudFullRes = nh.advertise("/velodyne_cloud_registered", 100); 934 | pubLaserCloudFullResLocal = nh.advertise("/velodyne_cloud_registered_local", 100); 935 | 936 | pubOdomAftMapped = nh.advertise("/aft_mapped_to_init", 100); 937 | 938 | pubOdomAftMappedHighFrec = nh.advertise("/aft_mapped_to_init_high_frec", 100); 939 | 940 | pubLaserAfterMappedPath = nh.advertise("/aft_mapped_path", 100); 941 | 942 | for (int i = 0; i < laserCloudNum; i++) 943 | { 944 | laserCloudCornerArray[i].reset(new pcl::PointCloud()); 945 | laserCloudSurfArray[i].reset(new pcl::PointCloud()); 946 | } 947 | 948 | std::thread mapping_process{process}; 949 | 950 | ros::spin(); 951 | 952 | return 0; 953 | } -------------------------------------------------------------------------------- /src/laserOdometry.cpp: -------------------------------------------------------------------------------- 1 | // This is an advanced implementation of the algorithm described in the following paper: 2 | // J. Zhang and S. Singh. LOAM: Lidar Odometry and Mapping in Real-time. 3 | // Robotics: Science and Systems Conference (RSS). Berkeley, CA, July 2014. 4 | 5 | // Modifier: Tong Qin qintonguav@gmail.com 6 | // Shaozu Cao saozu.cao@connect.ust.hk 7 | 8 | 9 | // Copyright 2013, Ji Zhang, Carnegie Mellon University 10 | // Further contributions copyright (c) 2016, Southwest Research Institute 11 | // All rights reserved. 12 | // 13 | // Redistribution and use in source and binary forms, with or without 14 | // modification, are permitted provided that the following conditions are met: 15 | // 16 | // 1. Redistributions of source code must retain the above copyright notice, 17 | // this list of conditions and the following disclaimer. 18 | // 2. Redistributions in binary form must reproduce the above copyright notice, 19 | // this list of conditions and the following disclaimer in the documentation 20 | // and/or other materials provided with the distribution. 21 | // 3. Neither the name of the copyright holder nor the names of its 22 | // contributors may be used to endorse or promote products derived from this 23 | // software without specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 26 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 27 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 28 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 29 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 30 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 31 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 32 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 33 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 34 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 35 | // POSSIBILITY OF SUCH DAMAGE. 36 | 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | #include 53 | #include 54 | 55 | #include "aloam_velodyne/common.h" 56 | #include "aloam_velodyne/tic_toc.h" 57 | #include "lidarFactor.hpp" 58 | 59 | #define DISTORTION 0 60 | 61 | 62 | int corner_correspondence = 0, plane_correspondence = 0; 63 | 64 | constexpr double SCAN_PERIOD = 0.1; 65 | constexpr double DISTANCE_SQ_THRESHOLD = 25; 66 | constexpr double NEARBY_SCAN = 2.5; 67 | 68 | int skipFrameNum = 5; 69 | bool systemInited = false; 70 | 71 | double timeCornerPointsSharp = 0; 72 | double timeCornerPointsLessSharp = 0; 73 | double timeSurfPointsFlat = 0; 74 | double timeSurfPointsLessFlat = 0; 75 | double timeLaserCloudFullRes = 0; 76 | 77 | pcl::KdTreeFLANN::Ptr kdtreeCornerLast(new pcl::KdTreeFLANN()); 78 | pcl::KdTreeFLANN::Ptr kdtreeSurfLast(new pcl::KdTreeFLANN()); 79 | 80 | pcl::PointCloud::Ptr cornerPointsSharp(new pcl::PointCloud()); 81 | pcl::PointCloud::Ptr cornerPointsLessSharp(new pcl::PointCloud()); 82 | pcl::PointCloud::Ptr surfPointsFlat(new pcl::PointCloud()); 83 | pcl::PointCloud::Ptr surfPointsLessFlat(new pcl::PointCloud()); 84 | 85 | pcl::PointCloud::Ptr laserCloudCornerLast(new pcl::PointCloud()); 86 | pcl::PointCloud::Ptr laserCloudSurfLast(new pcl::PointCloud()); 87 | pcl::PointCloud::Ptr laserCloudFullRes(new pcl::PointCloud()); 88 | 89 | int laserCloudCornerLastNum = 0; 90 | int laserCloudSurfLastNum = 0; 91 | 92 | // Transformation from current frame to world frame 93 | Eigen::Quaterniond q_w_curr(1, 0, 0, 0); 94 | Eigen::Vector3d t_w_curr(0, 0, 0); 95 | 96 | // q_curr_last(x, y, z, w), t_curr_last 97 | double para_q[4] = {0, 0, 0, 1}; 98 | double para_t[3] = {0, 0, 0}; 99 | 100 | Eigen::Map q_last_curr(para_q); 101 | Eigen::Map t_last_curr(para_t); 102 | 103 | std::queue cornerSharpBuf; 104 | std::queue cornerLessSharpBuf; 105 | std::queue surfFlatBuf; 106 | std::queue surfLessFlatBuf; 107 | std::queue fullPointsBuf; 108 | std::mutex mBuf; 109 | 110 | // undistort lidar point 111 | void TransformToStart(PointType const *const pi, PointType *const po) 112 | { 113 | //interpolation ratio 114 | double s; 115 | if (DISTORTION) 116 | s = (pi->intensity - int(pi->intensity)) / SCAN_PERIOD; 117 | else 118 | s = 1.0; 119 | //s = 1; 120 | Eigen::Quaterniond q_point_last = Eigen::Quaterniond::Identity().slerp(s, q_last_curr); 121 | Eigen::Vector3d t_point_last = s * t_last_curr; 122 | Eigen::Vector3d point(pi->x, pi->y, pi->z); 123 | Eigen::Vector3d un_point = q_point_last * point + t_point_last; 124 | 125 | po->x = un_point.x(); 126 | po->y = un_point.y(); 127 | po->z = un_point.z(); 128 | po->intensity = pi->intensity; 129 | } 130 | 131 | // transform all lidar points to the start of the next frame 132 | 133 | void TransformToEnd(PointType const *const pi, PointType *const po) 134 | { 135 | // undistort point first 136 | pcl::PointXYZI un_point_tmp; 137 | TransformToStart(pi, &un_point_tmp); 138 | 139 | Eigen::Vector3d un_point(un_point_tmp.x, un_point_tmp.y, un_point_tmp.z); 140 | Eigen::Vector3d point_end = q_last_curr.inverse() * (un_point - t_last_curr); 141 | 142 | po->x = point_end.x(); 143 | po->y = point_end.y(); 144 | po->z = point_end.z(); 145 | 146 | //Remove distortion time info 147 | po->intensity = int(pi->intensity); 148 | } 149 | 150 | void laserCloudSharpHandler(const sensor_msgs::PointCloud2ConstPtr &cornerPointsSharp2) 151 | { 152 | mBuf.lock(); 153 | cornerSharpBuf.push(cornerPointsSharp2); 154 | mBuf.unlock(); 155 | } 156 | 157 | void laserCloudLessSharpHandler(const sensor_msgs::PointCloud2ConstPtr &cornerPointsLessSharp2) 158 | { 159 | mBuf.lock(); 160 | cornerLessSharpBuf.push(cornerPointsLessSharp2); 161 | mBuf.unlock(); 162 | } 163 | 164 | void laserCloudFlatHandler(const sensor_msgs::PointCloud2ConstPtr &surfPointsFlat2) 165 | { 166 | mBuf.lock(); 167 | surfFlatBuf.push(surfPointsFlat2); 168 | mBuf.unlock(); 169 | } 170 | 171 | void laserCloudLessFlatHandler(const sensor_msgs::PointCloud2ConstPtr &surfPointsLessFlat2) 172 | { 173 | mBuf.lock(); 174 | surfLessFlatBuf.push(surfPointsLessFlat2); 175 | mBuf.unlock(); 176 | } 177 | 178 | //receive all point cloud 179 | void laserCloudFullResHandler(const sensor_msgs::PointCloud2ConstPtr &laserCloudFullRes2) 180 | { 181 | mBuf.lock(); 182 | fullPointsBuf.push(laserCloudFullRes2); 183 | mBuf.unlock(); 184 | } 185 | 186 | int main(int argc, char **argv) 187 | { 188 | ros::init(argc, argv, "laserOdometry"); 189 | ros::NodeHandle nh; 190 | 191 | nh.param("mapping_skip_frame", skipFrameNum, 2); 192 | 193 | //printf("Mapping %d Hz \n", 10 / skipFrameNum); 194 | 195 | ros::Subscriber subCornerPointsSharp = nh.subscribe("/laser_cloud_sharp", 100, laserCloudSharpHandler); 196 | 197 | ros::Subscriber subCornerPointsLessSharp = nh.subscribe("/laser_cloud_less_sharp", 100, laserCloudLessSharpHandler); 198 | 199 | ros::Subscriber subSurfPointsFlat = nh.subscribe("/laser_cloud_flat", 100, laserCloudFlatHandler); 200 | 201 | ros::Subscriber subSurfPointsLessFlat = nh.subscribe("/laser_cloud_less_flat", 100, laserCloudLessFlatHandler); 202 | 203 | ros::Subscriber subLaserCloudFullRes = nh.subscribe("/velodyne_cloud_2", 100, laserCloudFullResHandler); 204 | 205 | ros::Publisher pubLaserCloudCornerLast = nh.advertise("/laser_cloud_corner_last", 100); 206 | 207 | ros::Publisher pubLaserCloudSurfLast = nh.advertise("/laser_cloud_surf_last", 100); 208 | 209 | ros::Publisher pubLaserCloudFullRes = nh.advertise("/velodyne_cloud_3", 100); 210 | 211 | ros::Publisher pubLaserOdometry = nh.advertise("/laser_odom_to_init", 100); 212 | 213 | ros::Publisher pubLaserPath = nh.advertise("/laser_odom_path", 100); 214 | 215 | nav_msgs::Path laserPath; 216 | 217 | int frameCount = 0; 218 | ros::Rate rate(100); 219 | 220 | while (ros::ok()) 221 | { 222 | ros::spinOnce(); 223 | 224 | if (!cornerSharpBuf.empty() && !cornerLessSharpBuf.empty() && 225 | !surfFlatBuf.empty() && !surfLessFlatBuf.empty() && 226 | !fullPointsBuf.empty()) 227 | { 228 | timeCornerPointsSharp = cornerSharpBuf.front()->header.stamp.toSec(); 229 | timeCornerPointsLessSharp = cornerLessSharpBuf.front()->header.stamp.toSec(); 230 | timeSurfPointsFlat = surfFlatBuf.front()->header.stamp.toSec(); 231 | timeSurfPointsLessFlat = surfLessFlatBuf.front()->header.stamp.toSec(); 232 | timeLaserCloudFullRes = fullPointsBuf.front()->header.stamp.toSec(); 233 | 234 | if (timeCornerPointsSharp != timeLaserCloudFullRes || 235 | timeCornerPointsLessSharp != timeLaserCloudFullRes || 236 | timeSurfPointsFlat != timeLaserCloudFullRes || 237 | timeSurfPointsLessFlat != timeLaserCloudFullRes) 238 | { 239 | //printf("unsync messeage!"); 240 | ROS_BREAK(); 241 | } 242 | 243 | mBuf.lock(); 244 | cornerPointsSharp->clear(); 245 | pcl::fromROSMsg(*cornerSharpBuf.front(), *cornerPointsSharp); 246 | cornerSharpBuf.pop(); 247 | 248 | cornerPointsLessSharp->clear(); 249 | pcl::fromROSMsg(*cornerLessSharpBuf.front(), *cornerPointsLessSharp); 250 | cornerLessSharpBuf.pop(); 251 | 252 | surfPointsFlat->clear(); 253 | pcl::fromROSMsg(*surfFlatBuf.front(), *surfPointsFlat); 254 | surfFlatBuf.pop(); 255 | 256 | surfPointsLessFlat->clear(); 257 | pcl::fromROSMsg(*surfLessFlatBuf.front(), *surfPointsLessFlat); 258 | surfLessFlatBuf.pop(); 259 | 260 | laserCloudFullRes->clear(); 261 | pcl::fromROSMsg(*fullPointsBuf.front(), *laserCloudFullRes); 262 | fullPointsBuf.pop(); 263 | mBuf.unlock(); 264 | 265 | TicToc t_whole; 266 | // initializing 267 | if (!systemInited) 268 | { 269 | systemInited = true; 270 | std::cout << "Initialization finished \n"; 271 | } 272 | else 273 | { 274 | int cornerPointsSharpNum = cornerPointsSharp->points.size(); 275 | int surfPointsFlatNum = surfPointsFlat->points.size(); 276 | 277 | TicToc t_opt; 278 | for (size_t opti_counter = 0; opti_counter < 2; ++opti_counter) 279 | { 280 | corner_correspondence = 0; 281 | plane_correspondence = 0; 282 | 283 | //ceres::LossFunction *loss_function = NULL; 284 | ceres::LossFunction *loss_function = new ceres::HuberLoss(0.1); 285 | ceres::LocalParameterization *q_parameterization = 286 | new ceres::EigenQuaternionParameterization(); 287 | ceres::Problem::Options problem_options; 288 | 289 | ceres::Problem problem(problem_options); 290 | problem.AddParameterBlock(para_q, 4, q_parameterization); 291 | problem.AddParameterBlock(para_t, 3); 292 | 293 | pcl::PointXYZI pointSel; 294 | std::vector pointSearchInd; 295 | std::vector pointSearchSqDis; 296 | 297 | TicToc t_data; 298 | // find correspondence for corner features 299 | for (int i = 0; i < cornerPointsSharpNum; ++i) 300 | { 301 | TransformToStart(&(cornerPointsSharp->points[i]), &pointSel); 302 | kdtreeCornerLast->nearestKSearch(pointSel, 1, pointSearchInd, pointSearchSqDis); 303 | 304 | int closestPointInd = -1, minPointInd2 = -1; 305 | if (pointSearchSqDis[0] < DISTANCE_SQ_THRESHOLD) 306 | { 307 | closestPointInd = pointSearchInd[0]; 308 | int closestPointScanID = int(laserCloudCornerLast->points[closestPointInd].intensity); 309 | 310 | double minPointSqDis2 = DISTANCE_SQ_THRESHOLD; 311 | // search in the direction of increasing scan line 312 | for (int j = closestPointInd + 1; j < (int)laserCloudCornerLast->points.size(); ++j) 313 | { 314 | // if in the same scan line, continue 315 | if (int(laserCloudCornerLast->points[j].intensity) <= closestPointScanID) 316 | continue; 317 | 318 | // if not in nearby scans, end the loop 319 | if (int(laserCloudCornerLast->points[j].intensity) > (closestPointScanID + NEARBY_SCAN)) 320 | break; 321 | 322 | double pointSqDis = (laserCloudCornerLast->points[j].x - pointSel.x) * 323 | (laserCloudCornerLast->points[j].x - pointSel.x) + 324 | (laserCloudCornerLast->points[j].y - pointSel.y) * 325 | (laserCloudCornerLast->points[j].y - pointSel.y) + 326 | (laserCloudCornerLast->points[j].z - pointSel.z) * 327 | (laserCloudCornerLast->points[j].z - pointSel.z); 328 | 329 | if (pointSqDis < minPointSqDis2) 330 | { 331 | // find nearer point 332 | minPointSqDis2 = pointSqDis; 333 | minPointInd2 = j; 334 | } 335 | } 336 | 337 | // search in the direction of decreasing scan line 338 | for (int j = closestPointInd - 1; j >= 0; --j) 339 | { 340 | // if in the same scan line, continue 341 | if (int(laserCloudCornerLast->points[j].intensity) >= closestPointScanID) 342 | continue; 343 | 344 | // if not in nearby scans, end the loop 345 | if (int(laserCloudCornerLast->points[j].intensity) < (closestPointScanID - NEARBY_SCAN)) 346 | break; 347 | 348 | double pointSqDis = (laserCloudCornerLast->points[j].x - pointSel.x) * 349 | (laserCloudCornerLast->points[j].x - pointSel.x) + 350 | (laserCloudCornerLast->points[j].y - pointSel.y) * 351 | (laserCloudCornerLast->points[j].y - pointSel.y) + 352 | (laserCloudCornerLast->points[j].z - pointSel.z) * 353 | (laserCloudCornerLast->points[j].z - pointSel.z); 354 | 355 | if (pointSqDis < minPointSqDis2) 356 | { 357 | // find nearer point 358 | minPointSqDis2 = pointSqDis; 359 | minPointInd2 = j; 360 | } 361 | } 362 | } 363 | if (minPointInd2 >= 0) // both closestPointInd and minPointInd2 is valid 364 | { 365 | Eigen::Vector3d curr_point(cornerPointsSharp->points[i].x, 366 | cornerPointsSharp->points[i].y, 367 | cornerPointsSharp->points[i].z); 368 | Eigen::Vector3d last_point_a(laserCloudCornerLast->points[closestPointInd].x, 369 | laserCloudCornerLast->points[closestPointInd].y, 370 | laserCloudCornerLast->points[closestPointInd].z); 371 | Eigen::Vector3d last_point_b(laserCloudCornerLast->points[minPointInd2].x, 372 | laserCloudCornerLast->points[minPointInd2].y, 373 | laserCloudCornerLast->points[minPointInd2].z); 374 | 375 | double s; 376 | if (DISTORTION) 377 | s = (cornerPointsSharp->points[i].intensity - int(cornerPointsSharp->points[i].intensity)) / SCAN_PERIOD; 378 | else 379 | s = 1.0; 380 | ceres::CostFunction *cost_function = LidarEdgeFactor::Create(curr_point, last_point_a, last_point_b, s); 381 | problem.AddResidualBlock(cost_function, loss_function, para_q, para_t); 382 | corner_correspondence++; 383 | } 384 | } 385 | 386 | // find correspondence for plane features 387 | for (int i = 0; i < surfPointsFlatNum; ++i) 388 | { 389 | TransformToStart(&(surfPointsFlat->points[i]), &pointSel); 390 | kdtreeSurfLast->nearestKSearch(pointSel, 1, pointSearchInd, pointSearchSqDis); 391 | 392 | int closestPointInd = -1, minPointInd2 = -1, minPointInd3 = -1; 393 | if (pointSearchSqDis[0] < DISTANCE_SQ_THRESHOLD) 394 | { 395 | closestPointInd = pointSearchInd[0]; 396 | 397 | // get closest point's scan ID 398 | int closestPointScanID = int(laserCloudSurfLast->points[closestPointInd].intensity); 399 | double minPointSqDis2 = DISTANCE_SQ_THRESHOLD, minPointSqDis3 = DISTANCE_SQ_THRESHOLD; 400 | 401 | // search in the direction of increasing scan line 402 | for (int j = closestPointInd + 1; j < (int)laserCloudSurfLast->points.size(); ++j) 403 | { 404 | // if not in nearby scans, end the loop 405 | if (int(laserCloudSurfLast->points[j].intensity) > (closestPointScanID + NEARBY_SCAN)) 406 | break; 407 | 408 | double pointSqDis = (laserCloudSurfLast->points[j].x - pointSel.x) * 409 | (laserCloudSurfLast->points[j].x - pointSel.x) + 410 | (laserCloudSurfLast->points[j].y - pointSel.y) * 411 | (laserCloudSurfLast->points[j].y - pointSel.y) + 412 | (laserCloudSurfLast->points[j].z - pointSel.z) * 413 | (laserCloudSurfLast->points[j].z - pointSel.z); 414 | 415 | // if in the same or lower scan line 416 | if (int(laserCloudSurfLast->points[j].intensity) <= closestPointScanID && pointSqDis < minPointSqDis2) 417 | { 418 | minPointSqDis2 = pointSqDis; 419 | minPointInd2 = j; 420 | } 421 | // if in the higher scan line 422 | else if (int(laserCloudSurfLast->points[j].intensity) > closestPointScanID && pointSqDis < minPointSqDis3) 423 | { 424 | minPointSqDis3 = pointSqDis; 425 | minPointInd3 = j; 426 | } 427 | } 428 | 429 | // search in the direction of decreasing scan line 430 | for (int j = closestPointInd - 1; j >= 0; --j) 431 | { 432 | // if not in nearby scans, end the loop 433 | if (int(laserCloudSurfLast->points[j].intensity) < (closestPointScanID - NEARBY_SCAN)) 434 | break; 435 | 436 | double pointSqDis = (laserCloudSurfLast->points[j].x - pointSel.x) * 437 | (laserCloudSurfLast->points[j].x - pointSel.x) + 438 | (laserCloudSurfLast->points[j].y - pointSel.y) * 439 | (laserCloudSurfLast->points[j].y - pointSel.y) + 440 | (laserCloudSurfLast->points[j].z - pointSel.z) * 441 | (laserCloudSurfLast->points[j].z - pointSel.z); 442 | 443 | // if in the same or higher scan line 444 | if (int(laserCloudSurfLast->points[j].intensity) >= closestPointScanID && pointSqDis < minPointSqDis2) 445 | { 446 | minPointSqDis2 = pointSqDis; 447 | minPointInd2 = j; 448 | } 449 | else if (int(laserCloudSurfLast->points[j].intensity) < closestPointScanID && pointSqDis < minPointSqDis3) 450 | { 451 | // find nearer point 452 | minPointSqDis3 = pointSqDis; 453 | minPointInd3 = j; 454 | } 455 | } 456 | 457 | if (minPointInd2 >= 0 && minPointInd3 >= 0) 458 | { 459 | 460 | Eigen::Vector3d curr_point(surfPointsFlat->points[i].x, 461 | surfPointsFlat->points[i].y, 462 | surfPointsFlat->points[i].z); 463 | Eigen::Vector3d last_point_a(laserCloudSurfLast->points[closestPointInd].x, 464 | laserCloudSurfLast->points[closestPointInd].y, 465 | laserCloudSurfLast->points[closestPointInd].z); 466 | Eigen::Vector3d last_point_b(laserCloudSurfLast->points[minPointInd2].x, 467 | laserCloudSurfLast->points[minPointInd2].y, 468 | laserCloudSurfLast->points[minPointInd2].z); 469 | Eigen::Vector3d last_point_c(laserCloudSurfLast->points[minPointInd3].x, 470 | laserCloudSurfLast->points[minPointInd3].y, 471 | laserCloudSurfLast->points[minPointInd3].z); 472 | 473 | double s; 474 | if (DISTORTION) 475 | s = (surfPointsFlat->points[i].intensity - int(surfPointsFlat->points[i].intensity)) / SCAN_PERIOD; 476 | else 477 | s = 1.0; 478 | ceres::CostFunction *cost_function = LidarPlaneFactor::Create(curr_point, last_point_a, last_point_b, last_point_c, s); 479 | problem.AddResidualBlock(cost_function, loss_function, para_q, para_t); 480 | plane_correspondence++; 481 | } 482 | } 483 | } 484 | 485 | ////printf("coner_correspondance %d, plane_correspondence %d \n", corner_correspondence, plane_correspondence); 486 | //printf("data association time %f ms \n", t_data.toc()); 487 | 488 | if ((corner_correspondence + plane_correspondence) < 10) 489 | { 490 | //printf("less correspondence! *************************************************\n"); 491 | } 492 | 493 | TicToc t_solver; 494 | ceres::Solver::Options options; 495 | options.linear_solver_type = ceres::DENSE_QR; 496 | options.max_num_iterations = 4; 497 | options.minimizer_progress_to_stdout = false; 498 | ceres::Solver::Summary summary; 499 | ceres::Solve(options, &problem, &summary); 500 | //printf("solver time %f ms \n", t_solver.toc()); 501 | } 502 | //printf("optimization twice time %f \n", t_opt.toc()); 503 | 504 | t_w_curr = t_w_curr + q_w_curr * t_last_curr; 505 | q_w_curr = q_w_curr * q_last_curr; 506 | } 507 | 508 | TicToc t_pub; 509 | 510 | // publish odometry 511 | nav_msgs::Odometry laserOdometry; 512 | laserOdometry.header.frame_id = "/camera_init"; 513 | laserOdometry.child_frame_id = "/laser_odom"; 514 | laserOdometry.header.stamp = ros::Time().fromSec(timeSurfPointsLessFlat); 515 | laserOdometry.pose.pose.orientation.x = q_w_curr.x(); 516 | laserOdometry.pose.pose.orientation.y = q_w_curr.y(); 517 | laserOdometry.pose.pose.orientation.z = q_w_curr.z(); 518 | laserOdometry.pose.pose.orientation.w = q_w_curr.w(); 519 | laserOdometry.pose.pose.position.x = t_w_curr.x(); 520 | laserOdometry.pose.pose.position.y = t_w_curr.y(); 521 | laserOdometry.pose.pose.position.z = t_w_curr.z(); 522 | pubLaserOdometry.publish(laserOdometry); 523 | 524 | geometry_msgs::PoseStamped laserPose; 525 | laserPose.header = laserOdometry.header; 526 | laserPose.pose = laserOdometry.pose.pose; 527 | laserPath.header.stamp = laserOdometry.header.stamp; 528 | laserPath.poses.push_back(laserPose); 529 | laserPath.header.frame_id = "/camera_init"; 530 | pubLaserPath.publish(laserPath); 531 | 532 | // transform corner features and plane features to the scan end point 533 | if (0) 534 | { 535 | int cornerPointsLessSharpNum = cornerPointsLessSharp->points.size(); 536 | for (int i = 0; i < cornerPointsLessSharpNum; i++) 537 | { 538 | TransformToEnd(&cornerPointsLessSharp->points[i], &cornerPointsLessSharp->points[i]); 539 | } 540 | 541 | int surfPointsLessFlatNum = surfPointsLessFlat->points.size(); 542 | for (int i = 0; i < surfPointsLessFlatNum; i++) 543 | { 544 | TransformToEnd(&surfPointsLessFlat->points[i], &surfPointsLessFlat->points[i]); 545 | } 546 | 547 | int laserCloudFullResNum = laserCloudFullRes->points.size(); 548 | for (int i = 0; i < laserCloudFullResNum; i++) 549 | { 550 | TransformToEnd(&laserCloudFullRes->points[i], &laserCloudFullRes->points[i]); 551 | } 552 | } 553 | 554 | pcl::PointCloud::Ptr laserCloudTemp = cornerPointsLessSharp; 555 | cornerPointsLessSharp = laserCloudCornerLast; 556 | laserCloudCornerLast = laserCloudTemp; 557 | 558 | laserCloudTemp = surfPointsLessFlat; 559 | surfPointsLessFlat = laserCloudSurfLast; 560 | laserCloudSurfLast = laserCloudTemp; 561 | 562 | laserCloudCornerLastNum = laserCloudCornerLast->points.size(); 563 | laserCloudSurfLastNum = laserCloudSurfLast->points.size(); 564 | 565 | // std::cout << "the size of corner last is " << laserCloudCornerLastNum << ", and the size of surf last is " << laserCloudSurfLastNum << '\n'; 566 | 567 | kdtreeCornerLast->setInputCloud(laserCloudCornerLast); 568 | kdtreeSurfLast->setInputCloud(laserCloudSurfLast); 569 | 570 | if (frameCount % skipFrameNum == 0) 571 | { 572 | frameCount = 0; 573 | 574 | sensor_msgs::PointCloud2 laserCloudCornerLast2; 575 | pcl::toROSMsg(*laserCloudCornerLast, laserCloudCornerLast2); 576 | laserCloudCornerLast2.header.stamp = ros::Time().fromSec(timeSurfPointsLessFlat); 577 | laserCloudCornerLast2.header.frame_id = "/camera"; 578 | pubLaserCloudCornerLast.publish(laserCloudCornerLast2); 579 | 580 | sensor_msgs::PointCloud2 laserCloudSurfLast2; 581 | pcl::toROSMsg(*laserCloudSurfLast, laserCloudSurfLast2); 582 | laserCloudSurfLast2.header.stamp = ros::Time().fromSec(timeSurfPointsLessFlat); 583 | laserCloudSurfLast2.header.frame_id = "/camera"; 584 | pubLaserCloudSurfLast.publish(laserCloudSurfLast2); 585 | 586 | sensor_msgs::PointCloud2 laserCloudFullRes3; 587 | pcl::toROSMsg(*laserCloudFullRes, laserCloudFullRes3); 588 | laserCloudFullRes3.header.stamp = ros::Time().fromSec(timeSurfPointsLessFlat); 589 | laserCloudFullRes3.header.frame_id = "/camera"; 590 | pubLaserCloudFullRes.publish(laserCloudFullRes3); 591 | } 592 | //printf("publication time %f ms \n", t_pub.toc()); 593 | //printf("whole laserOdometry time %f ms \n", t_whole.toc()); 594 | if(t_whole.toc() > 100) 595 | ROS_WARN("odometry process over 100ms"); 596 | 597 | frameCount++; 598 | } 599 | rate.sleep(); 600 | } 601 | return 0; 602 | } -------------------------------------------------------------------------------- /src/lidarFactor.hpp: -------------------------------------------------------------------------------- 1 | // Author: Tong Qin qintonguav@gmail.com 2 | // Shaozu Cao saozu.cao@connect.ust.hk 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | struct LidarEdgeFactor 13 | { 14 | LidarEdgeFactor(Eigen::Vector3d curr_point_, Eigen::Vector3d last_point_a_, 15 | Eigen::Vector3d last_point_b_, double s_) 16 | : curr_point(curr_point_), last_point_a(last_point_a_), last_point_b(last_point_b_), s(s_) {} 17 | 18 | template 19 | bool operator()(const T *q, const T *t, T *residual) const 20 | { 21 | 22 | Eigen::Matrix cp{T(curr_point.x()), T(curr_point.y()), T(curr_point.z())}; 23 | Eigen::Matrix lpa{T(last_point_a.x()), T(last_point_a.y()), T(last_point_a.z())}; 24 | Eigen::Matrix lpb{T(last_point_b.x()), T(last_point_b.y()), T(last_point_b.z())}; 25 | 26 | //Eigen::Quaternion q_last_curr{q[3], T(s) * q[0], T(s) * q[1], T(s) * q[2]}; 27 | Eigen::Quaternion q_last_curr{q[3], q[0], q[1], q[2]}; 28 | Eigen::Quaternion q_identity{T(1), T(0), T(0), T(0)}; 29 | q_last_curr = q_identity.slerp(T(s), q_last_curr); 30 | Eigen::Matrix t_last_curr{T(s) * t[0], T(s) * t[1], T(s) * t[2]}; 31 | 32 | Eigen::Matrix lp; 33 | lp = q_last_curr * cp + t_last_curr; 34 | 35 | Eigen::Matrix nu = (lp - lpa).cross(lp - lpb); 36 | Eigen::Matrix de = lpa - lpb; 37 | 38 | residual[0] = nu.x() / de.norm(); 39 | residual[1] = nu.y() / de.norm(); 40 | residual[2] = nu.z() / de.norm(); 41 | 42 | return true; 43 | } 44 | 45 | static ceres::CostFunction *Create(const Eigen::Vector3d curr_point_, const Eigen::Vector3d last_point_a_, 46 | const Eigen::Vector3d last_point_b_, const double s_) 47 | { 48 | return (new ceres::AutoDiffCostFunction< 49 | LidarEdgeFactor, 3, 4, 3>( 50 | new LidarEdgeFactor(curr_point_, last_point_a_, last_point_b_, s_))); 51 | } 52 | 53 | Eigen::Vector3d curr_point, last_point_a, last_point_b; 54 | double s; 55 | }; 56 | 57 | struct LidarPlaneFactor 58 | { 59 | LidarPlaneFactor(Eigen::Vector3d curr_point_, Eigen::Vector3d last_point_j_, 60 | Eigen::Vector3d last_point_l_, Eigen::Vector3d last_point_m_, double s_) 61 | : curr_point(curr_point_), last_point_j(last_point_j_), last_point_l(last_point_l_), 62 | last_point_m(last_point_m_), s(s_) 63 | { 64 | ljm_norm = (last_point_j - last_point_l).cross(last_point_j - last_point_m); 65 | ljm_norm.normalize(); 66 | } 67 | 68 | template 69 | bool operator()(const T *q, const T *t, T *residual) const 70 | { 71 | 72 | Eigen::Matrix cp{T(curr_point.x()), T(curr_point.y()), T(curr_point.z())}; 73 | Eigen::Matrix lpj{T(last_point_j.x()), T(last_point_j.y()), T(last_point_j.z())}; 74 | //Eigen::Matrix lpl{T(last_point_l.x()), T(last_point_l.y()), T(last_point_l.z())}; 75 | //Eigen::Matrix lpm{T(last_point_m.x()), T(last_point_m.y()), T(last_point_m.z())}; 76 | Eigen::Matrix ljm{T(ljm_norm.x()), T(ljm_norm.y()), T(ljm_norm.z())}; 77 | 78 | //Eigen::Quaternion q_last_curr{q[3], T(s) * q[0], T(s) * q[1], T(s) * q[2]}; 79 | Eigen::Quaternion q_last_curr{q[3], q[0], q[1], q[2]}; 80 | Eigen::Quaternion q_identity{T(1), T(0), T(0), T(0)}; 81 | q_last_curr = q_identity.slerp(T(s), q_last_curr); 82 | Eigen::Matrix t_last_curr{T(s) * t[0], T(s) * t[1], T(s) * t[2]}; 83 | 84 | Eigen::Matrix lp; 85 | lp = q_last_curr * cp + t_last_curr; 86 | 87 | residual[0] = (lp - lpj).dot(ljm); 88 | 89 | return true; 90 | } 91 | 92 | static ceres::CostFunction *Create(const Eigen::Vector3d curr_point_, const Eigen::Vector3d last_point_j_, 93 | const Eigen::Vector3d last_point_l_, const Eigen::Vector3d last_point_m_, 94 | const double s_) 95 | { 96 | return (new ceres::AutoDiffCostFunction< 97 | LidarPlaneFactor, 1, 4, 3>( 98 | new LidarPlaneFactor(curr_point_, last_point_j_, last_point_l_, last_point_m_, s_))); 99 | } 100 | 101 | Eigen::Vector3d curr_point, last_point_j, last_point_l, last_point_m; 102 | Eigen::Vector3d ljm_norm; 103 | double s; 104 | }; 105 | 106 | struct LidarPlaneNormFactor 107 | { 108 | 109 | LidarPlaneNormFactor(Eigen::Vector3d curr_point_, Eigen::Vector3d plane_unit_norm_, 110 | double negative_OA_dot_norm_) : curr_point(curr_point_), plane_unit_norm(plane_unit_norm_), 111 | negative_OA_dot_norm(negative_OA_dot_norm_) {} 112 | 113 | template 114 | bool operator()(const T *q, const T *t, T *residual) const 115 | { 116 | Eigen::Quaternion q_w_curr{q[3], q[0], q[1], q[2]}; 117 | Eigen::Matrix t_w_curr{t[0], t[1], t[2]}; 118 | Eigen::Matrix cp{T(curr_point.x()), T(curr_point.y()), T(curr_point.z())}; 119 | Eigen::Matrix point_w; 120 | point_w = q_w_curr * cp + t_w_curr; 121 | 122 | Eigen::Matrix norm(T(plane_unit_norm.x()), T(plane_unit_norm.y()), T(plane_unit_norm.z())); 123 | residual[0] = norm.dot(point_w) + T(negative_OA_dot_norm); 124 | return true; 125 | } 126 | 127 | static ceres::CostFunction *Create(const Eigen::Vector3d curr_point_, const Eigen::Vector3d plane_unit_norm_, 128 | const double negative_OA_dot_norm_) 129 | { 130 | return (new ceres::AutoDiffCostFunction< 131 | LidarPlaneNormFactor, 1, 4, 3>( 132 | new LidarPlaneNormFactor(curr_point_, plane_unit_norm_, negative_OA_dot_norm_))); 133 | } 134 | 135 | Eigen::Vector3d curr_point; 136 | Eigen::Vector3d plane_unit_norm; 137 | double negative_OA_dot_norm; 138 | }; 139 | 140 | 141 | struct LidarDistanceFactor 142 | { 143 | 144 | LidarDistanceFactor(Eigen::Vector3d curr_point_, Eigen::Vector3d closed_point_) 145 | : curr_point(curr_point_), closed_point(closed_point_){} 146 | 147 | template 148 | bool operator()(const T *q, const T *t, T *residual) const 149 | { 150 | Eigen::Quaternion q_w_curr{q[3], q[0], q[1], q[2]}; 151 | Eigen::Matrix t_w_curr{t[0], t[1], t[2]}; 152 | Eigen::Matrix cp{T(curr_point.x()), T(curr_point.y()), T(curr_point.z())}; 153 | Eigen::Matrix point_w; 154 | point_w = q_w_curr * cp + t_w_curr; 155 | 156 | 157 | residual[0] = point_w.x() - T(closed_point.x()); 158 | residual[1] = point_w.y() - T(closed_point.y()); 159 | residual[2] = point_w.z() - T(closed_point.z()); 160 | return true; 161 | } 162 | 163 | static ceres::CostFunction *Create(const Eigen::Vector3d curr_point_, const Eigen::Vector3d closed_point_) 164 | { 165 | return (new ceres::AutoDiffCostFunction< 166 | LidarDistanceFactor, 3, 4, 3>( 167 | new LidarDistanceFactor(curr_point_, closed_point_))); 168 | } 169 | 170 | Eigen::Vector3d curr_point; 171 | Eigen::Vector3d closed_point; 172 | }; -------------------------------------------------------------------------------- /src/scanRegistration.cpp: -------------------------------------------------------------------------------- 1 | // This is an advanced implementation of the algorithm described in the following paper: 2 | // J. Zhang and S. Singh. LOAM: Lidar Odometry and Mapping in Real-time. 3 | // Robotics: Science and Systems Conference (RSS). Berkeley, CA, July 2014. 4 | 5 | // Modifier: Tong Qin qintonguav@gmail.com 6 | // Shaozu Cao saozu.cao@connect.ust.hk 7 | 8 | 9 | // Copyright 2013, Ji Zhang, Carnegie Mellon University 10 | // Further contributions copyright (c) 2016, Southwest Research Institute 11 | // All rights reserved. 12 | // 13 | // Redistribution and use in source and binary forms, with or without 14 | // modification, are permitted provided that the following conditions are met: 15 | // 16 | // 1. Redistributions of source code must retain the above copyright notice, 17 | // this list of conditions and the following disclaimer. 18 | // 2. Redistributions in binary form must reproduce the above copyright notice, 19 | // this list of conditions and the following disclaimer in the documentation 20 | // and/or other materials provided with the distribution. 21 | // 3. Neither the name of the copyright holder nor the names of its 22 | // contributors may be used to endorse or promote products derived from this 23 | // software without specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 26 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 27 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 28 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 29 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 30 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 31 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 32 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 33 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 34 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 35 | // POSSIBILITY OF SUCH DAMAGE. 36 | 37 | 38 | #include 39 | #include 40 | #include 41 | #include "aloam_velodyne/common.h" 42 | #include "aloam_velodyne/tic_toc.h" 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | #include 53 | #include 54 | #include 55 | 56 | using std::atan2; 57 | using std::cos; 58 | using std::sin; 59 | 60 | std::string LIDAR_TYPE; 61 | 62 | const double scanPeriod = 0.1; 63 | 64 | const int systemDelay = 0; 65 | int systemInitCount = 0; 66 | bool systemInited = false; 67 | int N_SCANS = 0; 68 | float cloudCurvature[400000]; 69 | int cloudSortInd[400000]; 70 | int cloudNeighborPicked[400000]; 71 | int cloudLabel[400000]; 72 | 73 | bool comp (int i,int j) { return (cloudCurvature[i] pubEachScan; 82 | 83 | bool PUB_EACH_LINE = false; 84 | 85 | double MINIMUM_RANGE = 0.1; 86 | 87 | template 88 | void removeClosedPointCloud(const pcl::PointCloud &cloud_in, 89 | pcl::PointCloud &cloud_out, float thres) 90 | { 91 | if (&cloud_in != &cloud_out) 92 | { 93 | cloud_out.header = cloud_in.header; 94 | cloud_out.points.resize(cloud_in.points.size()); 95 | } 96 | 97 | size_t j = 0; 98 | 99 | for (size_t i = 0; i < cloud_in.points.size(); ++i) 100 | { 101 | if (cloud_in.points[i].x * cloud_in.points[i].x + cloud_in.points[i].y * cloud_in.points[i].y + cloud_in.points[i].z * cloud_in.points[i].z < thres * thres) 102 | continue; 103 | cloud_out.points[j] = cloud_in.points[i]; 104 | j++; 105 | } 106 | if (j != cloud_in.points.size()) 107 | { 108 | cloud_out.points.resize(j); 109 | } 110 | 111 | cloud_out.height = 1; 112 | cloud_out.width = static_cast(j); 113 | cloud_out.is_dense = true; 114 | } 115 | 116 | void laserCloudHandler(const sensor_msgs::PointCloud2ConstPtr &laserCloudMsg) 117 | { 118 | if (!systemInited) 119 | { 120 | systemInitCount++; 121 | if (systemInitCount >= systemDelay) 122 | { 123 | systemInited = true; 124 | } 125 | else 126 | return; 127 | } 128 | 129 | TicToc t_whole; 130 | TicToc t_prepare; 131 | std::vector scanStartInd(N_SCANS, 0); 132 | std::vector scanEndInd(N_SCANS, 0); 133 | 134 | pcl::PointCloud laserCloudIn; 135 | pcl::fromROSMsg(*laserCloudMsg, laserCloudIn); 136 | std::vector indices; 137 | 138 | pcl::removeNaNFromPointCloud(laserCloudIn, laserCloudIn, indices); 139 | removeClosedPointCloud(laserCloudIn, laserCloudIn, MINIMUM_RANGE); 140 | 141 | 142 | int cloudSize = laserCloudIn.points.size(); 143 | float startOri = -atan2(laserCloudIn.points[0].y, laserCloudIn.points[0].x); 144 | float endOri = -atan2(laserCloudIn.points[cloudSize - 1].y, 145 | laserCloudIn.points[cloudSize - 1].x) + 146 | 2 * M_PI; 147 | 148 | if (endOri - startOri > 3 * M_PI) 149 | { 150 | endOri -= 2 * M_PI; 151 | } 152 | else if (endOri - startOri < M_PI) 153 | { 154 | endOri += 2 * M_PI; 155 | } 156 | ////printf("end Ori %f\n", endOri); 157 | 158 | bool halfPassed = false; 159 | int count = cloudSize; 160 | PointType point; 161 | std::vector> laserCloudScans(N_SCANS); 162 | for (int i = 0; i < cloudSize; i++) 163 | { 164 | point.x = laserCloudIn.points[i].x; 165 | point.y = laserCloudIn.points[i].y; 166 | point.z = laserCloudIn.points[i].z; 167 | 168 | float angle = atan(point.z / sqrt(point.x * point.x + point.y * point.y)) * 180 / M_PI; 169 | int scanID = 0; 170 | 171 | if (LIDAR_TYPE == "VLP16" && N_SCANS == 16) 172 | { 173 | scanID = int((angle + 15) / 2 + 0.5); 174 | if (scanID > (N_SCANS - 1) || scanID < 0) 175 | { 176 | count--; 177 | continue; 178 | } 179 | } 180 | else if (LIDAR_TYPE == "HDL32" && N_SCANS == 32) 181 | { 182 | scanID = int((angle + 92.0/3.0) * 3.0 / 4.0); 183 | if (scanID > (N_SCANS - 1) || scanID < 0) 184 | { 185 | count--; 186 | continue; 187 | } 188 | } 189 | // HDL64 (e.g., KITTI) 190 | else if (LIDAR_TYPE == "HDL64" && N_SCANS == 64) 191 | { 192 | if (angle >= -8.83) 193 | scanID = int((2 - angle) * 3.0 + 0.5); 194 | else 195 | scanID = N_SCANS / 2 + int((-8.83 - angle) * 2.0 + 0.5); 196 | 197 | // use [0 50] > 50 remove outlies 198 | if (angle > 2 || angle < -24.33 || scanID > 50 || scanID < 0) 199 | { 200 | count--; 201 | continue; 202 | } 203 | } 204 | // Ouster OS1-64 (e.g., MulRan) 205 | else if (LIDAR_TYPE == "OS1-64" && N_SCANS == 64) 206 | { 207 | scanID = int((angle + 22.5) / 2 + 0.5); // ouster os1-64 vfov is [-22.5, 22.5] see https://ouster.com/products/os1-lidar-sensor/ 208 | if (scanID > (N_SCANS - 1) || scanID < 0) 209 | { 210 | count--; 211 | continue; 212 | } 213 | } 214 | else 215 | { 216 | //printf("wrong scan number\n"); 217 | ROS_BREAK(); 218 | } 219 | ////printf("angle %f scanID %d \n", angle, scanID); 220 | 221 | float ori = -atan2(point.y, point.x); 222 | if (!halfPassed) 223 | { 224 | if (ori < startOri - M_PI / 2) 225 | { 226 | ori += 2 * M_PI; 227 | } 228 | else if (ori > startOri + M_PI * 3 / 2) 229 | { 230 | ori -= 2 * M_PI; 231 | } 232 | 233 | if (ori - startOri > M_PI) 234 | { 235 | halfPassed = true; 236 | } 237 | } 238 | else 239 | { 240 | ori += 2 * M_PI; 241 | if (ori < endOri - M_PI * 3 / 2) 242 | { 243 | ori += 2 * M_PI; 244 | } 245 | else if (ori > endOri + M_PI / 2) 246 | { 247 | ori -= 2 * M_PI; 248 | } 249 | } 250 | 251 | float relTime = (ori - startOri) / (endOri - startOri); 252 | point.intensity = scanID + scanPeriod * relTime; 253 | laserCloudScans[scanID].push_back(point); 254 | } 255 | 256 | cloudSize = count; 257 | //printf("points size %d \n", cloudSize); 258 | 259 | pcl::PointCloud::Ptr laserCloud(new pcl::PointCloud()); 260 | for (int i = 0; i < N_SCANS; i++) 261 | { 262 | scanStartInd[i] = laserCloud->size() + 5; 263 | *laserCloud += laserCloudScans[i]; 264 | scanEndInd[i] = laserCloud->size() - 6; 265 | } 266 | 267 | //printf("prepare time %f \n", t_prepare.toc()); 268 | 269 | for (int i = 5; i < cloudSize - 5; i++) 270 | { 271 | float diffX = laserCloud->points[i - 5].x + laserCloud->points[i - 4].x + laserCloud->points[i - 3].x + laserCloud->points[i - 2].x + laserCloud->points[i - 1].x - 10 * laserCloud->points[i].x + laserCloud->points[i + 1].x + laserCloud->points[i + 2].x + laserCloud->points[i + 3].x + laserCloud->points[i + 4].x + laserCloud->points[i + 5].x; 272 | float diffY = laserCloud->points[i - 5].y + laserCloud->points[i - 4].y + laserCloud->points[i - 3].y + laserCloud->points[i - 2].y + laserCloud->points[i - 1].y - 10 * laserCloud->points[i].y + laserCloud->points[i + 1].y + laserCloud->points[i + 2].y + laserCloud->points[i + 3].y + laserCloud->points[i + 4].y + laserCloud->points[i + 5].y; 273 | float diffZ = laserCloud->points[i - 5].z + laserCloud->points[i - 4].z + laserCloud->points[i - 3].z + laserCloud->points[i - 2].z + laserCloud->points[i - 1].z - 10 * laserCloud->points[i].z + laserCloud->points[i + 1].z + laserCloud->points[i + 2].z + laserCloud->points[i + 3].z + laserCloud->points[i + 4].z + laserCloud->points[i + 5].z; 274 | 275 | cloudCurvature[i] = diffX * diffX + diffY * diffY + diffZ * diffZ; 276 | cloudSortInd[i] = i; 277 | cloudNeighborPicked[i] = 0; 278 | cloudLabel[i] = 0; 279 | } 280 | 281 | 282 | TicToc t_pts; 283 | 284 | pcl::PointCloud cornerPointsSharp; 285 | pcl::PointCloud cornerPointsLessSharp; 286 | pcl::PointCloud surfPointsFlat; 287 | pcl::PointCloud surfPointsLessFlat; 288 | 289 | float t_q_sort = 0; 290 | for (int i = 0; i < N_SCANS; i++) 291 | { 292 | if( scanEndInd[i] - scanStartInd[i] < 6) 293 | continue; 294 | pcl::PointCloud::Ptr surfPointsLessFlatScan(new pcl::PointCloud); 295 | for (int j = 0; j < 6; j++) 296 | { 297 | int sp = scanStartInd[i] + (scanEndInd[i] - scanStartInd[i]) * j / 6; 298 | int ep = scanStartInd[i] + (scanEndInd[i] - scanStartInd[i]) * (j + 1) / 6 - 1; 299 | 300 | TicToc t_tmp; 301 | std::sort (cloudSortInd + sp, cloudSortInd + ep + 1, comp); 302 | t_q_sort += t_tmp.toc(); 303 | 304 | int largestPickedNum = 0; 305 | for (int k = ep; k >= sp; k--) 306 | { 307 | int ind = cloudSortInd[k]; 308 | 309 | if (cloudNeighborPicked[ind] == 0 && 310 | cloudCurvature[ind] > 0.1) 311 | { 312 | 313 | largestPickedNum++; 314 | if (largestPickedNum <= 2) 315 | { 316 | cloudLabel[ind] = 2; 317 | cornerPointsSharp.push_back(laserCloud->points[ind]); 318 | cornerPointsLessSharp.push_back(laserCloud->points[ind]); 319 | } 320 | else if (largestPickedNum <= 20) 321 | { 322 | cloudLabel[ind] = 1; 323 | cornerPointsLessSharp.push_back(laserCloud->points[ind]); 324 | } 325 | else 326 | { 327 | break; 328 | } 329 | 330 | cloudNeighborPicked[ind] = 1; 331 | 332 | for (int l = 1; l <= 5; l++) 333 | { 334 | float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l - 1].x; 335 | float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l - 1].y; 336 | float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l - 1].z; 337 | if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05) 338 | { 339 | break; 340 | } 341 | 342 | cloudNeighborPicked[ind + l] = 1; 343 | } 344 | for (int l = -1; l >= -5; l--) 345 | { 346 | float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l + 1].x; 347 | float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l + 1].y; 348 | float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l + 1].z; 349 | if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05) 350 | { 351 | break; 352 | } 353 | 354 | cloudNeighborPicked[ind + l] = 1; 355 | } 356 | } 357 | } 358 | 359 | int smallestPickedNum = 0; 360 | for (int k = sp; k <= ep; k++) 361 | { 362 | int ind = cloudSortInd[k]; 363 | 364 | if (cloudNeighborPicked[ind] == 0 && 365 | cloudCurvature[ind] < 0.1) 366 | { 367 | 368 | cloudLabel[ind] = -1; 369 | surfPointsFlat.push_back(laserCloud->points[ind]); 370 | 371 | smallestPickedNum++; 372 | if (smallestPickedNum >= 4) 373 | { 374 | break; 375 | } 376 | 377 | cloudNeighborPicked[ind] = 1; 378 | for (int l = 1; l <= 5; l++) 379 | { 380 | float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l - 1].x; 381 | float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l - 1].y; 382 | float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l - 1].z; 383 | if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05) 384 | { 385 | break; 386 | } 387 | 388 | cloudNeighborPicked[ind + l] = 1; 389 | } 390 | for (int l = -1; l >= -5; l--) 391 | { 392 | float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l + 1].x; 393 | float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l + 1].y; 394 | float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l + 1].z; 395 | if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05) 396 | { 397 | break; 398 | } 399 | 400 | cloudNeighborPicked[ind + l] = 1; 401 | } 402 | } 403 | } 404 | 405 | for (int k = sp; k <= ep; k++) 406 | { 407 | if (cloudLabel[k] <= 0) 408 | { 409 | surfPointsLessFlatScan->push_back(laserCloud->points[k]); 410 | } 411 | } 412 | } 413 | 414 | pcl::PointCloud surfPointsLessFlatScanDS; 415 | pcl::VoxelGrid downSizeFilter; 416 | downSizeFilter.setInputCloud(surfPointsLessFlatScan); 417 | downSizeFilter.setLeafSize(0.2, 0.2, 0.2); 418 | downSizeFilter.filter(surfPointsLessFlatScanDS); 419 | 420 | surfPointsLessFlat += surfPointsLessFlatScanDS; 421 | } 422 | //printf("sort q time %f \n", t_q_sort); 423 | //printf("seperate points time %f \n", t_pts.toc()); 424 | 425 | 426 | sensor_msgs::PointCloud2 laserCloudOutMsg; 427 | pcl::toROSMsg(*laserCloud, laserCloudOutMsg); 428 | laserCloudOutMsg.header.stamp = laserCloudMsg->header.stamp; 429 | laserCloudOutMsg.header.frame_id = "/camera_init"; 430 | pubLaserCloud.publish(laserCloudOutMsg); 431 | 432 | sensor_msgs::PointCloud2 cornerPointsSharpMsg; 433 | pcl::toROSMsg(cornerPointsSharp, cornerPointsSharpMsg); 434 | cornerPointsSharpMsg.header.stamp = laserCloudMsg->header.stamp; 435 | cornerPointsSharpMsg.header.frame_id = "/camera_init"; 436 | pubCornerPointsSharp.publish(cornerPointsSharpMsg); 437 | 438 | sensor_msgs::PointCloud2 cornerPointsLessSharpMsg; 439 | pcl::toROSMsg(cornerPointsLessSharp, cornerPointsLessSharpMsg); 440 | cornerPointsLessSharpMsg.header.stamp = laserCloudMsg->header.stamp; 441 | cornerPointsLessSharpMsg.header.frame_id = "/camera_init"; 442 | pubCornerPointsLessSharp.publish(cornerPointsLessSharpMsg); 443 | 444 | sensor_msgs::PointCloud2 surfPointsFlat2; 445 | pcl::toROSMsg(surfPointsFlat, surfPointsFlat2); 446 | surfPointsFlat2.header.stamp = laserCloudMsg->header.stamp; 447 | surfPointsFlat2.header.frame_id = "/camera_init"; 448 | pubSurfPointsFlat.publish(surfPointsFlat2); 449 | 450 | sensor_msgs::PointCloud2 surfPointsLessFlat2; 451 | pcl::toROSMsg(surfPointsLessFlat, surfPointsLessFlat2); 452 | surfPointsLessFlat2.header.stamp = laserCloudMsg->header.stamp; 453 | surfPointsLessFlat2.header.frame_id = "/camera_init"; 454 | pubSurfPointsLessFlat.publish(surfPointsLessFlat2); 455 | // std::cout << "published ... " << std::endl; 456 | 457 | // pub each scam 458 | if(PUB_EACH_LINE) 459 | { 460 | for(int i = 0; i< N_SCANS; i++) 461 | { 462 | sensor_msgs::PointCloud2 scanMsg; 463 | pcl::toROSMsg(laserCloudScans[i], scanMsg); 464 | scanMsg.header.stamp = laserCloudMsg->header.stamp; 465 | scanMsg.header.frame_id = "/camera_init"; 466 | pubEachScan[i].publish(scanMsg); 467 | } 468 | } 469 | 470 | //printf("scan registration time %f ms *************\n", t_whole.toc()); 471 | if(t_whole.toc() > 100) 472 | ROS_WARN("scan registration process over 100ms"); 473 | } 474 | 475 | int main(int argc, char **argv) 476 | { 477 | ros::init(argc, argv, "scanRegistration"); 478 | ros::NodeHandle nh; 479 | 480 | nh.param("scan_line", N_SCANS, 16); 481 | nh.param("lidar_type", LIDAR_TYPE, "KITTI"); 482 | nh.param("minimum_range", MINIMUM_RANGE, 0.1); 483 | 484 | //printf("scan line number %d \n", N_SCANS); 485 | 486 | if(N_SCANS != 16 && N_SCANS != 32 && N_SCANS != 64) 487 | { 488 | //printf("only support velodyne with 16, 32 or 64 scan line!"); 489 | return 0; 490 | } 491 | 492 | ros::Subscriber subLaserCloud = nh.subscribe("/velodyne_points", 100, laserCloudHandler); 493 | 494 | pubLaserCloud = nh.advertise("/velodyne_cloud_2", 100); 495 | 496 | pubCornerPointsSharp = nh.advertise("/laser_cloud_sharp", 100); 497 | 498 | pubCornerPointsLessSharp = nh.advertise("/laser_cloud_less_sharp", 100); 499 | 500 | pubSurfPointsFlat = nh.advertise("/laser_cloud_flat", 100); 501 | 502 | pubSurfPointsLessFlat = nh.advertise("/laser_cloud_less_flat", 100); 503 | 504 | pubRemovePoints = nh.advertise("/laser_remove_points", 100); 505 | 506 | if(PUB_EACH_LINE) 507 | { 508 | for(int i = 0; i < N_SCANS; i++) 509 | { 510 | ros::Publisher tmp = nh.advertise("/laser_scanid_" + std::to_string(i), 100); 511 | pubEachScan.push_back(tmp); 512 | } 513 | } 514 | ros::spin(); 515 | 516 | return 0; 517 | } 518 | -------------------------------------------------------------------------------- /utils/python/__pycache__/pypcdMyUtils.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/python/__pycache__/pypcdMyUtils.cpython-36.pyc -------------------------------------------------------------------------------- /utils/python/bone_table.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/python/bone_table.npy -------------------------------------------------------------------------------- /utils/python/jet_table.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/python/jet_table.npy -------------------------------------------------------------------------------- /utils/python/makeMergedMap.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import copy 5 | from io import StringIO 6 | 7 | import pypcd # for the install, use this command: python3.x (use your python ver) -m pip install --user git+https://github.com/DanielPollithy/pypcd.git 8 | from pypcd import pypcd 9 | 10 | import numpy as np 11 | from numpy import linalg as LA 12 | 13 | import open3d as o3d 14 | 15 | from pypcdMyUtils import * 16 | 17 | jet_table = np.load('jet_table.npy') 18 | bone_table = np.load('bone_table.npy') 19 | 20 | color_table = bone_table 21 | color_table_len = color_table.shape[0] 22 | 23 | 24 | ########################## 25 | # User only consider this block 26 | ########################## 27 | 28 | data_dir = "../sample_data/Seosan01/" # should end with / 29 | scan_idx_range_to_stack = [0, 20] # if you want a whole map, use [0, len(scan_files)] 30 | node_skip = 1 31 | 32 | num_points_in_a_scan = 150000 # for reservation (save faster) // e.g., use 150000 for 128 ray lidars, 100000 for 64 ray lidars, 30000 for 16 ray lidars, if error occured, use the larger value. 33 | 34 | is_live_vis = False # recommend to use false 35 | is_o3d_vis = True 36 | intensity_color_max = 200 37 | 38 | is_near_removal = True 39 | thres_near_removal = 2 # meter (to remove platform-myself structure ghost points) 40 | 41 | ########################## 42 | 43 | 44 | # 45 | scan_dir = data_dir + "Scans" 46 | scan_files = os.listdir(scan_dir) 47 | scan_files.sort() 48 | 49 | poses = [] 50 | f = open(data_dir+"optimized_poses.txt", 'r') 51 | while True: 52 | line = f.readline() 53 | if not line: break 54 | pose_SE3 = np.asarray([float(i) for i in line.split()]) 55 | pose_SE3 = np.vstack( (np.reshape(pose_SE3, (3, 4)), np.asarray([0,0,0,1])) ) 56 | poses.append(pose_SE3) 57 | f.close() 58 | 59 | 60 | # 61 | assert (scan_idx_range_to_stack[1] > scan_idx_range_to_stack[0]) 62 | print("Merging scans from", scan_idx_range_to_stack[0], "to", scan_idx_range_to_stack[1]) 63 | 64 | 65 | # 66 | if(is_live_vis): 67 | vis = o3d.visualization.Visualizer() 68 | vis.create_window('Map', visible = True) 69 | 70 | nodes_count = 0 71 | pcd_combined_for_vis = o3d.geometry.PointCloud() 72 | pcd_combined_for_save = None 73 | 74 | # The scans from 000000.pcd should be prepared if it is not used (because below code indexing is designed in a naive way) 75 | 76 | # manually reserve memory for fast write 77 | num_all_points_expected = int(num_points_in_a_scan * np.round((scan_idx_range_to_stack[1] - scan_idx_range_to_stack[0])/node_skip)) 78 | 79 | np_xyz_all = np.empty([num_all_points_expected, 3]) 80 | np_intensity_all = np.empty([num_all_points_expected, 1]) 81 | curr_count = 0 82 | 83 | for node_idx in range(len(scan_files)): 84 | if(node_idx < scan_idx_range_to_stack[0] or node_idx >= scan_idx_range_to_stack[1]): 85 | continue 86 | 87 | nodes_count = nodes_count + 1 88 | if( nodes_count % node_skip is not 0): 89 | if(node_idx is not scan_idx_range_to_stack[0]): # to ensure the vis init 90 | continue 91 | 92 | print("read keyframe scan idx", node_idx) 93 | 94 | scan_pose = poses[node_idx] 95 | 96 | scan_path = os.path.join(scan_dir, scan_files[node_idx]) 97 | scan_pcd = o3d.io.read_point_cloud(scan_path) 98 | scan_xyz_local = copy.deepcopy(np.asarray(scan_pcd.points)) 99 | 100 | scan_pypcd_with_intensity = pypcd.PointCloud.from_path(scan_path) 101 | scan_intensity = scan_pypcd_with_intensity.pc_data['intensity'] 102 | scan_intensity_colors_idx = np.round( (color_table_len-1) * np.minimum( 1, np.maximum(0, scan_intensity / intensity_color_max) ) ) 103 | scan_intensity_colors = color_table[scan_intensity_colors_idx.astype(int)] 104 | 105 | scan_pcd_global = scan_pcd.transform(scan_pose) # global coord, note that this is not deepcopy 106 | scan_pcd_global.colors = o3d.utility.Vector3dVector(scan_intensity_colors) 107 | scan_xyz = np.asarray(scan_pcd_global.points) 108 | 109 | scan_intensity = np.expand_dims(scan_intensity, axis=1) 110 | scan_ranges = LA.norm(scan_xyz_local, axis=1) 111 | 112 | if(is_near_removal): 113 | eff_idxes = np.where (scan_ranges > thres_near_removal) 114 | scan_xyz = scan_xyz[eff_idxes[0], :] 115 | scan_intensity = scan_intensity[eff_idxes[0], :] 116 | 117 | scan_pcd_global = scan_pcd_global.select_by_index(eff_idxes[0]) 118 | 119 | if(is_o3d_vis): 120 | pcd_combined_for_vis += scan_pcd_global # open3d pointcloud class append is fast 121 | 122 | if is_live_vis: 123 | if(node_idx is scan_idx_range_to_stack[0]): # to ensure the vis init 124 | vis.add_geometry(pcd_combined_for_vis) 125 | 126 | vis.update_geometry(pcd_combined_for_vis) 127 | vis.poll_events() 128 | vis.update_renderer() 129 | 130 | # save 131 | np_xyz_all[curr_count:curr_count + scan_xyz.shape[0], :] = scan_xyz 132 | np_intensity_all[curr_count:curr_count + scan_xyz.shape[0], :] = scan_intensity 133 | 134 | curr_count = curr_count + scan_xyz.shape[0] 135 | print(curr_count) 136 | 137 | # 138 | if(is_o3d_vis): 139 | print("draw the merged map.") 140 | o3d.visualization.draw_geometries([pcd_combined_for_vis]) 141 | 142 | 143 | # save ply having intensity 144 | np_xyz_all = np_xyz_all[0:curr_count, :] 145 | np_intensity_all = np_intensity_all[0:curr_count, :] 146 | 147 | np_xyzi_all = np.hstack( (np_xyz_all, np_intensity_all) ) 148 | xyzi = make_xyzi_point_cloud(np_xyzi_all) 149 | 150 | map_name = data_dir + "map_" + str(scan_idx_range_to_stack[0]) + "_to_" + str(scan_idx_range_to_stack[1]) + "_with_intensity.pcd" 151 | xyzi.save_pcd(map_name, compression='binary_compressed') 152 | print("intensity map is save (path:", map_name, ")") 153 | 154 | # save rgb colored points 155 | # map_name = data_dir + "map_" + str(scan_idx_range_to_stack[0]) + "_to_" + str(scan_idx_range_to_stack[1]) + ".pcd" 156 | # o3d.io.write_point_cloud(map_name, pcd_combined_for_vis) 157 | # print("the map is save (path:", map_name, ")") 158 | 159 | 160 | -------------------------------------------------------------------------------- /utils/python/pypcdMyUtils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pypcd # for the install, use this command: python3.x (use your python ver) -m pip install --user git+https://github.com/DanielPollithy/pypcd.git 3 | from pypcd import pypcd 4 | 5 | def make_xyzi_point_cloud(xyzl, label_type='f'): 6 | """ Make XYZL point cloud from numpy array. 7 | TODO i labels? 8 | """ 9 | md = {'version': .7, 10 | 'fields': ['x', 'y', 'z', 'intensity'], 11 | 'count': [1, 1, 1, 1], 12 | 'width': len(xyzl), 13 | 'height': 1, 14 | 'viewpoint': [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], 15 | 'points': len(xyzl), 16 | 'data': 'ASCII'} 17 | if label_type.lower() == 'f': 18 | md['size'] = [4, 4, 4, 4] 19 | md['type'] = ['F', 'F', 'F', 'F'] 20 | elif label_type.lower() == 'u': 21 | md['size'] = [4, 4, 4, 1] 22 | md['type'] = ['F', 'F', 'F', 'U'] 23 | else: 24 | raise ValueError('label type must be F or U') 25 | # TODO use .view() 26 | xyzl = xyzl.astype(np.float32) 27 | dt = np.dtype([('x', np.float32), ('y', np.float32), ('z', np.float32), 28 | ('intensity', np.float32)]) 29 | pc_data = np.rec.fromarrays([xyzl[:, 0], xyzl[:, 1], xyzl[:, 2], 30 | xyzl[:, 3]], dtype=dt) 31 | pc = pypcd.PointCloud(md, pc_data) 32 | return pc 33 | -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000000.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000000.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000001.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000001.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000002.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000002.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000003.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000003.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000004.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000004.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000005.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000005.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000006.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000006.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000007.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000007.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000008.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000008.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000009.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000009.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000010.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000010.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000011.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000011.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000012.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000012.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000013.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000013.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000014.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000014.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000015.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000015.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000016.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000016.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000017.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000017.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000018.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000018.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000019.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000019.pcd -------------------------------------------------------------------------------- /utils/sample_data/KAIST03/Scans/000020.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/KAIST03/Scans/000020.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000000.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000000.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000001.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000001.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000002.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000002.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000003.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000003.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000004.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000004.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000005.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000005.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000006.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000006.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000007.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000007.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000008.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000008.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000009.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000009.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000010.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000010.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000011.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000011.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000012.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000012.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000013.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000013.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000014.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000014.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000015.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000015.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000016.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000016.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000017.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000017.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000018.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000018.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000019.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000019.pcd -------------------------------------------------------------------------------- /utils/sample_data/Seosan01/Scans/000020.pcd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gisbi-kim/SC-A-LOAM/321cb4b07b7815c114237ce5d87014d2e46b972c/utils/sample_data/Seosan01/Scans/000020.pcd --------------------------------------------------------------------------------