diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 00000000..2c3671b5
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,114 @@
+cmake_minimum_required(VERSION 2.8.3)
+project(hdl_graph_slam)
+
+# -mavx causes a lot of errors!!
+add_definitions(-std=c++11 -msse -msse2 -msse3 -msse4 -msse4.1 -msse4.2)
+set(CMAKE_CXX_FLAGS "-std=c++11 -msse -msse2 -msse3 -msse4 -msse4.1 -msse4.2")
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_LIST_DIR}/cmake")
+
+# pcl 1.7 causes a segfault when it is built with debug mode
+set(CMAKE_BUILD_TYPE "RELEASE")
+
+find_package(catkin REQUIRED COMPONENTS
+ roscpp
+ pcl_ros
+ geodesy
+ nmea_msgs
+ sensor_msgs
+ message_generation
+ ndt_omp
+)
+
+find_package(PCL 1.7 REQUIRED)
+include_directories(${PCL_INCLUDE_DIRS})
+link_directories(${PCL_LIBRARY_DIRS})
+add_definitions(${PCL_DEFINITIONS})
+
+message(STATUS "PCL_INCLUDE_DIRS:" ${PCL_INCLUDE_DIRS})
+message(STATUS "PCL_LIBRARY_DIRS:" ${PCL_LIBRARY_DIRS})
+message(STATUS "PCL_DEFINITIONS:" ${PCL_DEFINITIONS})
+
+find_package(G2O REQUIRED)
+include_directories(SYSTEM ${G2O_INCLUDE_DIR} ${G2O_INCLUDE_DIRS})
+link_directories(${G2O_LIBRARY_DIRS})
+# link_libraries(${G2O_LIBRARIES})
+
+find_package(OpenMP)
+if (OPENMP_FOUND)
+ set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}")
+ set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
+endif()
+
+########################
+## message generation ##
+########################
+add_message_files(FILES
+ FloorCoeffs.msg
+)
+
+generate_messages(DEPENDENCIES std_msgs)
+
+###################################
+## catkin specific configuration ##
+###################################
+catkin_package(
+ INCLUDE_DIRS include
+# LIBRARIES hdl_scan_matching_odometry
+# CATKIN_DEPENDS pcl_ros roscpp sensor_msgs
+# DEPENDS system_lib
+)
+
+###########
+## Build ##
+###########
+include_directories(include)
+include_directories(
+ ${catkin_INCLUDE_DIRS}
+)
+
+# nodelets
+add_library(prefiltering_nodelet apps/prefiltering_nodelet.cpp)
+target_link_libraries(prefiltering_nodelet
+ ${catkin_LIBRARIES}
+ ${PCL_LIBRARIES}
+)
+
+
+add_library(floor_detection_nodelet apps/floor_detection_nodelet.cpp)
+target_link_libraries(floor_detection_nodelet
+ ${catkin_LIBRARIES}
+ ${PCL_LIBRARIES}
+)
+add_dependencies(floor_detection_nodelet ${PROJECT_NAME}_gencpp)
+
+
+add_library(scan_matching_odometry_nodelet
+ apps/scan_matching_odometry_nodelet.cpp
+ src/hdl_graph_slam/registrations.cpp
+)
+target_link_libraries(scan_matching_odometry_nodelet
+ ${catkin_LIBRARIES}
+ ${PCL_LIBRARIES}
+)
+
+
+add_library(hdl_graph_slam_nodelet
+ apps/hdl_graph_slam_nodelet.cpp
+ src/hdl_graph_slam/graph_slam.cpp
+ src/hdl_graph_slam/keyframe.cpp
+ src/hdl_graph_slam/map_cloud_generator.cpp
+ src/hdl_graph_slam/registrations.cpp
+ src/hdl_graph_slam/information_matrix_calculator.cpp
+)
+target_link_libraries(hdl_graph_slam_nodelet
+ ${catkin_LIBRARIES}
+ ${PCL_LIBRARIES}
+ ${G2O_TYPES_DATA}
+ ${G2O_CORE_LIBRARY}
+ ${G2O_STUFF_LIBRARY}
+ ${G2O_SOLVER_PCG}
+ ${G2O_SOLVER_CSPARSE}
+ ${G2O_TYPES_SLAM3D}
+ ${G2O_TYPES_SLAM3D_ADDONS}
+)
+add_dependencies(hdl_graph_slam_nodelet ${PROJECT_NAME}_gencpp)
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..bd7d43f7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,105 @@
+# hdl_graph_slam
+***hdl_graph_slam*** is an open source ROS package for real-time 3D slam using a 3D LIDAR. It is based on 3D Graph SLAM with NDT scan matching-based odometry estimation and loop detection. It also utilizes floor plane detection to generate an environmental map with a completely flat floor. This package also contains an implementation of multi-threaded NDT (10 times faster than the normal NDT in PCL!). We have tested this packaged mainly in indoor environments, but it can be applied to outdoor environment mapping as well.
+
+
+
+video (TODO: upload this video to youtube!)
+
+## Nodelets
+***hdl_graph_slam*** consists of four nodelets.
+- *prefiltering_nodelet*
+- *scan_matching_odometry_nodelet*
+- *floor_detection_nodelet*
+- *hdl_graph_slam_nodelet*
+
+The input point cloud is first downsampled by *prefiltering_nodelet*, and then passed to the next nodelets. While *scan_matching_odometry_nodelet* estimates the sensor pose by iteratively applying a scan matching between consecutive frames (i.e., odometry estimation), *floor_detection_nodelet* detects floor planes by RANSAC. The estimated odometry and the detected floor planes are sent to *hdl_graph_slam*. To compensate the accumulated error of the scan matching, it performs loop detection and optimizes a pose graph which takes odometry, loops, and floor planes into account.
+
+**[Sep/1/2017]** A GPS-based graph optimization has been implemented. *hdl_graph_slam_nodelet* receives GPS data (i.e., latitude, longitude) from */gps/geopoint* and converts them into the UTM coordinate, and then it adds them into the pose graph. It effectively compensate the scan matching error in outdoor environments.
+
+
+
+
+### Parameters
+All the parameters are listed in *launch/hdl_graph_slam.launch* as ros params.
+
+### Services
+- */hdl_graph_slam/dump* (std_srvs/Empty)
+ - save all data (point clouds, floor coeffs, odoms, and pose graph) to the current directory.
+
+## Multi-threaded Scan Matching Algorithms
+This package contains implementations of multi-threaded and SSE friendly NDT and GICP algorithms. They are derived from the implementations in PCL. With recent multi-core processors, they may show ten times faster processing speed than the NDT and the GICP in PCL. See *include/pclomp* and *src/pclomp* to use the boosted scan matching algorithms.
+
+## Requirements
+***hdl_graph_slam*** requires the following libraries:
+- OpenMP
+- PCL 1.7
+- g2o
+
+Note that ***hdl_graph_slam*** cannot be built with older g2o libraries (such as ros-indigo-libg2o). Install the latest g2o:
+```bash
+git clone https://github.com/RainerKuemmerle/g2o.git
+cd g2o
+mkdir build && cd build
+cmake .. -DCMAKE_BUILD_TYPE=RELEASE
+make -j8
+sudo make install
+```
+
+The following ros packages are required:
+- geodesy
+- pcl_ros
+```bash
+sudo apt-get install ros-indigo-geodesy ros-indigo-pcl_ros
+```
+
+## Example
+
+Example bag files (recorded in a small room):
+- [hdl_501.bag.tar.gz](http://www.aisl.cs.tut.ac.jp/databases/hdl_graph_slam/hdl_501.bag.tar.gz) (raw data, 344MB)
+- [hdl_501_filtered.bag.tar.gz](http://www.aisl.cs.tut.ac.jp/databases/hdl_graph_slam/hdl_501_filtered.bag.tar.gz) (downsampled data, 57MB, **Recommended!**)
+
+```bash
+rosparam set use_sim_time true
+roslaunch hdl_graph_slam hdl_graph_slam_501.launch
+```
+
+```bash
+roscd hdl_graph_slam/rviz
+rviz -d hdl_graph_slam.rviz
+```
+
+```bash
+rosbag play --clock hdl_501_filtered.bag
+```
+
+We also provide bag_player.py which adjusts the playback speed according to the processing speed of your PC. It allows processing data as fast as possible for your PC.
+
+```bash
+rosrun hdl_graph_slam bag_player.py hdl_501_filtered.bag
+```
+
+You'll see a generated point cloud like:
+
+
+
+
+## Example with GPS feature
+Ford Campus Vision and Lidar Data Set [URL].
+
+The following script converts the Ford Lidar Dataset to a rosbag and plays it. In this example, ***hdl_graph_slam*** utilized the GPS data to correct the pose graph.
+
+```bash
+cd IJRR-Dataset-2
+rosrun hdl_graph_slam ford2bag.py dataset-2.bag
+rosrun hdl_graph_slam bag_player.py dataset-2.bag
+```
+
+
+
+## Papers
+Kenji Koide, Jun Miura, and Emanuele Menegatti, A Portable 3D LIDAR-based System for Long-term and Wide-area People Behavior Measurement, Robotics and Autonomous Systems (under review) [PDF].
+
+## Contact
+Kenji Koide, Active Intelligent Systems Laboratory, Toyohashi University of Technology [URL]
+koide@aisl.cs.tut.ac.jp
+
diff --git a/apps/floor_detection_nodelet.cpp b/apps/floor_detection_nodelet.cpp
new file mode 100644
index 00000000..607a9c56
--- /dev/null
+++ b/apps/floor_detection_nodelet.cpp
@@ -0,0 +1,263 @@
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace hdl_graph_slam {
+
+class FloorDetectionNodelet : public nodelet::Nodelet {
+public:
+ typedef pcl::PointXYZI PointT;
+ EIGEN_MAKE_ALIGNED_OPERATOR_NEW
+
+ FloorDetectionNodelet() {}
+ virtual ~FloorDetectionNodelet() {}
+
+ virtual void onInit() {
+ NODELET_DEBUG("initializing floor_detection_nodelet...");
+ nh = getNodeHandle();
+ private_nh = getPrivateNodeHandle();
+
+ initialize_params();
+
+ points_sub = nh.subscribe("/filtered_points", 256, &FloorDetectionNodelet::cloud_callback, this);
+ floor_pub = nh.advertise("/floor_detection/floor_coeffs", 32);
+
+ read_until_pub = nh.advertise("/floor_detection/read_until", 32);
+ floor_filtered_pub = nh.advertise("/floor_detection/floor_filtered_points", 32);
+ floor_points_pub = nh.advertise("/floor_detection/floor_points", 32);
+ }
+
+
+private:
+ /**
+ * @brief initialize parameters
+ */
+ void initialize_params() {
+ tilt_deg = private_nh.param("tilt_deg", 0.0); // approximate sensor tilt angle [deg]
+ sensor_height = private_nh.param("sensor_height", 2.0); // approximate sensor height [m]
+ height_clip_range= private_nh.param("height_clip_range", 1.0); // points with heights in [sensor_height - height_clip_range, sensor_height + height_clip_range] will be used for floor detection
+ floor_pts_thresh = private_nh.param("floor_pts_thresh", 512); // minimum number of support points of RANSAC to accept a detected floor plane
+ floor_normal_thresh = private_nh.param("floor_normal_thresh", 10.0); // verticality check thresold for the detected floor plane [deg]
+ use_normal_filtering = private_nh.param("use_normal_filtering", true); // if true, points with "non-"vertical normals will be filtered before RANSAC
+ normal_filter_thresh = private_nh.param("normal_filter_thresh", 20.0); // "non-"verticality check threshold [deg]
+ }
+
+ /**
+ * @brief callback for point clouds
+ * @param cloud_msg point cloud msg
+ */
+ void cloud_callback(const sensor_msgs::PointCloud2ConstPtr& cloud_msg) {
+ pcl::PointCloud::Ptr cloud(new pcl::PointCloud());
+ pcl::fromROSMsg(*cloud_msg, *cloud);
+
+ if(cloud->empty()) {
+ return;
+ }
+
+ // floor detection
+ boost::optional floor = detect(cloud);
+
+ // publish the detected floor coefficients
+ hdl_graph_slam::FloorCoeffs coeffs;
+ coeffs.header = cloud_msg->header;
+ if(floor) {
+ coeffs.coeffs.resize(4);
+ for(int i=0; i<4; i++) {
+ coeffs.coeffs[i] = (*floor)[i];
+ }
+ }
+
+ floor_pub.publish(coeffs);
+
+ // for offline estimation
+ std_msgs::HeaderPtr read_until(new std_msgs::Header());
+ read_until->frame_id = "/velodyne_points";
+ read_until->stamp = cloud_msg->header.stamp + ros::Duration(1, 0);
+ read_until_pub.publish(read_until);
+
+ read_until->frame_id = "/filtered_points";
+ read_until_pub.publish(read_until);
+ }
+
+ /**
+ * @brief detect the floor plane from a point cloud
+ * @param cloud input cloud
+ * @return detected floor plane coefficients
+ */
+ boost::optional detect(const pcl::PointCloud::Ptr& cloud) const {
+ // compensate the tilt rotation
+ Eigen::Matrix4f tilt_matrix = Eigen::Matrix4f::Identity();
+ tilt_matrix.topLeftCorner(3, 3) = Eigen::AngleAxisf(tilt_deg * M_PI / 180.0f, Eigen::Vector3f::UnitY()).toRotationMatrix();
+
+ // filtering before RANSAC (height and normal filtering)
+ pcl::PointCloud::Ptr filtered(new pcl::PointCloud);
+ pcl::transformPointCloud(*cloud, *filtered, tilt_matrix);
+ filtered = plane_clip(filtered, Eigen::Vector4f(0.0f, 0.0f, 1.0f, sensor_height + height_clip_range), false);
+ filtered = plane_clip(filtered, Eigen::Vector4f(0.0f, 0.0f, 1.0f, sensor_height - height_clip_range), true);
+
+ if(use_normal_filtering) {
+ filtered = normal_filtering(filtered);
+ }
+
+ pcl::transformPointCloud(*filtered, *filtered, static_cast(tilt_matrix.inverse()));
+
+ if(floor_filtered_pub.getNumSubscribers()) {
+ filtered->header = cloud->header;
+ floor_filtered_pub.publish(filtered);
+ }
+
+ // too few points for RANSAC
+ if(filtered->size() < floor_pts_thresh) {
+ return boost::none;
+ }
+
+ // RANSAC
+ pcl::SampleConsensusModelPlane::Ptr model_p(new pcl::SampleConsensusModelPlane(filtered));
+ pcl::RandomSampleConsensus ransac(model_p);
+ ransac.setDistanceThreshold(0.1);
+ ransac.computeModel();
+
+ pcl::PointIndices::Ptr inliers(new pcl::PointIndices);
+ ransac.getInliers(inliers->indices);
+
+ // too few inliers
+ if(inliers->indices.size() < floor_pts_thresh) {
+ return boost::none;
+ }
+
+ // verticality check of the detected floor's normal
+ Eigen::Vector4f reference = tilt_matrix.inverse() * Eigen::Vector4f::UnitZ();
+
+ Eigen::VectorXf coeffs;
+ ransac.getModelCoefficients(coeffs);
+
+ double dot = coeffs.head<3>().dot(reference.head<3>());
+ if(std::abs(dot) < std::cos(floor_normal_thresh * M_PI / 180.0)) {
+ // the normal is not vertical
+ return boost::none;
+ }
+
+ // make the normal upward
+ if(coeffs.head<3>().dot(Eigen::Vector3f::UnitZ()) < 0.0f) {
+ coeffs *= -1.0f;
+ }
+
+ if(floor_points_pub.getNumSubscribers()) {
+ pcl::PointCloud::Ptr inlier_cloud(new pcl::PointCloud);
+ pcl::ExtractIndices extract;
+ extract.setInputCloud(filtered);
+ extract.setIndices(inliers);
+ extract.filter(*inlier_cloud);
+ inlier_cloud->header = cloud->header;
+
+ floor_points_pub.publish(inlier_cloud);
+ }
+
+ return Eigen::Vector4f(coeffs);
+ }
+
+ /**
+ * @brief plane_clip
+ * @param src_cloud
+ * @param plane
+ * @param negative
+ * @return
+ */
+ pcl::PointCloud::Ptr plane_clip(const pcl::PointCloud::Ptr& src_cloud, const Eigen::Vector4f& plane, bool negative) const {
+ pcl::PlaneClipper3D clipper(plane);
+ pcl::PointIndices::Ptr indices(new pcl::PointIndices);
+
+ clipper.clipPointCloud3D(*src_cloud, indices->indices);
+
+ pcl::PointCloud::Ptr dst_cloud(new pcl::PointCloud);
+
+ pcl::ExtractIndices extract;
+ extract.setInputCloud(src_cloud);
+ extract.setIndices(indices);
+ extract.setNegative(negative);
+ extract.filter(*dst_cloud);
+
+ return dst_cloud;
+ }
+
+ /**
+ * @brief filter points with non-vertical normals
+ * @param cloud input cloud
+ * @return filtered cloud
+ */
+ pcl::PointCloud::Ptr normal_filtering(const pcl::PointCloud::Ptr& cloud) const {
+ pcl::NormalEstimation ne;
+ ne.setInputCloud(cloud);
+
+ pcl::search::KdTree::Ptr tree(new pcl::search::KdTree);
+ ne.setSearchMethod(tree);
+
+ pcl::PointCloud::Ptr normals(new pcl::PointCloud);
+ ne.setKSearch(10);
+ ne.setViewPoint(0.0f, 0.0f, sensor_height);
+ ne.compute(*normals);
+
+ pcl::PointCloud::Ptr filtered(new pcl::PointCloud);
+ filtered->reserve(cloud->size());
+
+ for (int i = 0; i < cloud->size(); i++) {
+ float dot = normals->at(i).getNormalVector3fMap().normalized().dot(Eigen::Vector3f::UnitZ());
+ if (std::abs(dot) > std::cos(normal_filter_thresh * M_PI / 180.0)) {
+ filtered->push_back(cloud->at(i));
+ }
+ }
+
+ filtered->width = filtered->size();
+ filtered->height = 1;
+ filtered->is_dense = false;
+
+ return filtered;
+ }
+
+
+private:
+ ros::NodeHandle nh;
+ ros::NodeHandle private_nh;
+
+ // ROS topics
+ ros::Subscriber points_sub;
+
+ ros::Publisher floor_pub;
+ ros::Publisher floor_points_pub;
+ ros::Publisher floor_filtered_pub;
+
+ ros::Publisher read_until_pub;
+
+ // floor detection parameters
+ // see initialize_params() for the details
+ double tilt_deg;
+ double sensor_height;
+ double height_clip_range;
+
+ int floor_pts_thresh;
+ double floor_normal_thresh;
+
+ bool use_normal_filtering;
+ double normal_filter_thresh;
+};
+
+}
+
+PLUGINLIB_EXPORT_CLASS(hdl_graph_slam::FloorDetectionNodelet, nodelet::Nodelet)
diff --git a/apps/hdl_graph_slam_nodelet.cpp b/apps/hdl_graph_slam_nodelet.cpp
new file mode 100644
index 00000000..b0c39064
--- /dev/null
+++ b/apps/hdl_graph_slam_nodelet.cpp
@@ -0,0 +1,697 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+
+namespace hdl_graph_slam {
+
+class HdlGraphSlamNodelet : public nodelet::Nodelet {
+public:
+ typedef pcl::PointXYZI PointT;
+
+ HdlGraphSlamNodelet() {
+ }
+ virtual ~HdlGraphSlamNodelet() {
+
+ }
+
+ virtual void onInit() {
+ nh = getNodeHandle();
+ mt_nh = getMTNodeHandle();
+ private_nh = getPrivateNodeHandle();
+
+ // init parameters
+ map_frame_id = private_nh.param("map_frame_id", "map");
+ odom_frame_id = private_nh.param("odom_frame_id", "odom");
+ map_cloud_resolution = private_nh.param("map_cloud_resolution", 0.05);
+ trans_odom2map.setIdentity();
+
+ max_keyframes_per_update = private_nh.param("max_keyframes_per_update", 10);
+
+ //
+ graph_slam.reset(new GraphSLAM());
+ keyframe_updater.reset(new KeyframeUpdater(private_nh));
+ loop_detector.reset(new LoopDetector(private_nh));
+ map_cloud_generator.reset(new MapCloudGenerator());
+ inf_calclator.reset(new InformationMatrixCalculator(private_nh));
+ nmea_parser.reset(new NmeaSentenceParser());
+
+ gps_edge_stddev = private_nh.param("gps_edge_stddev", 10000.0);
+ floor_edge_stddev = private_nh.param("floor_edge_stddev", 10.0);
+ last_gps_edge_stamp = ros::Time(0);
+
+ // subscribers
+ odom_sub.reset(new message_filters::Subscriber(nh, "/odom", 32));
+ cloud_sub.reset(new message_filters::Subscriber(nh, "/filtered_points", 32));
+ sync.reset(new message_filters::TimeSynchronizer(*odom_sub, *cloud_sub, 32));
+ sync->registerCallback(boost::bind(&HdlGraphSlamNodelet::cloud_callback, this, _1, _2));
+ floor_sub = nh.subscribe("/floor_detection/floor_coeffs", 32, &HdlGraphSlamNodelet::floor_coeffs_callback, this);
+ gps_sub = nh.subscribe("/gps/geopoint", 32, &HdlGraphSlamNodelet::gps_callback, this);
+ nmea_sub = nh.subscribe("/gpsimu_driver/nmea_sentence", 32, &HdlGraphSlamNodelet::nmea_callback, this);
+
+ // publishers
+ markers_pub = nh.advertise("/hdl_graph_slam/markers", 16);
+ odom2map_pub = nh.advertise("/hdl_graph_slam/odom2pub", 16);
+ map_points_pub = mt_nh.advertise("/hdl_graph_slam/map_points", 1);
+ read_until_pub = nh.advertise("/hdl_graph_slam/read_until", 32);
+
+ dump_service_server = nh.advertiseService("/hdl_graph_slam/dump", &HdlGraphSlamNodelet::dump_service, this);
+
+ double graph_update_interval = private_nh.param("graph_update_interval", 3.0);
+ double map_cloud_update_interval = private_nh.param("map_cloud_update_interval", 10.0);
+ optimization_timer = mt_nh.createTimer(ros::Duration(graph_update_interval), &HdlGraphSlamNodelet::optimization_timer_callback, this);
+ map_publish_timer = mt_nh.createWallTimer(ros::WallDuration(map_cloud_update_interval), &HdlGraphSlamNodelet::map_points_publish_timer_callback, this);
+ }
+
+private:
+ /**
+ * @brief received point clouds are pushed to #keyframe_queue
+ * @param odom_msg
+ * @param cloud_msg
+ */
+ void cloud_callback(const nav_msgs::OdometryConstPtr& odom_msg, const sensor_msgs::PointCloud2::ConstPtr& cloud_msg) {
+ const ros::Time& stamp = odom_msg->header.stamp;
+ Eigen::Isometry3d odom = odom2isometry(odom_msg);
+
+ pcl::PointCloud::Ptr cloud(new pcl::PointCloud());
+ pcl::fromROSMsg(*cloud_msg, *cloud);
+
+ if(!keyframe_updater->update(odom)) {
+ std::lock_guard lock(keyframe_queue_mutex);
+ if(keyframe_queue.empty()) {
+ std_msgs::Header read_until;
+ read_until.stamp = stamp + ros::Duration(30, 0);
+ read_until.frame_id = "/velodyne_points";
+ read_until_pub.publish(read_until);
+ read_until.frame_id = "/filtered_points";
+ read_until_pub.publish(read_until);
+ }
+
+ return;
+ }
+
+ double accum_d = keyframe_updater->get_accum_distance();
+ KeyFrame::Ptr keyframe(new KeyFrame(stamp, odom, accum_d, cloud));
+
+ std::lock_guard lock(keyframe_queue_mutex);
+ keyframe_queue.push_back(keyframe);
+ }
+
+ /**
+ * @brief this method adds all the keyframes in #keyframe_queue to the pose graph
+ * @return if true, at least one keyframe is added to the pose graph
+ */
+ bool flush_keyframe_queue() {
+ std::lock_guard lock(keyframe_queue_mutex);
+
+ if(keyframe_queue.empty()) {
+ return false;
+ }
+
+ trans_odom2map_mutex.lock();
+ Eigen::Isometry3d odom2map(trans_odom2map.cast());
+ trans_odom2map_mutex.unlock();
+
+ int num_processed = 0;
+ for(int i=0; i(keyframe_queue.size(), max_keyframes_per_update); i++) {
+ num_processed = i;
+
+ const auto& keyframe = keyframe_queue[i];
+ new_keyframes.push_back(keyframe);
+
+ Eigen::Isometry3d odom = odom2map * keyframe->odom;
+ keyframe->node = graph_slam->add_se3_node(odom);
+ keyframe_hash[keyframe->stamp] = keyframe;
+
+ if(i==0 && keyframes.empty()) {
+ continue;
+ }
+
+ // add edge between keyframes
+ const auto& prev_keyframe = i == 0 ? keyframes.back() : keyframe_queue[i - 1];
+
+ Eigen::Isometry3d relative_pose = keyframe->odom.inverse() * prev_keyframe->odom;
+ Eigen::MatrixXd information = inf_calclator->calc_information_matrix(prev_keyframe->cloud, keyframe->cloud, relative_pose);
+ graph_slam->add_se3_edge(keyframe->node, prev_keyframe->node, relative_pose, information);
+ }
+
+ std_msgs::Header read_until;
+ read_until.stamp = keyframe_queue[num_processed]->stamp + ros::Duration(30, 0);
+ read_until.frame_id = "/velodyne_points";
+ read_until_pub.publish(read_until);
+ read_until.frame_id = "/filtered_points";
+ read_until_pub.publish(read_until);
+
+ keyframe_queue.erase(keyframe_queue.begin(), keyframe_queue.begin() + num_processed + 1);
+
+ return true;
+ }
+
+ void nmea_callback(const nmea_msgs::SentenceConstPtr& nmea_msg) {
+ GPRMC grmc = nmea_parser->parse(nmea_msg->sentence);
+
+ if(grmc.status != 'A') {
+ return;
+ }
+
+ geographic_msgs::GeoPointStampedPtr gps_msg(new geographic_msgs::GeoPointStamped());
+ gps_msg->header = nmea_msg->header;
+ gps_msg->position.latitude = grmc.latitude;
+ gps_msg->position.longitude = grmc.longitude;
+ gps_msg->position.altitude = NAN;
+
+ gps_callback(gps_msg);
+ }
+
+ /**
+ * @brief received gps data is added to #gps_queue
+ * @param gps_msg
+ */
+ void gps_callback(const geographic_msgs::GeoPointStampedConstPtr& gps_msg) {
+ std::lock_guard lock(gps_queue_mutex);
+ gps_queue.push_back(gps_msg);
+ }
+
+ std::ofstream ofs;
+
+ /**
+ * @brief
+ * @return
+ */
+ bool flush_gps_queue() {
+ std::lock_guard lock(gps_queue_mutex);
+
+ if(keyframes.empty()) {
+ return false;
+ }
+
+ const auto& latest_keyframe_stamp = keyframes.back()->stamp;
+
+ bool updated = false;
+ auto seek = keyframes.begin();
+ for(const auto& gps_msg : gps_queue) {
+ seek = std::lower_bound(seek, keyframes.end(), gps_msg->header.stamp, [&](const KeyFrame::Ptr& key, const ros::Time& stamp) { return key->stamp < stamp; });
+ if(seek == keyframes.end()) {
+ break;
+ }
+
+ double residual = ((*seek)->stamp - gps_msg->header.stamp).toSec();
+ if(std::abs(residual) > 0.1 || (*seek)->utm_coord) {
+ continue;
+ }
+
+ if(gps_msg->header.stamp - last_gps_edge_stamp < ros::Duration(30.0)) {
+ continue;
+ }
+
+ geodesy::UTMPoint utm;
+ geodesy::fromMsg(gps_msg->position, utm);
+
+ if(!ofs.is_open()) {
+ ofs.open("latlon");
+ }
+
+ ofs << boost::format("%.9f %.9f %.9f %.9f") % gps_msg->position.latitude % gps_msg->position.longitude % utm.easting % utm.northing << std::endl;
+
+ Eigen::Vector3d xyz(utm.easting, utm.northing, utm.altitude);
+
+ if(!zero_utm) {
+ zero_utm = xyz;
+ }
+ xyz -= (*zero_utm);
+
+ (*seek)->utm_coord = xyz;
+
+ Eigen::Matrix2d information_matrix = Eigen::Matrix2d::Identity() / gps_edge_stddev;
+ graph_slam->add_se3_prior_xy_edge((*seek)->node, xyz.head<2>(), information_matrix);
+
+ last_gps_edge_stamp = gps_msg->header.stamp;
+
+ updated = true;
+ }
+
+ auto remove_loc = std::upper_bound(gps_queue.begin(), gps_queue.end(), latest_keyframe_stamp,
+ [=](const ros::Time& stamp, const geographic_msgs::GeoPointStampedConstPtr& geopoint) {
+ return stamp < geopoint->header.stamp;
+ }
+ );
+ gps_queue.erase(gps_queue.begin(), remove_loc);
+
+ return updated;
+ }
+
+
+ /**
+ * @brief received floor coefficients are added to #floor_coeffs_queue
+ * @param floor_coeffs_msg
+ */
+ void floor_coeffs_callback(const hdl_graph_slam::FloorCoeffsConstPtr& floor_coeffs_msg) {
+ if(floor_coeffs_msg->coeffs.empty()) {
+ return;
+ }
+
+ std::lock_guard lock(floor_coeffs_queue_mutex);
+ floor_coeffs_queue.push_back(floor_coeffs_msg);
+ }
+
+ /**
+ * @brief this methods associates floor coefficients messages with registered keyframes, and then adds the associated coeffs to the pose graph
+ * @return if true, at least one floor plane edge is added to the pose graph
+ */
+ bool flush_floor_queue() {
+ std::lock_guard lock(floor_coeffs_queue_mutex);
+
+ if(keyframes.empty()) {
+ return false;
+ }
+
+ const auto& latest_keyframe_stamp = keyframes.back()->stamp;
+
+ bool updated = false;
+ for(const auto& floor_coeffs : floor_coeffs_queue) {
+ if(floor_coeffs->header.stamp > latest_keyframe_stamp) {
+ break;
+ }
+
+ auto found = keyframe_hash.find(floor_coeffs->header.stamp);
+ if(found == keyframe_hash.end()) {
+ continue;
+ }
+
+ const auto& keyframe = found->second;
+
+ Eigen::Vector4d coeffs(floor_coeffs->coeffs[0], floor_coeffs->coeffs[1], floor_coeffs->coeffs[2], floor_coeffs->coeffs[3]);
+ Eigen::Matrix3d information = Eigen::Matrix3d::Identity() * (1.0 / floor_edge_stddev);
+ graph_slam->add_se3_plane_edge(keyframe->node, graph_slam->floor_plane_node, coeffs, information);
+
+ keyframe->floor_coeffs = coeffs;
+
+ updated = true;
+ }
+
+ auto remove_loc = std::upper_bound(floor_coeffs_queue.begin(), floor_coeffs_queue.end(), latest_keyframe_stamp,
+ [=](const ros::Time& stamp, const hdl_graph_slam::FloorCoeffsConstPtr& coeffs) {
+ return stamp < coeffs->header.stamp;
+ }
+ );
+ floor_coeffs_queue.erase(floor_coeffs_queue.begin(), remove_loc);
+
+ return updated;
+ }
+
+ /**
+ * @brief generate a map point cloud and publish it
+ * @param event
+ */
+ void map_points_publish_timer_callback(const ros::WallTimerEvent& event) {
+ if(!map_points_pub.getNumSubscribers()) {
+ return;
+ }
+
+ std::vector snapshot;
+
+ keyframes_snapshot_mutex.lock();
+ snapshot = keyframes_snapshot;
+ keyframes_snapshot_mutex.unlock();
+
+ auto cloud = map_cloud_generator->generate(snapshot, 0.05);
+ if(!cloud) {
+ return;
+ }
+
+ cloud->header.frame_id = map_frame_id;
+ cloud->header.stamp = snapshot.back()->cloud->header.stamp;
+
+ sensor_msgs::PointCloud2Ptr cloud_msg(new sensor_msgs::PointCloud2());
+ pcl::toROSMsg(*cloud, *cloud_msg);
+
+ map_points_pub.publish(cloud_msg);
+ }
+
+ /**
+ * @brief this methods adds all the data in the queues to the pose graph, and then optimizes the pose graph
+ * @param event
+ */
+ void optimization_timer_callback(const ros::TimerEvent& event) {
+ std::lock_guard lock(main_thread_mutex);
+
+ // add keyframes and floor coeffs in the queues to the pose graph
+ if(!flush_keyframe_queue() & !flush_floor_queue() & !flush_gps_queue()) {
+ std_msgs::Header read_until;
+ read_until.stamp = event.current_real + ros::Duration(30, 0);
+ read_until.frame_id = "/velodyne_points";
+ read_until_pub.publish(read_until);
+ read_until.frame_id = "/filtered_points";
+ read_until_pub.publish(read_until);
+
+ return;
+ }
+
+ // loop detection
+ std::vector loops = loop_detector->detect(keyframes, new_keyframes, *graph_slam);
+ for(const auto& loop : loops) {
+ Eigen::Isometry3d relpose(loop->relative_pose.cast());
+ Eigen::MatrixXd information_matrix = inf_calclator->calc_information_matrix(loop->key1->cloud, loop->key2->cloud, relpose);
+ graph_slam->add_se3_edge(loop->key1->node, loop->key2->node, relpose, information_matrix);
+ }
+
+ std::copy(new_keyframes.begin(), new_keyframes.end(), std::back_inserter(keyframes));
+ new_keyframes.clear();
+
+ // optimize the pose graph
+ graph_slam->optimize();
+
+ // publish tf
+ const auto& keyframe = keyframes.back();
+ Eigen::Isometry3d trans = keyframe->node->estimate() * keyframe->odom.inverse();
+ trans_odom2map_mutex.lock();
+ trans_odom2map = trans.matrix().cast();
+ trans_odom2map_mutex.unlock();
+
+ if(map_points_pub.getNumSubscribers()) {
+ std::vector snapshot(keyframes.size());
+ std::transform(keyframes.begin(), keyframes.end(), snapshot.begin(),
+ [=](const KeyFrame::Ptr& k) {
+ return std::make_shared(k);
+ });
+
+ std::lock_guard lock(keyframes_snapshot_mutex);
+ keyframes_snapshot.swap(snapshot);
+ }
+
+ if(odom2map_pub.getNumSubscribers()) {
+ geometry_msgs::TransformStamped ts = matrix2transform(keyframe->stamp, trans.matrix().cast(), map_frame_id, odom_frame_id);
+ odom2map_pub.publish(ts);
+ }
+
+ if(markers_pub.getNumSubscribers()) {
+ auto markers = create_marker_array(event.current_real);
+ markers_pub.publish(markers);
+ }
+ }
+
+ /**
+ * @brief create visualization marker
+ * @param stamp
+ * @return
+ */
+ visualization_msgs::MarkerArray create_marker_array(const ros::Time& stamp) const {
+ visualization_msgs::MarkerArray markers;
+ markers.markers.resize(4);
+
+ // node markers
+ visualization_msgs::Marker& traj_marker = markers.markers[0];
+ traj_marker.header.frame_id = "map";
+ traj_marker.header.stamp = stamp;
+ traj_marker.ns = "nodes";
+ traj_marker.id = 0;
+ traj_marker.type = visualization_msgs::Marker::SPHERE_LIST;
+
+ traj_marker.pose.orientation.w = 1.0;
+ traj_marker.scale.x = traj_marker.scale.y = traj_marker.scale.z = 0.5;
+
+ traj_marker.points.resize(keyframes.size());
+ traj_marker.colors.resize(keyframes.size());
+ for(int i=0; inode->estimate().translation();
+ traj_marker.points[i].x = pos.x();
+ traj_marker.points[i].y = pos.y();
+ traj_marker.points[i].z = pos.z();
+
+ double p = static_cast(i) / keyframes.size();
+ traj_marker.colors[i].r = 1.0 - p;
+ traj_marker.colors[i].g = p;
+ traj_marker.colors[i].b = 0.0;
+ traj_marker.colors[i].a = 1.0;
+ }
+
+ // edge markers
+ visualization_msgs::Marker& edge_marker = markers.markers[1];
+ edge_marker.header.frame_id = "map";
+ edge_marker.header.stamp = stamp;
+ edge_marker.ns = "edges";
+ edge_marker.id = 1;
+ edge_marker.type = visualization_msgs::Marker::LINE_LIST;
+
+ edge_marker.pose.orientation.w = 1.0;
+ edge_marker.scale.x = 0.05;
+
+ edge_marker.points.resize(graph_slam->graph->edges().size() * 2);
+ edge_marker.colors.resize(graph_slam->graph->edges().size() * 2);
+
+ auto edge_itr = graph_slam->graph->edges().begin();
+ for(int i=0; edge_itr != graph_slam->graph->edges().end(); edge_itr++, i++) {
+ g2o::HyperGraph::Edge* edge = *edge_itr;
+ g2o::EdgeSE3* edge_se3 = dynamic_cast(edge);
+ if(edge_se3) {
+ g2o::VertexSE3* v1 = dynamic_cast(edge_se3->vertices()[0]);
+ g2o::VertexSE3* v2 = dynamic_cast(edge_se3->vertices()[1]);
+ Eigen::Vector3d pt1 = v1->estimate().translation();
+ Eigen::Vector3d pt2 = v2->estimate().translation();
+
+ edge_marker.points[i*2].x = pt1.x();
+ edge_marker.points[i*2].y = pt1.y();
+ edge_marker.points[i*2].z = pt1.z();
+ edge_marker.points[i*2 + 1].x = pt2.x();
+ edge_marker.points[i*2 + 1].y = pt2.y();
+ edge_marker.points[i*2 + 1].z = pt2.z();
+
+ double p1 = static_cast(v1->id()) / graph_slam->graph->vertices().size();
+ double p2 = static_cast(v2->id()) / graph_slam->graph->vertices().size();
+ edge_marker.colors[i*2].r = 1.0 - p1;
+ edge_marker.colors[i*2].g = p1;
+ edge_marker.colors[i*2].a = 1.0;
+ edge_marker.colors[i*2 + 1].r = 1.0 - p2;
+ edge_marker.colors[i*2 + 1].g = p2;
+ edge_marker.colors[i*2 + 1].a = 1.0;
+
+ if(std::abs(v1->id() - v2->id()) > 2) {
+ edge_marker.points[i*2].z += 0.5;
+ edge_marker.points[i*2 + 1].z += 0.5;
+ }
+
+ continue;
+ }
+
+ g2o::EdgeSE3Plane* edge_plane = dynamic_cast(edge);
+ if(edge_plane) {
+ g2o::VertexSE3* v1 = dynamic_cast(edge_plane->vertices()[0]);
+ Eigen::Vector3d pt1 = v1->estimate().translation();
+ Eigen::Vector3d pt2(pt1.x(), pt1.y(), 0.0);
+
+ edge_marker.points[i*2].x = pt1.x();
+ edge_marker.points[i*2].y = pt1.y();
+ edge_marker.points[i*2].z = pt1.z();
+ edge_marker.points[i*2 + 1].x = pt2.x();
+ edge_marker.points[i*2 + 1].y = pt2.y();
+ edge_marker.points[i*2 + 1].z = pt2.z();
+
+ edge_marker.colors[i*2].b = 1.0;
+ edge_marker.colors[i*2].a = 1.0;
+ edge_marker.colors[i*2 + 1].b = 1.0;
+ edge_marker.colors[i*2 + 1].a = 1.0;
+
+ continue;
+ }
+
+ g2o::EdgeSE3PriorXY* edge_priori_xy = dynamic_cast(edge);
+ if(edge_priori_xy) {
+ g2o::VertexSE3* v1 = dynamic_cast(edge_priori_xy->vertices()[0]);
+ Eigen::Vector3d pt1 = v1->estimate().translation();
+ Eigen::Vector3d pt2 = Eigen::Vector3d::Zero();
+ pt2.head<2>() = edge_priori_xy->measurement();
+
+ edge_marker.points[i*2].x = pt1.x();
+ edge_marker.points[i*2].y = pt1.y();
+ edge_marker.points[i*2].z = pt1.z();
+ edge_marker.points[i*2 + 1].x = pt2.x();
+ edge_marker.points[i*2 + 1].y = pt2.y();
+ edge_marker.points[i*2 + 1].z = pt2.z();
+
+ edge_marker.colors[i*2].r = 1.0;
+ edge_marker.colors[i*2].a = 1.0;
+ edge_marker.colors[i*2 + 1].r = 1.0;
+ edge_marker.colors[i*2 + 1].a = 1.0;
+
+ continue;
+ }
+ }
+
+ // sphere
+ visualization_msgs::Marker& sphere_marker = markers.markers[3];
+ sphere_marker.header.frame_id = "map";
+ sphere_marker.header.stamp = stamp;
+ sphere_marker.ns = "loop_close_radius";
+ sphere_marker.id = 0;
+ sphere_marker.type = visualization_msgs::Marker::SPHERE;
+
+ if(!keyframes.empty()) {
+ Eigen::Vector3d pos = keyframes.back()->node->estimate().translation();
+ sphere_marker.pose.position.x = pos.x();
+ sphere_marker.pose.position.y = pos.y();
+ sphere_marker.pose.position.z = pos.z();
+ }
+ sphere_marker.pose.orientation.w = 1.0;
+ sphere_marker.scale.x = sphere_marker.scale.y = sphere_marker.scale.z = loop_detector->get_distance_thresh() * 2.0;
+
+ sphere_marker.color.r = 1.0;
+ sphere_marker.color.a = 0.3;
+
+ return markers;
+ }
+
+ /**
+ * @brief dump all data to the current directory
+ * @param req
+ * @param res
+ * @return
+ */
+ bool dump_service(std_srvs::EmptyRequest& req, std_srvs::EmptyResponse& res) {
+ std::lock_guard lock(main_thread_mutex);
+
+ std::array buffer;
+ buffer.fill(0);
+ time_t rawtime;
+ time(&rawtime);
+ const auto timeinfo = localtime(&rawtime);
+ strftime(buffer.data(), sizeof(buffer), "%d-%m-%Y %H:%M:%S", timeinfo);
+ std::string directory(buffer.data());
+
+ if(!boost::filesystem::is_directory(directory)) {
+ boost::filesystem::create_directory(directory);
+ }
+
+ std::cout << "all data dumped to:" << std::flush;
+ system("pwd");
+
+ graph_slam->save(directory + "/graph.g2o");
+ for(int i=0; idump(sst.str());
+ }
+
+ return true;
+ }
+
+private:
+ // ROS
+ ros::NodeHandle nh;
+ ros::NodeHandle mt_nh;
+ ros::NodeHandle private_nh;
+ ros::Timer optimization_timer;
+ ros::WallTimer map_publish_timer;
+
+ std::unique_ptr> odom_sub;
+ std::unique_ptr> cloud_sub;
+ std::unique_ptr> sync;
+
+ ros::Subscriber gps_sub;
+ ros::Subscriber nmea_sub;
+ ros::Subscriber floor_sub;
+
+ ros::Publisher markers_pub;
+
+ std::string map_frame_id;
+ std::string odom_frame_id;
+
+ std::mutex trans_odom2map_mutex;
+ Eigen::Matrix4f trans_odom2map;
+ ros::Publisher odom2map_pub;
+
+ ros::Publisher read_until_pub;
+ ros::Publisher map_points_pub;
+
+ std::atomic_bool dump_request;
+ ros::ServiceServer dump_service_server;
+
+ // keyframe queue
+ std::mutex keyframe_queue_mutex;
+ std::deque keyframe_queue;
+
+ // gps queue
+ double gps_edge_stddev;
+ boost::optional zero_utm;
+ ros::Time last_gps_edge_stamp;
+ std::mutex gps_queue_mutex;
+ std::deque gps_queue;
+
+ // floor_coeffs queue
+ double floor_edge_stddev;
+ std::mutex floor_coeffs_queue_mutex;
+ std::deque floor_coeffs_queue;
+
+ // for map cloud generation
+ double map_cloud_resolution;
+ std::mutex keyframes_snapshot_mutex;
+ std::vector keyframes_snapshot;
+ std::unique_ptr map_cloud_generator;
+
+ // graph slam
+ // all the below members must be accessed after locking main_thread_mutex
+ std::mutex main_thread_mutex;
+
+ int max_keyframes_per_update;
+ std::deque new_keyframes;
+
+ std::vector keyframes;
+ std::unordered_map keyframe_hash;
+
+ std::unique_ptr graph_slam;
+ std::unique_ptr loop_detector;
+ std::unique_ptr keyframe_updater;
+ std::unique_ptr nmea_parser;
+
+ std::unique_ptr inf_calclator;
+};
+
+}
+
+PLUGINLIB_EXPORT_CLASS(hdl_graph_slam::HdlGraphSlamNodelet, nodelet::Nodelet)
diff --git a/apps/prefiltering_nodelet.cpp b/apps/prefiltering_nodelet.cpp
new file mode 100644
index 00000000..4aadd0b4
--- /dev/null
+++ b/apps/prefiltering_nodelet.cpp
@@ -0,0 +1,162 @@
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace hdl_graph_slam {
+
+class PrefilteringNodelet : public nodelet::Nodelet {
+public:
+ typedef pcl::PointXYZI PointT;
+
+ PrefilteringNodelet() {}
+ virtual ~PrefilteringNodelet() {}
+
+ virtual void onInit() {
+ nh = getNodeHandle();
+ private_nh = getPrivateNodeHandle();
+
+ initialize_params();
+
+ points_sub = nh.subscribe("/velodyne_points", 64, &PrefilteringNodelet::cloud_callback, this);
+ points_pub = nh.advertise("/filtered_points", 32);
+ }
+
+private:
+ void initialize_params() {
+ std::string downsample_method = private_nh.param("downsample_method", "VOXELGRID");
+ double downsample_resolution = private_nh.param("downsample_resolution", 0.1);
+
+ if(downsample_method == "VOXELGRID") {
+ std::cout << "downsample: VOXELGRID " << downsample_resolution << std::endl;
+ boost::shared_ptr> voxelgrid(new pcl::VoxelGrid());
+ voxelgrid->setLeafSize(downsample_resolution, downsample_resolution, downsample_resolution);
+ downsample_filter = voxelgrid;
+ } else if(downsample_method == "APPROX_VOXELGRID") {
+ std::cout << "downsample: APPROX_VOXELGRID " << downsample_resolution << std::endl;
+ boost::shared_ptr> approx_voxelgrid(new pcl::ApproximateVoxelGrid());
+ approx_voxelgrid->setLeafSize(downsample_resolution, downsample_resolution, downsample_resolution);
+ downsample_filter = approx_voxelgrid;
+ } else {
+ if(downsample_method != "NONE") {
+ std::cerr << "warning: unknown downsampling type (" << downsample_method << ")" << std::endl;
+ std::cerr << " : use passthrough filter" <("outlier_removal_method", "STATISTICAL");
+ if(outlier_removal_method == "STATISTICAL") {
+ int mean_k = private_nh.param("statistical_mean_k", 20);
+ double stddev_mul_thresh = private_nh.param("statistical_stddev", 1.0);
+ std::cout << "outlier_removal: STATISTICAL " << mean_k << " - " << stddev_mul_thresh << std::endl;
+
+ pcl::StatisticalOutlierRemoval::Ptr sor(new pcl::StatisticalOutlierRemoval());
+ sor->setMeanK(mean_k);
+ sor->setStddevMulThresh(stddev_mul_thresh);
+ outlier_removal_filter = sor;
+ } else if(outlier_removal_method == "RADIUS") {
+ double radius = private_nh.param("radius_radius", 0.8);
+ int min_neighbors = private_nh.param("radus_min_neighbors", 2);
+ std::cout << "outlier_removal: RADIUS " << radius << " - " << min_neighbors << std::endl;
+
+ pcl::RadiusOutlierRemoval::Ptr rad(new pcl::RadiusOutlierRemoval());
+ rad->setRadiusSearch(radius);
+ rad->setMinNeighborsInRadius(min_neighbors);
+ } else {
+ std::cout << "outlier_removal: NONE" << std::endl;
+ }
+
+ use_distance_filter = private_nh.param("use_distance_filter", true);
+ distance_near_thresh = private_nh.param("distance_near_thresh", 1.0);
+ distance_far_thresh = private_nh.param("distance_far_thresh", 100.0);
+ }
+
+ void cloud_callback(const pcl::PointCloud::ConstPtr& src_cloud) {
+ if(src_cloud->empty()) {
+ return;
+ }
+
+ pcl::PointCloud::ConstPtr filtered = distance_filter(src_cloud);
+ filtered = downsample(filtered);
+ filtered = outlier_removal(filtered);
+
+ points_pub.publish(filtered);
+ }
+
+ pcl::PointCloud::ConstPtr downsample(const pcl::PointCloud::ConstPtr& cloud) const {
+ if(!downsample_filter) {
+ return cloud;
+ }
+
+ pcl::PointCloud::Ptr filtered(new pcl::PointCloud());
+ downsample_filter->setInputCloud(cloud);
+ downsample_filter->filter(*filtered);
+ filtered->header = cloud->header;
+
+ return filtered;
+ }
+
+ pcl::PointCloud::ConstPtr outlier_removal(const pcl::PointCloud::ConstPtr& cloud) const {
+ if(!outlier_removal_filter) {
+ return cloud;
+ }
+
+ pcl::PointCloud::Ptr filtered(new pcl::PointCloud());
+ outlier_removal_filter->setInputCloud(cloud);
+ outlier_removal_filter->filter(*filtered);
+ filtered->header = cloud->header;
+
+ return filtered;
+ }
+
+ pcl::PointCloud::ConstPtr distance_filter(const pcl::PointCloud::ConstPtr& cloud) const {
+ pcl::PointCloud::Ptr filtered(new pcl::PointCloud());
+ filtered->reserve(cloud->size());
+
+ std::copy_if(cloud->begin(), cloud->end(), std::back_inserter(filtered->points),
+ [&](const PointT& p) {
+ double d = p.getVector3fMap().norm();
+ return d > distance_near_thresh && d < distance_far_thresh;
+ }
+ );
+
+ filtered->width = filtered->size();
+ filtered->height = 1;
+ filtered->is_dense = false;
+
+ filtered->header = cloud->header;
+
+ return filtered;
+ }
+
+private:
+ ros::NodeHandle nh;
+ ros::NodeHandle private_nh;
+
+ ros::Subscriber points_sub;
+ ros::Publisher points_pub;
+
+ bool use_distance_filter;
+ double distance_near_thresh;
+ double distance_far_thresh;
+
+ pcl::Filter::Ptr downsample_filter;
+ pcl::Filter::Ptr outlier_removal_filter;
+
+};
+
+}
+
+PLUGINLIB_EXPORT_CLASS(hdl_graph_slam::PrefilteringNodelet, nodelet::Nodelet)
diff --git a/apps/scan_matching_odometry_nodelet.cpp b/apps/scan_matching_odometry_nodelet.cpp
new file mode 100644
index 00000000..a3a7914e
--- /dev/null
+++ b/apps/scan_matching_odometry_nodelet.cpp
@@ -0,0 +1,266 @@
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+
+namespace hdl_graph_slam {
+
+class ScanMatchingOdometryNodelet : public nodelet::Nodelet {
+public:
+ typedef pcl::PointXYZI PointT;
+ EIGEN_MAKE_ALIGNED_OPERATOR_NEW
+
+ ScanMatchingOdometryNodelet() {}
+ virtual ~ScanMatchingOdometryNodelet() {}
+
+ virtual void onInit() {
+ NODELET_DEBUG("initializing scan_matching_odometry_nodelet...");
+ nh = getNodeHandle();
+ private_nh = getPrivateNodeHandle();
+
+ initialize_params();
+
+ points_sub = nh.subscribe("/filtered_points", 256, &ScanMatchingOdometryNodelet::cloud_callback, this);
+ read_until_pub = nh.advertise("/scan_matching_odometry/read_until", 32);
+ odom_pub = nh.advertise("/odom", 32);
+ }
+
+private:
+ /**
+ * @brief initialize parameters
+ */
+ void initialize_params() {
+ auto& pnh = private_nh;
+ odom_frame_id = pnh.param("odom_frame_id", "odom");
+ base_frame_id = pnh.param("base_frame_id", "velodyne");
+
+ // The minimum tranlational distance and rotation angle between keyframes.
+ // If this value is zero, frames are always compared with the previous frame
+ keyframe_delta_trans = pnh.param("keyframe_delta_trans", 0.25);
+ keyframe_delta_angle = pnh.param("keyframe_delta_angle", 0.15);
+ keyframe_delta_time = pnh.param("keyframe_delta_time", 1.0);
+
+ // Registration validation by thresholding
+ transform_thresholding = pnh.param("transform_thresholding", false);
+ max_acceptable_trans = pnh.param("max_acceptable_trans", 1.0);
+ max_acceptable_angle = pnh.param("max_acceptable_angle", 1.0);
+
+ // select a downsample method (VOXELGRID, APPROX_VOXELGRID, NONE)
+ std::string downsample_method = pnh.param("downsample_method", "VOXELGRID");
+ double downsample_resolution = pnh.param("downsample_resolution", 0.1);
+ if(downsample_method == "VOXELGRID") {
+ std::cout << "downsample: VOXELGRID " << downsample_resolution << std::endl;
+ boost::shared_ptr> voxelgrid(new pcl::VoxelGrid());
+ voxelgrid->setLeafSize(downsample_resolution, downsample_resolution, downsample_resolution);
+ downsample_filter = voxelgrid;
+ } else if(downsample_method == "APPROX_VOXELGRID") {
+ std::cout << "downsample: APPROX_VOXELGRID " << downsample_resolution << std::endl;
+ boost::shared_ptr> approx_voxelgrid(new pcl::ApproximateVoxelGrid());
+ approx_voxelgrid->setLeafSize(downsample_resolution, downsample_resolution, downsample_resolution);
+ downsample_filter = approx_voxelgrid;
+ } else {
+ if(downsample_method != "NONE") {
+ std::cerr << "warning: unknown downsampling type (" << downsample_method << ")" << std::endl;
+ std::cerr << " : use passthrough filter" <> passthrough(new pcl::PassThrough());
+ downsample_filter = passthrough;
+ }
+
+ registration = select_registration_method(pnh);
+ }
+
+ /**
+ * @brief callback for point clouds
+ * @param cloud_msg point cloud msg
+ */
+ void cloud_callback(const sensor_msgs::PointCloud2ConstPtr& cloud_msg) {
+ if(!ros::ok()) {
+ return;
+ }
+
+ pcl::PointCloud::Ptr cloud(new pcl::PointCloud());
+ pcl::fromROSMsg(*cloud_msg, *cloud);
+
+ Eigen::Matrix4f pose = matching(cloud_msg->header.stamp, cloud);
+ publish_odometry(cloud_msg->header.stamp, pose);
+
+ // In offline estimation, point clouds until the published time will be supplied
+ std_msgs::HeaderPtr read_until(new std_msgs::Header());
+ read_until->frame_id = "/velodyne_points";
+ read_until->stamp = cloud_msg->header.stamp + ros::Duration(1, 0);
+ read_until_pub.publish(read_until);
+
+ read_until->frame_id = "/filtered_points";
+ read_until_pub.publish(read_until);
+
+ }
+
+ /**
+ * @brief downsample a point cloud
+ * @param cloud input cloud
+ * @return downsampled point cloud
+ */
+ pcl::PointCloud::ConstPtr downsample(const pcl::PointCloud::ConstPtr& cloud) const {
+ if(!downsample_filter) {
+ return cloud;
+ }
+
+ pcl::PointCloud::Ptr filtered(new pcl::PointCloud());
+ downsample_filter->setInputCloud(cloud);
+ downsample_filter->filter(*filtered);
+
+ return filtered;
+ }
+
+ /**
+ * @brief estimate the relative pose between an input cloud and a keyframe cloud
+ * @param stamp the timestamp of the input cloud
+ * @param cloud the input cloud
+ * @return the relative pose between the input cloud and the keyframe cloud
+ */
+ Eigen::Matrix4f matching(const ros::Time& stamp, const pcl::PointCloud