plane fitting method

3 minute read

Published:

plane-fiitting

平面方程: ax + by + cz + d = 0,其中系数a, b, c为平面的法向量,最少需要三个坐标点我们就可以确定方程的系数,可以构建如下的矩阵形式:

                    Ax = 0 其中x = [a, b, c, d],A为n个坐标点(xi, yi, zi)构成的矩阵, 1为增广的一列,使用svd矩阵分解可以很容易的求解待求的参数向量,如下所示:
Eigen::Vector4d GetPlaneCofficient(const Eigen::Vector3d& point1,
                                   const Eigen::Vector3d& point2,
                                   const Eigen::Vector3d& point3) {
  Eigen::Matrix<double, 3, 4> A;
  A << point1.x(), point1.y(), point1.z(), 1,
       point2.x(), point2.y(), point2.z(), 1,
       point3.x(), point3.y(), point3.z(), 1;
  const Eigen::JacobiSVD<Eigen::Matrix<double, 3, 4>> init_svd
      (A,Eigen::ComputeFullU | Eigen::ComputeFullV);
  Eigen::Vector4d v = init_svd.matrixV().col(3);
  return v;
}

当我们有大量的平面点可以作为观测使用的时候,可以先用ransac机制筛选inlier点,在利用所有的inlier点进行平面参数的优化求解,如下所示:

void FindViaRansac(std::vector<Eigen::Vector3d>& road_points,
                   Eigen::Vector4d& plane_cofficient) {
  int best_inlier_count = 0;
  std::vector<int> best_inlier_indexs;
  Eigen::Vector4d best_plane_coff;
  for(int i = 0; i < max_iter_times; i++) {
    std::vector<int> random_index =
        create_random_array(3, 0, static_cast<int>(road_points.size() - 1));
    auto point1 = road_points[random_index[0]];
    auto point2 = road_points[random_index[1]];
    auto point3 = road_points[random_index[2]];
    Eigen::Vector4d plane_coff = GetPlaneCofficient(point1, point2, point3);
    int inlier_count = 0;
    std::vector<int> inlier_indexs;
    for(int j = 0; j < road_points.size(); j++) {
      if(PointToPlaneDis(road_points[j], plane_coff) < thres_dis_point_plane) {
        inlier_count++;
        inlier_indexs.push_back(j);
      }
    }
    if(inlier_count > best_inlier_count) {
      best_inlier_count = inlier_count;
      best_inlier_indexs = inlier_indexs;
      best_plane_coff = plane_coff;
    }
  }

  std::vector<Eigen::Vector3d> inlier_points(best_inlier_count);
  for(int i = 0; i < best_inlier_indexs.size(); i++) {
    inlier_points[i] = (road_points[best_inlier_indexs[i]]);
  }
}

通过随机采样,获得inlier最多情况下的平面参数,利用这组参数作为初值,利用g2o/ceres等优化器进行参数估计,inlier的设定为点到平面的距离应该小于一定的阈值

如下g2o/ceres中vertex, edge以及residual的定义:

#pragma once

#include "g2o/core/base_unary_edge.h"
#include "g2o/core/base_vertex.h"

#include <ceres/autodiff_cost_function.h>
#include <ceres/ceres.h>
#include <ceres/loss_function.h>

#include <Eigen/Core>

// Eigen::Vector4d is A, B, C, D
class VeretxPlane : public g2o::BaseVertex<4, Eigen::Vector4d> {
public:
  VeretxPlane() : g2o::BaseVertex<4, Eigen::Vector4d>() {}

  bool read(std::istream &is) { return false; };
  bool write(std::ostream &os) const { return false; };

  virtual void setToOriginImpl() {
    _estimate.setZero();
  }

  virtual void oplusImpl(const double *update_) {
    Eigen::Map<const Eigen::Vector4d> update(update_);
    setEstimate(update + estimate());
  }
};

class EdgePlanePoint : public g2o::BaseUnaryEdge<1, double, VeretxPlane> {
public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW;

  EdgePlanePoint() = delete;

  EdgePlanePoint(const Eigen::Vector3d& plane_point) {
    plane_point_ = plane_point;
  }

  virtual void computeError() {
    const VeretxPlane* v = static_cast<VeretxPlane*>(_vertices[0]);
    Eigen::Vector4d plane_cofficients = v->estimate();
    _error(0, 0) = plane_cofficients[0] * plane_point_.x() +
                 plane_cofficients[1] * plane_point_.y() +
                 plane_cofficients[2] * plane_point_.z() +
                 plane_cofficients[3];
  }
  virtual void linearizeOplus() {
    Eigen::Matrix<double, 1, 4> jaco;
    jaco << plane_point_.x(), plane_point_.y(), plane_point_.z(), 1;
    _jacobianOplusXi = jaco;
  }

  // dummy read and write functions because we don't care...
  virtual bool read(std::istream &in) { return false; }
  virtual bool write(std::ostream &out) const { return false; }

public:
  Eigen::Vector3d plane_point_;
};

class PlanePointResidual {
public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
  PlanePointResidual(Eigen::Vector3d plane_point) {
    plane_point_ = plane_point;
  }

  template <typename T>
  bool operator()(const T* a, const T* b, const T* c,
                  const T* d, T* residual) const {
    residual[0] = *a * T(plane_point_.x()) +
                  *b * T(plane_point_.y()) +
                  *c * T(plane_point_.z()) +
                  *d;
    return true;
  }

  static ceres::CostFunction* Create(Eigen::Vector3d plane_point) {
    return new ceres::AutoDiffCostFunction<PlanePointResidual, 1, 1, 1, 1, 1>(
          new PlanePointResidual(plane_point));
  }

  Eigen::Vector3d plane_point_;
};

最后利用优化器进行参数求解即可:

Eigen::Vector4d GetPlaneCofficientByCeres(
    const std::vector<Eigen::Vector3d>& road_points,
    Eigen::Vector4d init_plane_coff) {
  double a = init_plane_coff[0];
  double b = init_plane_coff[1];
  double c = init_plane_coff[2];
  double d = init_plane_coff[3];

  ceres::Problem problem;
  for(int i = 0; i < road_points.size(); i++) {
    ceres::CostFunction* cost_function =
      PlanePointResidual::Create(road_points[i]);
    problem.AddResidualBlock(cost_function, nullptr, &a, &b, &c, &d);
  }
  // Run
  ceres::Solver::Options options;
  options.max_num_iterations = 10;
  options.linear_solver_type = ceres::DENSE_QR;
  //options.logging_type = ceres::LoggingType::SILENT;
  options.minimizer_type = ceres::TRUST_REGION;
  options.trust_region_strategy_type = ceres::LEVENBERG_MARQUARDT;
  ceres::Solver::Summary summary;
  ceres::Solve(options, &problem, &summary);

  Eigen::Vector4d result(a, b, c, d);
  result /= result[3];
  return result;
}