/** **************************************************************************************** * This node presents a fast and precise method to estimate the planar motion of a lidar * from consecutive range scans. It is very useful for the estimation of the robot odometry from * 2D laser range measurements. * This module is developed for mobile robots with innacurate or inexistent built-in odometry. * It allows the estimation of a precise odometry with low computational cost. * For more information, please refer to: * * Planar Odometry from a Radial Laser Scanner. A Range Flow-based Approach. ICRA'16. * Available at: http://mapir.isa.uma.es/mapirwebsite/index.php/mapir-downloads/papers/217 * * Maintainer: Javier G. Monroy * MAPIR group: http://mapir.isa.uma.es/ ******************************************************************************************** */ #include "rf2o_laser_odometry/CLaserOdometry2D.h" // -------------------------------------------- // CLaserOdometry2D //--------------------------------------------- CLaserOdometry2D::CLaserOdometry2D() : module_initialized(false), first_laser_scan(true) { // } CLaserOdometry2D::~CLaserOdometry2D() { // } void CLaserOdometry2D::setLaserPose(const mrpt::poses::CPose3D& laser_pose) { //Set laser pose on the robot laser_pose_on_robot = laser_pose; laser_pose_on_robot_inv = laser_pose_on_robot; laser_pose_on_robot_inv.inverse(); } bool CLaserOdometry2D::is_initialized() { return module_initialized; } void CLaserOdometry2D::Init(const sensor_msgs::LaserScan& scan, const geometry_msgs::Pose& initial_robot_pose) { //Got an initial scan laser, obtain its parametes ROS_INFO_COND(verbose, "[rf2o] Got first Laser Scan .... Configuring node"); width = scan.ranges.size(); // Num of samples (size) of the scan laser cols = width; // Max reolution. Should be similar to the width parameter fovh = fabs(scan.angle_max - scan.angle_min); // Horizontal Laser's FOV ctf_levels = 5; // Coarse-to-Fine levels iter_irls = 5; //Num iterations to solve iterative reweighted least squares //Robot initial pose (see MQTT:bridge) mrpt::poses::CPose3D robotInitialPose; geometry_msgs::Pose _src = initial_robot_pose; robotInitialPose.x(_src.position.x); robotInitialPose.y(_src.position.y); mrpt::math::CQuaternionDouble quat; quat.x(_src.orientation.x); quat.y(_src.orientation.y); quat.z(_src.orientation.z); quat.r(_src.orientation.w); double roll, pitch, yaw; quat.rpy(roll, pitch, yaw); robotInitialPose.setYawPitchRoll(yaw,pitch,roll); //robotInitialPose.phi(yaw); //Set the initial pose laser_pose = robotInitialPose + laser_pose_on_robot; laser_oldpose = robotInitialPose + laser_pose_on_robot; // Init module (internal) //------------------------ range_wf.setSize(1, width); //Resize vectors according to levels transformations.resize(ctf_levels); for (unsigned int i = 0; i < ctf_levels; i++) transformations[i].resize(3, 3); //Resize pyramid unsigned int s, cols_i; const unsigned int pyr_levels = round(log2(round(float(width) / float(cols)))) + ctf_levels; range.resize(pyr_levels); range_old.resize(pyr_levels); range_inter.resize(pyr_levels); xx.resize(pyr_levels); xx_inter.resize(pyr_levels); xx_old.resize(pyr_levels); yy.resize(pyr_levels); yy_inter.resize(pyr_levels); yy_old.resize(pyr_levels); range_warped.resize(pyr_levels); xx_warped.resize(pyr_levels); yy_warped.resize(pyr_levels); for (unsigned int i = 0; i < pyr_levels; i++) { s = pow(2.f, int(i)); cols_i = ceil(float(width) / float(s)); range[i].resize(1, cols_i); range_inter[i].resize(1, cols_i); range_old[i].resize(1, cols_i); range[i].assign(0.0f); range_old[i].assign(0.0f); xx[i].resize(1, cols_i); xx_inter[i].resize(1, cols_i); xx_old[i].resize(1, cols_i); xx[i].assign(0.0f); xx_old[i].assign(0.0f); yy[i].resize(1, cols_i); yy_inter[i].resize(1, cols_i); yy_old[i].resize(1, cols_i); yy[i].assign(0.f); yy_old[i].assign(0.f); if (cols_i <= cols) { range_warped[i].resize(1, cols_i); xx_warped[i].resize(1, cols_i); yy_warped[i].resize(1, cols_i); } } dt.resize(1, cols); dtita.resize(1, cols); normx.resize(1, cols); normy.resize(1, cols); norm_ang.resize(1, cols); weights.setSize(1, cols); null.setSize(1, cols); null.assign(0); cov_odo.assign(0.f); fps = 1.f; //In Hz num_valid_range = 0; //Compute gaussian mask g_mask[0] = 1.f / 16.f; g_mask[1] = 0.25f; g_mask[2] = 6.f / 16.f; g_mask[3] = g_mask[1]; g_mask[4] = g_mask[0]; kai_abs.assign(0.f); kai_loc_old.assign(0.f); module_initialized = true; last_odom_time = ros::Time::now(); } const mrpt::poses::CPose3D& CLaserOdometry2D::getIncrement() const { return last_increment; } const Eigen::Matrix& CLaserOdometry2D::getIncrementCovariance() const { return cov_odo; } mrpt::poses::CPose3D& CLaserOdometry2D::getPose() { return robot_pose; } const mrpt::poses::CPose3D& CLaserOdometry2D::getPose() const { return robot_pose; } void CLaserOdometry2D::odometryCalculation(const sensor_msgs::LaserScan& scan) { //================================================================================== // DIFERENTIAL ODOMETRY MULTILEVEL //================================================================================== //copy laser scan to internal variable for (unsigned int i = 0; i 3) { solveSystemNonLinear(); //solveSystemOneLevel(); //without robust-function } //8. Filter solution filterLevelSolution(); } m_runtime = 1000*m_clock.Tac(); ROS_INFO_COND(verbose, "[rf2o] execution time (ms): %f", m_runtime); //Update poses PoseUpdate(); } void CLaserOdometry2D::createImagePyramid() { const float max_range_dif = 0.3f; //Push the frames back range_old.swap(range); xx_old.swap(xx); yy_old.swap(yy); //The number of levels of the pyramid does not match the number of levels used //in the odometry computation (because we sometimes want to finish with lower resolutions) unsigned int pyr_levels = round(log2(round(float(width)/float(cols)))) + ctf_levels; //Generate levels for (unsigned int i = 0; i Filter (not downsampling); if (i == 0) { for (unsigned int u = 0; u < cols_i; u++) { const float dcenter = range_wf(u); //Inner pixels if ((u>1)&&(u 0.f) { float sum = 0.f; float weight = 0.f; for (int l=-2; l<3; l++) { const float abs_dif = abs(range_wf(u+l)-dcenter); if (abs_dif < max_range_dif) { const float aux_w = g_mask[2+l]*(max_range_dif - abs_dif); weight += aux_w; sum += aux_w*range_wf(u+l); } } range[i](u) = sum/weight; } else range[i](u) = 0.f; } //Boundary else { if (dcenter > 0.f) { float sum = 0.f; float weight = 0.f; for (int l=-2; l<3; l++) { const int indu = u+l; if ((indu>=0)&&(indu0)&&(u 0.f) { float sum = 0.f; float weight = 0.f; for (int l=-2; l<3; l++) { const float abs_dif = abs(range[i_1](u2+l)-dcenter); if (abs_dif < max_range_dif) { const float aux_w = g_mask[2+l]*(max_range_dif - abs_dif); weight += aux_w; sum += aux_w*range[i_1](u2+l); } } range[i](u) = sum/weight; } else range[i](u) = 0.f; } //Boundary else { if (dcenter > 0.f) { float sum = 0.f; float weight = 0.f; const unsigned int cols_i2 = range[i_1].cols(); for (int l=-2; l<3; l++) { const int indu = u2+l; if ((indu>=0)&&(indu 0.f) { const float tita = -0.5*fovh + float(u)*fovh/float(cols_i-1); xx[i](u) = range[i](u)*cos(tita); yy[i](u) = range[i](u)*sin(tita); } else { xx[i](u) = 0.f; yy[i](u) = 0.f; } } } } void CLaserOdometry2D::calculateCoord() { for (unsigned int u = 0; u < cols_i; u++) { if ((range_old[image_level](u) == 0.f) || (range_warped[image_level](u) == 0.f)) { range_inter[image_level](u) = 0.f; xx_inter[image_level](u) = 0.f; yy_inter[image_level](u) = 0.f; } else { range_inter[image_level](u) = 0.5f*(range_old[image_level](u) + range_warped[image_level](u)); xx_inter[image_level](u) = 0.5f*(xx_old[image_level](u) + xx_warped[image_level](u)); yy_inter[image_level](u) = 0.5f*(yy_old[image_level](u) + yy_warped[image_level](u)); } } } void CLaserOdometry2D::calculaterangeDerivativesSurface() { //The gradient size ir reserved at the maximum size (at the constructor) //Compute connectivity rtita.resize(1,cols_i); //Defined in a different way now, without inversion rtita.assign(1.f); for (unsigned int u = 0; u < cols_i-1; u++) { const float dist = mrpt::math::square(xx_inter[image_level](u+1) - xx_inter[image_level](u)) + mrpt::math::square(yy_inter[image_level](u+1) - yy_inter[image_level](u)); if (dist > 0.f) rtita(u) = sqrt(dist); } //Spatial derivatives for (unsigned int u = 1; u < cols_i-1; u++) dtita(u) = (rtita(u-1)*(range_inter[image_level](u+1)-range_inter[image_level](u)) + rtita(u)*(range_inter[image_level](u) - range_inter[image_level](u-1)))/(rtita(u)+rtita(u-1)); dtita(0) = dtita(1); dtita(cols_i-1) = dtita(cols_i-2); //Temporal derivative for (unsigned int u = 0; u < cols_i; u++) dt(u) = fps*(range_warped[image_level](u) - range_old[image_level](u)); //Apply median filter to the range derivatives //MatrixXf dtitamed = dtita, dtmed = dt; //vector svector(3); //for (unsigned int u=1; u M_PI) norm_ang(u) -= M_PI; normx(u) = cos(tita + alfa); normy(u) = sin(tita + alfa); } } } void CLaserOdometry2D::computeWeights() { //The maximum weight size is reserved at the constructor weights.assign(0.f); //Parameters for error_linearization const float kdtita = 1.f; const float kdt = kdtita/mrpt::math::square(fps); const float k2d = 0.2f; for (unsigned int u = 1; u < cols_i-1; u++) if (null(u) == 0) { // Compute derivatives //----------------------------------------------------------------------- const float ini_dtita = range_old[image_level](u+1) - range_old[image_level](u-1); const float final_dtita = range_warped[image_level](u+1) - range_warped[image_level](u-1); const float dtitat = ini_dtita - final_dtita; const float dtita2 = dtita(u+1) - dtita(u-1); const float w_der = kdt*mrpt::math::square(dt(u)) + kdtita*mrpt::math::square(dtita(u)) + k2d*(abs(dtitat) + abs(dtita2)); weights(u) = sqrt(1.f/w_der); } const float inv_max = 1.f/weights.maximum(); weights = inv_max*weights; } void CLaserOdometry2D::findNullPoints() { //Size of null matrix is set to its maximum size (constructor) num_valid_range = 0; for (unsigned int u = 1; u < cols_i-1; u++) { if (range_inter[image_level](u) == 0.f) null(u) = 1; else { num_valid_range++; null(u) = 0; } } } // Solves the system without considering any robust-function void CLaserOdometry2D::solveSystemOneLevel() { A.resize(num_valid_range,3); B.setSize(num_valid_range,1); unsigned int cont = 0; const float kdtita = (cols_i-1)/fovh; //Fill the matrix A and the vector B //The order of the variables will be (vx, vy, wz) for (unsigned int u = 1; u < cols_i-1; u++) if (null(u) == 0) { // Precomputed expressions const float tw = weights(u); const float tita = -0.5*fovh + u/kdtita; //Fill the matrix A A(cont, 0) = tw*(cos(tita) + dtita(u)*kdtita*sin(tita)/range_inter[image_level](u)); A(cont, 1) = tw*(sin(tita) - dtita(u)*kdtita*cos(tita)/range_inter[image_level](u)); A(cont, 2) = tw*(-yy[image_level](u)*cos(tita) + xx[image_level](u)*sin(tita) - dtita(u)*kdtita); B(cont,0) = tw*(-dt(u)); cont++; } //Solve the linear system of equations using a minimum least squares method Eigen::MatrixXf AtA, AtB; AtA.multiply_AtA(A); AtB.multiply_AtB(A,B); Var = AtA.ldlt().solve(AtB); //Covariance matrix calculation Cov Order -> vx,vy,wz Eigen::MatrixXf res(num_valid_range,1); res = A*Var - B; cov_odo = (1.f/float(num_valid_range-3))*AtA.inverse()*res.squaredNorm(); kai_loc_level = Var; } // Solves the system by considering the Cauchy M-estimator robust-function void CLaserOdometry2D::solveSystemNonLinear() { A.resize(num_valid_range,3); Aw.resize(num_valid_range,3); B.setSize(num_valid_range,1); Bw.setSize(num_valid_range,1); unsigned int cont = 0; const float kdtita = float(cols_i-1)/fovh; //Fill the matrix A and the vector B //The order of the variables will be (vx, vy, wz) for (unsigned int u = 1; u < cols_i-1; u++) if (null(u) == 0) { // Precomputed expressions const float tw = weights(u); const float tita = -0.5*fovh + u/kdtita; //Fill the matrix A A(cont, 0) = tw*(cos(tita) + dtita(u)*kdtita*sin(tita)/range_inter[image_level](u)); A(cont, 1) = tw*(sin(tita) - dtita(u)*kdtita*cos(tita)/range_inter[image_level](u)); A(cont, 2) = tw*(-yy[image_level](u)*cos(tita) + xx[image_level](u)*sin(tita) - dtita(u)*kdtita); B(cont,0) = tw*(-dt(u)); cont++; } //Solve the linear system of equations using a minimum least squares method Eigen::MatrixXf AtA, AtB; AtA.multiply_AtA(A); AtB.multiply_AtB(A,B); Var = AtA.ldlt().solve(AtB); //Covariance matrix calculation Cov Order -> vx,vy,wz Eigen::MatrixXf res(num_valid_range,1); res = A*Var - B; //cout << endl << "max res: " << res.maxCoeff(); //cout << endl << "min res: " << res.minCoeff(); ////Compute the energy //Compute the average dt float aver_dt = 0.f, aver_res = 0.f; unsigned int ind = 0; for (unsigned int u = 1; u < cols_i-1; u++) if (null(u) == 0) { aver_dt += fabsf(dt(u)); aver_res += fabsf(res(ind++)); } aver_dt /= cont; aver_res /= cont; // printf("\n Aver dt = %f, aver res = %f", aver_dt, aver_res); const float k = 10.f/aver_dt; //200 //float energy = 0.f; //for (unsigned int i=0; i 0.f) { //Transform point to the warped reference frame const float x_w = acu_trans(0,0)*xx[image_level](j) + acu_trans(0,1)*yy[image_level](j) + acu_trans(0,2); const float y_w = acu_trans(1,0)*xx[image_level](j) + acu_trans(1,1)*yy[image_level](j) + acu_trans(1,2); const float tita_w = atan2(y_w, x_w); const float range_w = sqrt(x_w*x_w + y_w*y_w); //Calculate warping const float uwarp = kdtita*(tita_w + 0.5*fovh); //The warped pixel (which is not integer in general) contributes to all the surrounding ones if (( uwarp >= 0.f)&&( uwarp < cols_lim)) { const int uwarp_l = uwarp; const int uwarp_r = uwarp_l + 1; const float delta_r = float(uwarp_r) - uwarp; const float delta_l = uwarp - float(uwarp_l); //Very close pixel if (abs(round(uwarp) - uwarp) < 0.05f) { range_warped[image_level](round(uwarp)) += range_w; wacu(round(uwarp)) += 1.f; } else { const float w_r = mrpt::math::square(delta_l); range_warped[image_level](uwarp_r) += w_r*range_w; wacu(uwarp_r) += w_r; const float w_l = mrpt::math::square(delta_r); range_warped[image_level](uwarp_l) += w_l*range_w; wacu(uwarp_l) += w_l; } } } } //Scale the averaged range and compute coordinates for (unsigned int u = 0; u 0.f) { const float tita = -0.5f*fovh + float(u)/kdtita; range_warped[image_level](u) /= wacu(u); xx_warped[image_level](u) = range_warped[image_level](u)*cos(tita); yy_warped[image_level](u) = range_warped[image_level](u)*sin(tita); } else { range_warped[image_level](u) = 0.f; xx_warped[image_level](u) = 0.f; yy_warped[image_level](u) = 0.f; } } } void CLaserOdometry2D::filterLevelSolution() { // Calculate Eigenvalues and Eigenvectors //---------------------------------------------------------- Eigen::SelfAdjointEigenSolver eigensolver(cov_odo); if (eigensolver.info() != Eigen::Success) { ROS_INFO_COND(verbose, "[rf2o] ERROR: Eigensolver couldn't find a solution. Pose is not updated"); return; } //First, we have to describe both the new linear and angular speeds in the "eigenvector" basis //------------------------------------------------------------------------------------------------- Eigen::Matrix Bii; Eigen::Matrix kai_b; Bii = eigensolver.eigenvectors(); kai_b = Bii.colPivHouseholderQr().solve(kai_loc_level); //Second, we have to describe both the old linear and angular speeds in the "eigenvector" basis too //------------------------------------------------------------------------------------------------- mrpt::math::CMatrixFloat31 kai_loc_sub; //Important: we have to substract the solutions from previous levels Eigen::Matrix3f acu_trans; acu_trans.setIdentity(); for (unsigned int i=0; i 1.f) kai_loc_sub(2) = 0.f; else { #if MRPT_VERSION>=0x130 kai_loc_sub(2) = -fps*acos(acu_trans(0,0))*mrpt::utils::sign(acu_trans(1,0)); #else kai_loc_sub(2) = -fps*acos(acu_trans(0,0))*mrpt::math::sign(acu_trans(1,0)); #endif } kai_loc_sub += kai_loc_old; Eigen::Matrix kai_b_old; kai_b_old = Bii.colPivHouseholderQr().solve(kai_loc_sub); //Filter speed const float cf = 15e3f*expf(-int(level)), df = 0.05f*expf(-int(level)); Eigen::Matrix kai_b_fil; for (unsigned int i=0; i<3; i++) { kai_b_fil(i,0) = (kai_b(i,0) + (cf*eigensolver.eigenvalues()(i,0) + df)*kai_b_old(i,0))/(1.f + cf*eigensolver.eigenvalues()(i,0) + df); //kai_b_fil_f(i,0) = (1.f*kai_b(i,0) + 0.f*kai_b_old_f(i,0))/(1.0f + 0.f); } //Transform filtered speed to local reference frame and compute transformation Eigen::Matrix kai_loc_fil = Bii.inverse().colPivHouseholderQr().solve(kai_b_fil); //transformation const float incrx = kai_loc_fil(0)/fps; const float incry = kai_loc_fil(1)/fps; const float rot = kai_loc_fil(2)/fps; transformations[level](0,0) = cos(rot); transformations[level](0,1) = -sin(rot); transformations[level](1,0) = sin(rot); transformations[level](1,1) = cos(rot); transformations[level](0,2) = incrx; transformations[level](1,2) = incry; } void CLaserOdometry2D::PoseUpdate() { //First, compute the overall transformation //--------------------------------------------------- Eigen::Matrix3f acu_trans; acu_trans.setIdentity(); for (unsigned int i=1; i<=ctf_levels; i++) acu_trans = transformations[i-1]*acu_trans; // Compute kai_loc and kai_abs //-------------------------------------------------------- kai_loc(0) = fps*acu_trans(0,2); kai_loc(1) = fps*acu_trans(1,2); if (acu_trans(0,0) > 1.f) kai_loc(2) = 0.f; else { #if MRPT_VERSION>=0x130 kai_loc(2) = fps*acos(acu_trans(0,0))*mrpt::utils::sign(acu_trans(1,0)); #else kai_loc(2) = fps*acos(acu_trans(0,0))*mrpt::math::sign(acu_trans(1,0)); #endif } //cout << endl << "Arc cos (incr tita): " << kai_loc(2); float phi = laser_pose.yaw(); kai_abs(0) = kai_loc(0)*cos(phi) - kai_loc(1)*sin(phi); kai_abs(1) = kai_loc(0)*sin(phi) + kai_loc(1)*cos(phi); kai_abs(2) = kai_loc(2); // Update poses //------------------------------------------------------- laser_oldpose = laser_pose; mrpt::math::CMatrixDouble33 aux_acu = acu_trans; mrpt::poses::CPose2D pose_aux_2D(acu_trans(0,2), acu_trans(1,2), kai_loc(2)/fps); laser_pose = laser_pose + mrpt::poses::CPose3D(pose_aux_2D); last_increment = pose_aux_2D; // Compute kai_loc_old //------------------------------------------------------- phi = laser_pose.yaw(); kai_loc_old(0) = kai_abs(0)*cos(phi) + kai_abs(1)*sin(phi); kai_loc_old(1) = -kai_abs(0)*sin(phi) + kai_abs(1)*cos(phi); kai_loc_old(2) = kai_abs(2); ROS_INFO_COND(verbose, "[rf2o] LASERodom = [%f %f %f]",laser_pose.x(),laser_pose.y(),laser_pose.yaw()); //Compose Transformations robot_pose = laser_pose + laser_pose_on_robot_inv; ROS_INFO_COND(verbose, "BASEodom = [%f %f %f]",robot_pose.x(),robot_pose.y(),robot_pose.yaw()); // Estimate linear/angular speeds (mandatory for base_local_planner) // last_scan -> the last scan received // last_odom_time -> The time of the previous scan lasser used to estimate the pose //------------------------------------------------------------------------------------- double time_inc_sec = (current_scan_time - last_odom_time).toSec(); last_odom_time = current_scan_time; lin_speed = acu_trans(0,2) / time_inc_sec; //double lin_speed = sqrt( mrpt::math::square(robot_oldpose.x()-robot_pose.x()) + mrpt::math::square(robot_oldpose.y()-robot_pose.y()) )/time_inc_sec; double ang_inc = robot_pose.yaw() - robot_oldpose.yaw(); if (ang_inc > 3.14159) ang_inc -= 2*3.14159; if (ang_inc < -3.14159) ang_inc += 2*3.14159; ang_speed = ang_inc/time_inc_sec; robot_oldpose = robot_pose; //filter speeds /* last_m_lin_speeds.push_back(lin_speed); if (last_m_lin_speeds.size()>4) last_m_lin_speeds.erase(last_m_lin_speeds.begin()); double sum = std::accumulate(last_m_lin_speeds.begin(), last_m_lin_speeds.end(), 0.0); lin_speed = sum / last_m_lin_speeds.size(); last_m_ang_speeds.push_back(ang_speed); if (last_m_ang_speeds.size()>4) last_m_ang_speeds.erase(last_m_ang_speeds.begin()); double sum2 = std::accumulate(last_m_ang_speeds.begin(), last_m_ang_speeds.end(), 0.0); ang_speed = sum2 / last_m_ang_speeds.size(); */ }