ceres学习笔记(三)

news2024/11/16 22:48:03

学习了example中pose_graph_3d的部分,记录一下学习过程。

前言:

翻译一下readme里面的内容:

...

该示例还说明了如何将 Eigen 的几何模块与 Ceres 的自动微分功能结合使用。 为了表示方向,我们将使用 Eigen 的四元数,它使用 Hamiltonian 约定,但与 Ceres 的旋转表示相比具有不同的元素顺序。 具体来说,它们的区别在于标量分量 q_w 是第一个还是最后一个;  Ceres 的四元数的元素顺序是 [q_w, q_x, q_y, q_z],而 Eigen 的四元数是 [q_x, q_y, q_z, q_w]。

该包定义了对 3 维位姿图优化问题进行建模所需的必要 Ceres 成本函数,以及构建和解决问题的二进制文件。 显示成本函数是为了指导目的,并且可以通过使用需要更长的时间来实现的分析导数来加速。

运行 ---------- 此包包含一个可执行文件 `pose_graph_3d`,它将读取问题定义文件。 此可执行文件可以处理任何使用 g2o 格式的 3D 问题定义,四元数用于方向表示。 为不同格式(如 TORO 或其他格式)实施新阅读器会相对简单。  `pose_graph_3d` 将打印 Ceres 求解器的完整摘要,然后将机器人的原始和优化姿势(分别为 `poses_original.txt` 和 `poses_optimized.txt`)输出到磁盘,格式如下:

pose_id x y z q_x q_y q_z q_w

pose_id x y z q_x q_y q_z q_w

pose_id x y z q_x q_y q_z q_w

其中 `pose_id` 是文件定义中对应的整数 ID。 请注意,文件将按 `pose_id` 的升序排序。
...

主要是对于ceres用于位姿优化的一个说明,对于ceres和eigen关于四元数定义还有costfunction的设置。然后对于g2o文件内容的说明。example里面没有找到g2o文件的data,最后在github里面翻到了,链接如下:

Datasets – Luca Carlone

一、pose_graph_3d_error_term.h部分的解析

pose_graph_3d将代价函数类的定义单独写了一个头文件pose_graph_3d_error_term.h。先对它进行学习。

翻译注释:

计算两个姿势的误差项,这两个姿势之间具有相对姿势测量值。 带hat的变量是测量值。 我们有两个位姿 x_a 和 x_b。 通过传感器测量,我们可以测量帧 B w.r.t 帧 A 的变换,表示为 t_ab_hat。 我们可以计算位姿的当前估计和测量之间的误差度量。
在这个公式中,我们选择将刚性变换表示为哈密顿四元数 q 和位置 p。 四元数顺序为 [x, y, z,w]。

估计的测量值是:t_ab = [ p_ab ] = [ R(q_a)^T * (p_b - p_a) ]

                                                                      [ q_ab ]

                                                                      [ q_a^{-1}* q_b ]

其中 ^{-1} 表示逆矩阵,R(q) 是四元数的旋转矩阵。 现在我们可以计算估计和测量转换之间的误差度量。 对于方向误差,我们将使用标准乘法误差,结果为:

error = [ p_ab - \hat{p}_ab ]

               [ 2.0 * Vec(q_ab * \hat{q}_ab^{-1}) ]

其中 Vec(*) 返回四元数的向量(虚部)部分。 由于测量具有与其准确性相关的不确定性,我们将通过测量信息矩阵的平方根对误差进行加权:

residuals = I^{1/2) * error 其中 I 是信息矩阵,它是协方差的倒数。

下面对PoseGraph3dErrorTerm类进行解析。

 private:
  // The measurement for the position of B relative to A in the A frame.
  const Pose3d t_ab_measured_;
  // The square root of the measurement information matrix.
  const Eigen::Matrix<double, 6, 6> sqrt_information_;

有两个私有变量,

t_ab_measured_表示在A下,B相对于A的位姿。这里要看一下Pose3d的定义:

struct Pose3d {
  Eigen::Vector3d p;
  Eigen::Quaterniond q;

  // The name of the data type in the g2o file format.
  static std::string name() { return "VERTEX_SE3:QUAT"; }

  EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};

它包括一个数据类型是Eigen::Vector3d的p表示位置,一个数据类型是Eigen::Quaterniond的q表示旋转。然后还定义了一个name函数返回VERTEX_SE3:QUAT,是se3下的四元数。

sqrt_information_表示测量信息矩阵的平方根,它的数据类型是double,size是6*6。

然后是构造函数:

  PoseGraph3dErrorTerm(const Pose3d& t_ab_measured,
                       const Eigen::Matrix<double, 6, 6>& sqrt_information)
      : t_ab_measured_(t_ab_measured), sqrt_information_(sqrt_information) {}

它将传入的两个参数分别赋值给两个私有变量。

接下来是关键的重载运算符operator():

  template <typename T>
  bool operator()(const T* const p_a_ptr,
                  const T* const q_a_ptr,
                  const T* const p_b_ptr,
                  const T* const q_b_ptr,
                  T* residuals_ptr) const

传入了5个参数,注意这里全部是相对于世界坐标系下的

p_a_ptr表示a点的位置;

q_a_ptr表示a点的旋转用四元数来表示;

p_b_ptr表示b点的位置;

q_b_ptr表示b点的旋转;

residuals_ptr是最后算的残差。

    Eigen::Map<const Eigen::Matrix<T, 3, 1>> p_a(p_a_ptr);
    Eigen::Map<const Eigen::Quaternion<T>> q_a(q_a_ptr);

    Eigen::Map<const Eigen::Matrix<T, 3, 1>> p_b(p_b_ptr);
    Eigen::Map<const Eigen::Quaternion<T>> q_b(q_b_ptr);

这里使用了eigen里面的映射函数主要是为了数据转换,将p_a_ptr等分别赋值给p_a等,方便之后的运算。

    // Compute the relative transformation between the two frames.
    Eigen::Quaternion<T> q_a_inverse = q_a.conjugate();
    Eigen::Quaternion<T> q_ab_estimated = q_a_inverse * q_b;

计算a和b之间的位姿变换关系,q_a_inverse表示q_a的逆,使用conjugate函数来求。最后求出了q_ab_estimated表示从b变换到a的旋转矩阵。这里就相当于q_b是相对于世界坐标系下的旋转,q_a是相当于世界坐标系下的旋转。然后q_a_inverse * q_b就算出了b在a坐标系下的旋转。

公式推导如上图所示。

    // Represent the displacement between the two frames in the A frame.
    Eigen::Matrix<T, 3, 1> p_ab_estimated = q_a_inverse * (p_b - p_a);

这是求出了从b到a的平移向量p_ab_estimated。同理p_b表示世界坐标系下b的位置,p_a表示世界坐标系下a的位置,然后p_ab_estimated就应该表示b点在a坐标系下平移向量。公式推导如下:

比较清晰明确。

    // Compute the error between the two orientation estimates.
    Eigen::Quaternion<T> delta_q =
        t_ab_measured_.q.template cast<T>() * q_ab_estimated.conjugate();

这部分是计算从b到a的旋转部分的测量值和估计值之间的差,这里运用了四元数的差来求旋转的角度差,这部分没有找到什么资料,在一篇博客里面看到了四元数的差,只给出了公式,但也没有给出推导的过程。

四元数的差、对数、指数、幂以及差值_Magic_Conch_Shell的博客-CSDN博客_四元数的差

    // Compute the residuals.
    // [ position         ]   [ delta_p          ]
    // [ orientation (3x1)] = [ 2 * delta_q(0:2) ]
    Eigen::Map<Eigen::Matrix<T, 6, 1>> residuals(residuals_ptr);
    residuals.template block<3, 1>(0, 0) =
        p_ab_estimated - t_ab_measured_.p.template cast<T>();
    residuals.template block<3, 1>(3, 0) = T(2.0) * delta_q.vec();

这部分就是给6*1的残差向量赋值,前三行是位移部分的差值,后三行是旋转部分的差值,vec是取四元数的虚部,这里不知道为什么乘了2。

    // Scale the residuals by the measurement uncertainty.
    residuals.applyOnTheLeft(sqrt_information_.template cast<T>());

这里是加上了信息矩阵。

  static ceres::CostFunction* Create(
      const Pose3d& t_ab_measured,
      const Eigen::Matrix<double, 6, 6>& sqrt_information) {
    return new ceres::AutoDiffCostFunction<PoseGraph3dErrorTerm, 6, 3, 4, 3, 4>(
        new PoseGraph3dErrorTerm(t_ab_measured, sqrt_information));
  }

这里是创建自动求导的代价函数,是因为四元数不能使用常规方法来求导,就使用了这个。其中

new ceres::AutoDiffCostFunction<PoseGraph3dErrorTerm, 6, 3, 4, 3, 4>

第一个6表示residuals是6维的;

第二个3表示operator()函数哪里的p_a_ptr是3维的,后面的类似。

二、common/read_g2o.h部分

这是一个处理g2o文件的头文件,下面来简单解析一下。

结合一下g20文件的内容来看:

// Reads a single pose from the input and inserts it into the map. Returns false
// if there is a duplicate entry.
template <typename Pose, typename Allocator>
bool ReadVertex(std::ifstream* infile,
                std::map<int, Pose, std::less<int>, Allocator>* poses) {
  int id;
  Pose pose;
  *infile >> id >> pose;

  // Ensure we don't have duplicate poses.
  if (poses->find(id) != poses->end()) {
    LOG(ERROR) << "Duplicate vertex with ID: " << id;
    return false;
  }
  (*poses)[id] = pose;

  return true;
}

阅读顶点部分,通过传入的文件,将分别传入id和pose数据结构里面。这里有一个if判断,判断的内容是查看poses是否有id,这里的处理很巧妙,它希望的是判断是否有重复的位姿,这里的poses应该还没有存入id和pose,存入操作在下面,find()stl函数所以如果发现有相同的就返回iterator值,没有就会返回end()后面的iterator值,所以判断是否等于,如果不等于说明poses里面已经有key为id的数据,说明重复输入了。然后有一个map数据格式的变量poses,将id和pose赋予它。

// Reads the contraints between two vertices in the pose graph
template <typename Constraint, typename Allocator>
void ReadConstraint(std::ifstream* infile,
                    std::vector<Constraint, Allocator>* constraints) {
  Constraint constraint;
  *infile >> constraint;

  constraints->push_back(constraint);
}

阅读两点之间限制的边部分,这里是将g2o中的两点之间限制的边部分传入,具体得看main里面怎么写。

// In 3D, a vertex is defined as follows:
//
// VERTEX_SE3:QUAT ID x y z q_x q_y q_z q_w
//
// where the quaternion is in Hamilton form.
// A constraint is defined as follows:
//
// EDGE_SE3:QUAT ID_a ID_b x_ab y_ab z_ab q_x_ab q_y_ab q_z_ab q_w_ab I_11 I_12 I_13 ... I_16 I_22 I_23 ... I_26 ... I_66 // NOLINT
//
// where I_ij is the (i, j)-th entry of the information matrix for the
// measurement. Only the upper-triangular part is stored. The measurement order
// is the delta position followed by the delta orientation.

这部分注释主要的意思是说明g2o不同行的格式。

开头为VERTEX_SE3:QUAT的表示第一个参数是序号,后面3个是在世界坐标系下的位移值,最后四个是四元数。

开头为EDGE_SE3:QUAT的表示由b到a坐标系下的相关值,

第一个值和第二个值表示从第二个到第一个的变换;

第三个值到第五个值表示从b到a坐标系下位移值的变换;

第六个值到第十个值表示从b到a坐标系下旋转的变换,以四元数表示;

后面的就是信息矩阵的上半部分,因为它是对称的所以存了一半。

template <typename Pose,
          typename Constraint,
          typename MapAllocator,
          typename VectorAllocator>
bool ReadG2oFile(const std::string& filename,
                 std::map<int, Pose, std::less<int>, MapAllocator>* poses,
                 std::vector<Constraint, VectorAllocator>* constraints) 

这是阅读g2o文件,

第一个参数是阅读的文件名;

第二个参数是关于顶点的poses;

第三个参数是关于两点之间的限制,边constraints。

  poses->clear();
  constraints->clear();

  std::ifstream infile(filename.c_str());
  if (!infile) {
    return false;
  }

清空加判断是否有文件输入。

  std::string data_type;
  while (infile.good()) {
    // Read whether the type is a node or a constraint.
    infile >> data_type;
    if (data_type == Pose::name()) {
      if (!ReadVertex(&infile, poses)) {
        return false;
      }
    } else if (data_type == Constraint::name()) {
      ReadConstraint(&infile, constraints);
    } else {
      LOG(ERROR) << "Unknown data type: " << data_type;
      return false;
    }

    // Clear any trailing whitespace from the line.
    infile >> std::ws;
  }

定义string类型data_type,它用来判断读入的是VERTEX_SE3:QUAT还是EDGE_SE3:QUAT来执行ReadVertex函数或者ReadConstraint函数。很妙的设计!

三、type.h部分

这部分主要是对于一些数据结构的定义优化和对于g2o文件读取的设置。

struct Pose3d {
  Eigen::Vector3d p;
  Eigen::Quaterniond q;

  // The name of the data type in the g2o file format.
  static std::string name() { return "VERTEX_SE3:QUAT"; }

  EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};

定义Pose3d的结构,包含一个vector3d的p和四元数q,并且name函数返回VERTEX_SE3:QUAT,正好是g2o文件中顶点开头的字符串。

inline std::istream& operator>>(std::istream& input, Pose3d& pose) {
  input >> pose.p.x() >> pose.p.y() >> pose.p.z() >> pose.q.x() >> pose.q.y() >>
      pose.q.z() >> pose.q.w();
  // Normalize the quaternion to account for precision loss due to
  // serialization.
  pose.q.normalize();
  return input;
}

重写了>>运算符,将左侧数据写入pose里面,并将q正规化。

typedef std::map<int,
                 Pose3d,
                 std::less<int>,
                 Eigen::aligned_allocator<std::pair<const int, Pose3d>>>
    MapOfPoses;

定义了这么一个别名。

// The constraint between two vertices in the pose graph. The constraint is the
// transformation from vertex id_begin to vertex id_end.
struct Constraint3d {
  int id_begin;
  int id_end;

  // The transformation that represents the pose of the end frame E w.r.t. the
  // begin frame B. In other words, it transforms a vector in the E frame to
  // the B frame.
  Pose3d t_be;

  // The inverse of the covariance matrix for the measurement. The order of the
  // entries are x, y, z, delta orientation.
  Eigen::Matrix<double, 6, 6> information;

  // The name of the data type in the g2o file format.
  static std::string name() { return "EDGE_SE3:QUAT"; }

  EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};

定义了Constraint3d的结构,有id_begin和id_end分别表示序号。

t_be表示由end到begin的变换旋转加平移;

information是6*6的信息矩阵;

name函数返回EDGE_SE3:QUAT字符串。

inline std::istream& operator>>(std::istream& input, Constraint3d& constraint) {
  Pose3d& t_be = constraint.t_be;
  input >> constraint.id_begin >> constraint.id_end >> t_be;

  for (int i = 0; i < 6 && input.good(); ++i) {
    for (int j = i; j < 6 && input.good(); ++j) {
      input >> constraint.information(i, j);
      if (i != j) {
        constraint.information(j, i) = constraint.information(i, j);
      }
    }
  }
  return input;
}

重写了>>运算符,并把相对应的量分别输入,注意对于信息矩阵的赋值,因为g2o文件的信息矩阵只存了上三角,所要要这样写。

typedef std::vector<Constraint3d, Eigen::aligned_allocator<Constraint3d>>
    VectorOfConstraints;

另一个别名。

四、pose_graph_3d.cc部分

从main开始看起:

  ceres::examples::MapOfPoses poses;
  ceres::examples::VectorOfConstraints constraints;

前面部分都是一些读参,这两条是运用了上面type.h里面的两个别名。

poses是一个map,key为int类型,value是Pose3d类型的变量;

constraints是一个vector类型,存储为Constraint3d类型的变量。

  CHECK(ceres::examples::ReadG2oFile(FLAGS_input, &poses, &constraints))
      << "Error reading the file: " << FLAGS_input;

  std::cout << "Number of poses: " << poses.size() << '\n';
  std::cout << "Number of constraints: " << constraints.size() << '\n';

  CHECK(ceres::examples::OutputPoses("poses_original.txt", poses))
      << "Error outputting to poses_original.txt";

进入ReadG2oFile函数读取g2o文件,并输出一些原始数据。

  ceres::Problem problem;
  ceres::examples::BuildOptimizationProblem(constraints, &poses, &problem);

设置problem对象,并开始优化的相关操作。

具体看BuildOptimizationProblem函数。

// Constructs the nonlinear least squares optimization problem from the pose
// graph constraints.
void BuildOptimizationProblem(const VectorOfConstraints& constraints,
                              MapOfPoses* poses,
                              ceres::Problem* problem) 

注释的意思是构造通过图限制来构造最小二乘的优化问题。BuildOptimizationProblem函数的第一个参数是constraints表示两点之间的约束的数据结构;

第二个是poses,代表顶点的数据;

第三个就是ceres的problem。

  CHECK(poses != NULL);
  CHECK(problem != NULL);
  if (constraints.empty()) {
    LOG(INFO) << "No constraints, no problem to optimize.";
    return;
  }

判断参数是否为空。

  ceres::LossFunction* loss_function = NULL;
  ceres::LocalParameterization* quaternion_local_parameterization =
      new EigenQuaternionParameterization;

设置loss_function为空指针,说明没有设置核函数;

设置了ceres关于四元数相关计算变量。

  for (VectorOfConstraints::const_iterator constraints_iter =
           constraints.begin();
       constraints_iter != constraints.end();
       ++constraints_iter) {
    const Constraint3d& constraint = *constraints_iter;

    MapOfPoses::iterator pose_begin_iter = poses->find(constraint.id_begin);
    CHECK(pose_begin_iter != poses->end())
        << "Pose with ID: " << constraint.id_begin << " not found.";
    MapOfPoses::iterator pose_end_iter = poses->find(constraint.id_end);
    CHECK(pose_end_iter != poses->end())
        << "Pose with ID: " << constraint.id_end << " not found.";

    const Eigen::Matrix<double, 6, 6> sqrt_information =
        constraint.information.llt().matrixL();
    // Ceres will take ownership of the pointer.
    ceres::CostFunction* cost_function =
        PoseGraph3dErrorTerm::Create(constraint.t_be, sqrt_information);

    problem->AddResidualBlock(cost_function,
                              loss_function,
                              pose_begin_iter->second.p.data(),
                              pose_begin_iter->second.q.coeffs().data(),
                              pose_end_iter->second.p.data(),
                              pose_end_iter->second.q.coeffs().data());

    problem->SetParameterization(pose_begin_iter->second.q.coeffs().data(),
                                 quaternion_local_parameterization);
    problem->SetParameterization(pose_end_iter->second.q.coeffs().data(),
                                 quaternion_local_parameterization);
  }

在这个for循环中完成对于边限制数据的读取计算。下面进行详细的分析:

  for (VectorOfConstraints::const_iterator constraints_iter =
           constraints.begin();
       constraints_iter != constraints.end();
       ++constraints_iter)

因为是vector容器,所以可以使用迭代器。

    const Constraint3d& constraint = *constraints_iter;

    MapOfPoses::iterator pose_begin_iter = poses->find(constraint.id_begin);
    CHECK(pose_begin_iter != poses->end())
        << "Pose with ID: " << constraint.id_begin << " not found.";
    MapOfPoses::iterator pose_end_iter = poses->find(constraint.id_end);
    CHECK(pose_end_iter != poses->end())
        << "Pose with ID: " << constraint.id_end << " not found.";

取出begin_id和end_id,就是数据的前两个数值,分别赋值给pose_begin_iter和pose_end_iter。注意它是用find函数在poses里面取。

    const Eigen::Matrix<double, 6, 6> sqrt_information =
        constraint.information.llt().matrixL();

将信息矩阵进行llt分解并赋值给sqrt_information矩阵,Eigen的LLT分解实现了Cholesky 分解。

Eigen的LLT分解_火星机器人life的博客-CSDN博客_eigen llt

    // Ceres will take ownership of the pointer.
    ceres::CostFunction* cost_function =
        PoseGraph3dErrorTerm::Create(constraint.t_be, sqrt_information);

通过Create构造函数生成cost_function。

    problem->AddResidualBlock(cost_function,
                              loss_function,
                              pose_begin_iter->second.p.data(),
                              pose_begin_iter->second.q.coeffs().data(),
                              pose_end_iter->second.p.data(),
                              pose_end_iter->second.q.coeffs().data());

这里是proble对象的AddResidualBlock模块,

第一个参数cost_function是设定的代价函数;

第二个参数loss_function是核函数,没有定义;

第三个参数pose_begin_iter->second.p.data()是第一个点的位置变量;

第四个参数pose_begin_iter->second.q.coeffs().data()是第一个点的旋转变量;coeffs()是xyzw的顺序;

后面两个参数类似。

    problem->SetParameterization(pose_begin_iter->second.q.coeffs().data(),
                                 quaternion_local_parameterization);
    problem->SetParameterization(pose_end_iter->second.q.coeffs().data(),
                                 quaternion_local_parameterization);

这里的意思是说,四元数不符合常规运算的,eigen专门为它配置了运算,需要在problem配置时加入。

位姿图优化问题有六个未完全约束的自由度。 这通常称为规范自由度。 
您可以对所有节点应用刚体变换,优化问题仍将具有完全相同的成本。  
Levenberg-Marquardt 算法具有可缓解此问题的内部阻尼,但最好适当地约束规范自由度。 
这可以通过将其中一个姿势设置为常量来实现,这样优化器就无法更改它。

大概意思是第一个点固定?

  MapOfPoses::iterator pose_start_iter = poses->begin();
  CHECK(pose_start_iter != poses->end()) << "There are no poses.";
  problem->SetParameterBlockConstant(pose_start_iter->second.p.data());
  problem->SetParameterBlockConstant(pose_start_iter->second.q.coeffs().data());

取出poses的第一个赋值给迭代器pose_start_iter,然后将它的p和q都在problem中设置成恒定值,就是上面那段翻译的意思。

再回到main里面来。

  CHECK(ceres::examples::SolveOptimizationProblem(&problem))
      << "The solve was not successful, exiting.";

开始优化,就是ceres求导的solve环节。

// Returns true if the solve was successful.
bool SolveOptimizationProblem(ceres::Problem* problem) {
  CHECK(problem != NULL);

  ceres::Solver::Options options;
  options.max_num_iterations = 200;
  options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY;

  ceres::Solver::Summary summary;
  ceres::Solve(options, problem, &summary);

  std::cout << summary.FullReport() << '\n';

  return summary.IsSolutionUsable();
}

设置solver的options,最大迭代次数设置为200次;

采用ceres::SPARSE_NORMAL_CHOLESKY稀疏且可夫斯基模式求解?

然后开始solve,并输入summary的信息,最后返回是否成功标识。

  CHECK(ceres::examples::OutputPoses("poses_optimized.txt", poses))
      << "Error outputting to poses_original.txt";

最后将优化后的位姿输出到poses_optimized.txt文件中去。

结束,终于算是理解的比较透彻的了。

五、最终运行

在经过我一系列的修改后,终于成功的跑起来了。

以下是输出结果:

Number of poses: 1661
Number of constraints: 6275

Solver Summary (v 2.0.0-eigen-(3.4.0)-lapack-suitesparse-(5.1.2)-cxsparse-(3.1.9)-eigensparse-no_openmp)

                                     Original                  Reduced
Parameter blocks                         3322                     3320
Parameters                              11627                    11620
Effective parameters                     9966                     9960
Residual blocks                          6275                     6275
Residuals                               37650                    37650

Minimizer                        TRUST_REGION

Sparse linear algebra library    SUITE_SPARSE
Trust region strategy     LEVENBERG_MARQUARDT

                                        Given                     Used
Linear solver          SPARSE_NORMAL_CHOLESKY   SPARSE_NORMAL_CHOLESKY
Threads                                     1                        1
Linear solver ordering              AUTOMATIC                     3320

Cost:
Initial                          8.362723e+03
Final                            6.341883e-01
Change                           8.362088e+03

Minimizer iterations                       20
Successful steps                           20
Unsuccessful steps                          0

Time (in seconds):
Preprocessor                         0.004724

  Residual only evaluation           0.597028 (20)
  Jacobian & residual evaluation    18.880399 (20)
  Linear solver                      0.425803 (20)
Minimizer                           19.939410

Postprocessor                        0.000370
Total                               19.944509

Termination:                      CONVERGENCE (Function tolerance reached. |cost_change|/cost: 6.104042e-07 <= 1.000000e-06)

看一下生成的两个txt文件:

 使用python可视化显示:

 好像效果不明显,换个sphere看看。

 这就明显多了。CMakelists.txt内容比较简单,就不写了。

时间2023年1月17日23点06分,到这个时间为止,没有发现有比我对这个源码注释的最详细的。hhh,啦啦啦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/170670.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

测试开发 | Pytest 结合 Allure 生成测试报告

本文节选自霍格沃玆测试学院测试开发内部教材&#xff0c;进阶学习文末加群&#xff01; 测试报告在项目中是至关重要的角色&#xff0c;一个好的测试报告&#xff1a; 可以体现测试人员的工作量&#xff1b; 开发人员可以从测试报告中了解缺陷的情况&#xff1b; 测试经理可…

锂电产业如何利用视觉检测系统降本增效?

导语&#xff1a;机器视觉检测已在锂电池生产的各个环节中&#xff0c;为产品产量与质量提供可靠保障。维视智造作为锂电池视觉检测系统提供商&#xff0c;为企业提供专业、系统、稳定的锂电行业解决方案&#xff0c;可保证0漏检&#xff0c;确保安全生产&#xff0c;全面提升生…

炫酷 RGB 之.NET nanoFramework 点灯大师

前面介绍了 .NET nanoFramework 入门&#xff0c;本文继续以微雪的 ESP32-S2-Pico 为例介绍 .NET nanoFramework 的开发&#xff1a;控制 ESP32 板载 RGB 灯 和 外接 RGB 灯。内容包含 状态灯的意义、WS2812 、HSV、PWM 等相关知识。 文章目录1. 背景2. 状态灯的意义3. 板载 LE…

萌新如何使用printf函数?

&#x1f40e;作者的话 如果你搜索输入输出函数&#xff0c;那么你会看到输入输出流、Turbo标准库、标准输出端、stdout什么什么乱七八糟的&#xff0c;作为一个萌新&#xff0c;哪懂这些&#xff1f; 本文介绍萌新在前期的学习中&#xff0c;常用的输入输出函数及其功能~ 跳跃…

ROS2机器人编程简述humble-第二章-Controlling the Iterative Execution .3.1

2.3 ANALYZING THE BR2 BASICS PACKAGE 这一节内容有些多……前一篇&#xff1a;ROS2机器人编程简述humble-第二章-DEVELOPING THE FIRST NODE .2里面只有节点&#xff0c;没有任何实际功能。logger.cpp代码如下所示&#xff1a;#include "rclcpp/rclcpp.hpp"using n…

微信小程序分享的图片被裁切了。怎么让他不裁剪正常比例5:4显示

现在的效果 希望的效果 最主要的是下面的这个函数。把图片转成了5:4的临时图片 cutShareImg(doctorImg:string ){let thatthis;return new Promise((resolve) > {wx.getImageInfo({src: doctorImg, // 这里填写网络图片路径 success: (res) > {var data resconsole.l…

使用 LibreOffice 将 word 转化为 pdf 并解决中文乱码问题

目录 一、安装 LibreOffice 二、解决乱码问题 2.1 查看是否安装中文字体 2.2 准备字体 2.3 导入字体 2.4 验证 项目中有一个在线上传 word 并预览 pdf 报告的需求&#xff0c;因为项目部署在 ubuntu 上面&#xff0c;所以借助libreoffice 实现 word 转 pdf&#xff0c;然…

详细实例说明+典型案例实现 对枚举法进行全面分析 | C++

第五章 枚举法 目录 ●第五章 枚举法 ●前言 1.简要介绍 2.代码及结果示例&#xff08;简单理解&#xff09; 3.生活实例 ●二、枚举法的典型案例——鸡兔同笼&质数求解 1.鸡兔同笼 2.质数求解&#xff08;枚举法&#xff09; ●总结 前言 简单的来说…

最新 vue-cli 构建项目

vue-cli 构建项目 当前使用最新版本构建一个vue node项目 插件 vue-clivueelement-plusroutervuex 安装vue-cli npm install -g vue-cli安装完后 vue --version 查看版本 vue --version创建一个项目 vue create demo这里要选择版本&#xff0c;不同版本要相组合配置的插件…

反射的基本使用

文章目录1. 一个需求引出反射2. 反射机制2.1 Java Reflection2.2 Java 反射机制可以完成2.3 反射相关的主要类2.4 反射优点和缺点2.5 反射调用优化-关闭访问检查3. Class类3.1 基本介绍3.2 Class类的常用方法3.3 获取Class类对象3.4 哪些类型有Class对象3.5 类加载3.6 类加载流…

aws imagebuilder 理解并使用imagebuilder构建pcluster自定义ami

参考资料 ec2-image-builder-workshop Troubleshoot EC2 Image Builder 理解imagebuilder imagebuilder 使用 cinc-client 进行客户端统一配置&#xff0c;CINC is not Chef&#xff0c;而是chef的免费分发版本。 https://cinc.sh/about/ imagebuilder管道的整体逻辑如下 核…

OpenHarmony如何切换横竖屏?

前言在日常开发中&#xff0c;大多APP可能根据实际情况直接将APP的界面方向固定&#xff0c;或竖屏或横屏。但在使用过程中&#xff0c;我们还是会遇到横竖屏切换的功能需求&#xff0c;可能是通过物理重力感应触发&#xff0c;也有可能是用户手动触发。所以本文主要带大家了解…

Git 代码版本管理工具详解 进厂必备

目录前言Git 概述什么是版本控制&#xff1f;为什么需要版本控制&#xff1f;版本控制工具集中式分布式Git 工作机制Git安装Git 常用命令(部分)初始化本地库设置用户签名初始化本地库查看本地库状态***工作区代码编写***添加暂存区撤销工作区的修改***提交本地库***工作区修改代…

选择排序算法的实现和优化

初识选择排序&#xff1a; 算法思想[以升序为例]&#xff1a; 第一趟选择排序时&#xff0c;从第一个记录开始&#xff0c;通过n-1次关键字的比较&#xff0c;从第n个记录中选出关键字最小的记录&#xff0c;并和第一个记录进行交换 第二趟选择排序时&#xff0c;从第二个记…

Linux学习笔记【part1】目录结构与VIM文本编辑器

Linux基础篇学习笔记 1.CentOS 7 64位安装 第一步&#xff0c;在软件选择中可以设置图形界面。 第二步&#xff0c;手动分区中设置挂载点&#xff0c;分别为引导分区、通用分区和交换区。 第三步&#xff0c;设置内核崩溃转储机制&#xff0c;这对服务器来说非常有用。 第四步…

传输层协议:TCP与UDP协议的区别

TCP和UDP有哪些区别&#xff1f; 关于TCP与UDP协议两个协议的区别&#xff0c;大部分人会回答&#xff0c;TCP是面向连接的&#xff0c;UDP是面向无连接的。 什么叫面向连接&#xff0c;什么叫无连接呢&#xff1f;在互通之前&#xff0c;面向连接的协议会先建立连接。例如&a…

网络工程师备考7章

考点分布: 注:考点不多,这个重点记住即可; 7.1 IPV4的问题与改进 7.2 IPV6的报文格式 注:版本0110表示IPV6,源地址和目的地址都是128位(bit),整个头部固定40个B(字节) 注:通信类型和流标记实际上是没有用的。负载长度是实际的报文长度,下一个头部:IPV6是可以作…

297. 二叉树的序列化与反序列化

297. 二叉树的序列化与反序列化 难度困难 序列化是将一个数据结构或者对象转换为连续的比特位的操作&#xff0c;进而可以将转换后的数据存储在一个文件或者内存中&#xff0c;同时也可以通过网络传输到另一个计算机环境&#xff0c;采取相反方式重构得到原数据。 请设计一个…

Linux:查看服务器信息,CPU、内存、系统版本、内核版本等

还是最近工作的总结&#xff0c;性能验证要根据服务器的配置才能做进一步的结论论证&#xff0c;废话不多说 目录查看Linux内核版本查看Linux系统版本CPU查看CPU信息&#xff08;型号&#xff09;物理CPU个数每个物理CPU中core的个数(即核数)查看逻辑CPU的个数内存查看内存信息…

【C语言航路】第十三站:动态内存管理

目录 一、为什么存在动态内存分配 二、动态内存函数 1.内存的分区 2.malloc和free &#xff08;1&#xff09;malloc和free库函数文档 &#xff08;2&#xff09;malloc和free的使用 2.calloc &#xff08;1&#xff09;calloc的库函数文档 &#xff08;2&#xff09;c…