LED虚拟拍摄-跟踪算法

LED虚拟拍摄-跟踪算法

图引用拍摄黑科技,LED虚拟影棚揭秘

标定流程

上面是一台Track设备,现精度比较高的主要是Redspy,Mosys,一般影视用这二种,其底层技术参考SMAL单目+惯性传感器(IMU),因为需要稳定精准的结果,实现上会贴红外反光片,使用红外相机得到这些贴片对应的稳定特征点用于建图(红外相机受外部亮度影响较小,在红外相机里,红外反光贴片相对周边会非常明亮,就像星空里发光的星星一样),也因为单目的原因,传感器本身的尺度与现实其实不对应,需要额外确保传感器本身的尺度与现实对应.

Track跟踪,主要是如下几步,求相机内参,相机与Track相对变换,Track空间与LED空间的相对变换,现在的标定一般来说,都是三步一起解决的.

标定内参,主要还是类似张正友标定法,得到在不同相机姿态态,得到一系列角点的2D-3D关系,其角点的选取可以来自棋盘格,这种方式限制大,需要拍摄全,对相机方向与位置限定了,影响后续手眼标定的精度,所以现在更多选择是的Aruco码,Aruco角点是有索引的,这样就方便找到明确的3D位置,相机只需要拍摄一部分aruco码就可以了,不会对相机距离与方向有限制,不过Aruco角点精度可能比不上圆形块,开了亚像素也不太行,于是像hecoos这样的,会利用视频流打圆形块,根据一系列帧出现/没出现计算出唯一索引,也能确定唯一位置,但是这种方式标定就会比较费时.

得到一系列3D-2D的角点后,代入cv::calibrateCamera求出就可以了,也得到每帧数据相机的变换,一般在记录图片时,也会记录Track的变换,这样就得到一组相机与Track的变换,如果根据这组数据,得到相机与Track相对变换了,这其实就是机器人常见的手眼标定的问题.

其手眼标定(本文限定眼在手上)的算法利用二个坐标系的钢体变化,构建一个AX=XB的问题去求解,先求得相记与Track的变换矩阵,然后得到Track坐标系与LED坐标系的变换,把结果代入记录数组,计算重投影的误差来判断结果的好坏.

实际过程来说,一般是取九张不同姿态的图,然后先算内参,再用手眼标定算相机与Track相对变换,但是直接九张图效果一般不太好,会加上RANSAC算法,选择其中3-5张结合结果,使用SVD得到最优Track空间与LED空间的相对变换,最后根据这二个结果代入之前记录计算重投影拿到最优结果.

手眼标定算法改进

这里有个问题,前面提到因为单目的原因,传感器本身的尺度与现实其实不对应,意思Track给出的数据是1m,但是在现实中可能对应10cm,10m,3m这些位移,OpneCV本身手眼标定并不能计算这种与现实的尺度不匹配的数据,现场一般会利用Redspy/Mosys硬件本身的功能纠正,但是使用比较麻烦,比如已经绑在摄像机上需要重新取下,人工取尺输入现实数据移动,精度就和人扯上关系了,和人扯上关系,就容易出问题,能不能改了?其实是可以,还记的当时那段时间刚好新冠被封锁在租的小区里,就仔细看了下opencv里的手眼标定的Tsai算法.

改进的主要就是求得位移的过程,先来看下opencv原手眼标定里求位移相关逻辑.

c++ 复制代码
    // 解决追踪器与摄像机使用不同尺寸问题.
    Mat A(3 * K, 3, CV_64FC1);
    // Will store: Pcij - Pgij
    Mat B(3 * K, 1, CV_64FC1);
    idx = 0;
    for (size_t i = 0; i < Hg.size(); i++)
    {
        for (size_t j = i+1; j < Hg.size(); j++, idx++)
        {
            //Defines coordinate transformation from Gi to Gj
            //Hgi is from Gi (gripper) to RW (robot base)
            //Hgj is from Gj (gripper) to RW (robot base)
            Mat Hgij = vec_Hgij[static_cast<size_t>(idx)];
            //Defines coordinate transformation from Ci to Cj
            //Hci is from CW (calibration target) to Ci (camera)
            //Hcj is from CW (calibration target) to Cj (camera)
            Mat Hcij = vec_Hcij[static_cast<size_t>(idx)];

            //Left-hand side: (Rgij - I)
            Mat diff = Hgij(Rect(0,0,3,3)) - Mat::eye(3,3,CV_64FC1);
            diff.copyTo(A(Rect(0, idx*3, 3, 3)));

            //Right-hand side: Rcg*Tcij - Tgij
            diff = Rcg*Hcij(Rect(3, 0, 1, 3)) - Hgij(Rect(3, 0, 1, 3));
            diff.copyTo(B(Rect(0, idx*3, 1, 3)));
        }
    }

    Mat Tcg;
    //Translation from camera to gripper is obtained from the set of equations:
    //    (Rgij - I) * Tcg = Rcg*Tcij - Tgij    (eq 15)
    solve(A, B, Tcg, DECOMP_SVD);

利用等式

\[(Rgij - I) * Tcg = Rcg*Tcij - Tgij \]

其中Rgij表示标定记录中二次记录之间追踪器空间下追踪器之间的旋转,根据记录是已知量.I表示3x3的单位矩阵,是固定量.Tcg表示追踪器空间下,摄像机相对追踪器的位移偏移,也就是我们要求解的值.Rcg表示追踪器空间下,摄像机相对追踪器的旋转偏移.Tcij 表示标定记录中二次记录之间摄像机空间下标定板之间的旋转,根据记录是已知量.Tgij 表示标定记录中二次记录之间追踪器空间下追踪器之间的位移,根据记录是已知量.假定Tracl与现实中的缩放参数是Scale,那么可以重组为如下等式.

\[(Rgij - I) * Tcg - Rcg*Tcij*Scale = -Tgij \]

原来矩阵算式如下.

\[\begin{bmatrix} A_{00}&A_{01}&A_{02}\\ A_{10}&A_{11}&A_{12}\\ A_{20}&A_{21}&A_{22}\\ \end{bmatrix} * \begin{bmatrix} T_{0}\\ T_{1}\\ T_{2}\\ \end{bmatrix} = \begin{bmatrix} B_{0}\\ B_{1}\\ B_{2}\\ \end{bmatrix} \]

现在变换后,矩阵算式如下.

\[\begin{bmatrix} A_{00}&A_{01}&A_{02}&C_{03}\\ A_{10}&A_{11}&A_{12}&C_{13}\\ A_{20}&A_{21}&A_{22}&C_{23}\\ \end{bmatrix} * \begin{bmatrix} T_{0}\\ T_{1}\\ T_{2}\\ S_{3}\\ \end{bmatrix} = \begin{bmatrix} B_{0}\\ B_{1}\\ B_{2}\\ \end{bmatrix} \]

其中新变换里的C表示- Rcg*Tcij,这样就把缩放参数带入要求解的[T0,T1,T2,S3]中的S3里面,新的代码如下.

c++ 复制代码
    // 解决追踪器与摄像机使用不同尺寸问题.
    Mat TA(3 * K, 4, CV_64FC1);
    // Will store: Pcij - Pgij
    Mat TB(3 * K, 1, CV_64FC1);
    idx = 0;
    for (size_t i = 0; i < Hg.size(); i++) {
        for (size_t j = i + 1; j < Hg.size(); j++, idx++) {
            // Defines coordinate transformation from Gi to Gj
            // Hgi is from Gi (gripper) to RW (robot base)
            // Hgj is from Gj (gripper) to RW (robot base)
            Mat Hgij = vec_Hgij[static_cast<size_t>(idx)];
            // Defines coordinate transformation from Ci to Cj
            // Hci is from CW (calibration target) to Ci (camera)
            // Hcj is from CW (calibration target) to Cj (camera)
            Mat Hcij = vec_Hcij[static_cast<size_t>(idx)];

            // Left-hand side:3x3_(Rgij - I) 3x1_(Rcg*Tcij)
            Mat ldiff1 = Hgij(Rect(0, 0, 3, 3)) - Mat::eye(3, 3, CV_64FC1);
            ldiff1.copyTo(TA(Rect(0, idx * 3, 3, 3)));
            Mat ldiff2 = -(Rcg * Hcij(Rect(3, 0, 1, 3)));
            ldiff2.copyTo(TA(Rect(3, idx * 3, 1, 3)));

            // Right-hand side: -Tgij
            Mat diff = -Hgij(Rect(3, 0, 1, 3));
            diff.copyTo(TB(Rect(0, idx * 3, 1, 3)));
        }
    }
    Mat Tcg;
    solve(TA, TB, Tcg, DECOMP_SVD);

其求得的4*1前面三个表示Tcg(摄像机相对追踪器的位移),最后数据表示现实世界相对追踪器坐标系下缩放是scale.

这样后,应该得到类似一个如下结果.

C++ 复制代码
struct ECameraTrack {
  // 追踪器坐标系如何变换成标定板坐标系
  Eigen::Matrix4d base2target = {};
  // 保存摄像机相对track的姿态
  Eigen::Matrix4d camera2track = {};
  // 追踪器坐标系相对标定板的位移缩放
  double scale = 1.0;
};

图优化标定结果

上面处理后,一般来说,结果平均在8个像素误差左右,用于VP肯定是够了,VP一般是扩展FOV的,但是如果想用于XR拍摄,这个精度就可能会不够,还能不能计算更精准,先看下优化后的效果.

可以看到平均4个像素误差优化到1个左右,实际实现情况大的LED幕墙,原结果平均8个像素(还不稳定)可以优化到稳定的平均3个像素左右误差,精度够XR用了.

那段时间正好在看同事推荐的视觉SLAM十四讲,学习图优化相关框架g2o,可以直接使用测量值比较误差求解或是优化参数,简单总结图优化的步骤,一是确定需要优化的变量,二是确定变量到观测量的计算过程,通过这个过程得到计算结果与观测量比较得到误差,第三步是构建变量与计算过程与观测量误差的图.

前面二步对应图优化框架g2o里的二个概念,分别是顶点和边,其顶点对应的就是需要求解的变量,而边就是由顶点构建计算过程,得到结果并与测量值确定误差,第三步优化过程就是构建顶点,边与测量值的图.

相对常规解法,图优化是直接根据测量值优化结果,只需要构建一个参数能正确和测量值比较误差的模型就行,相比手眼标定算法,需要理解钢体关系多帧间几个变化的相等性到构建AX=XB的处理来说,直接由结果去代入重投影比较误差,然后构建模型会非常容易理解,在3D视觉中,有非常多的这种需求,知道变量,知道变量导致的结果误差,但是不知道怎么求解,其图优化就可以求解这种情况,所以在3D视觉中,其图优化使用非常普遍.

如下有些代码,其变换关系不理解会比较乱,为了方便理解,简单介绍一下结构与命名,比如target说的是屏幕坐标系,camera表示摄像机在屏幕坐标系下的运动,base是追踪器坐标系,track表示追踪器在追踪器坐标系下的运动,这样如base2target表示追踪器坐标系转到屏幕坐标系下的变换,而camera2track表示camera相对track在追踪器坐标系下相对变换,camera2target表示摄像机在屏幕坐标系下的变换,target2camera表示camera2target的逆变换,可以理解成屏幕在摄像机下的变换,track2base表示追踪器在追踪器坐标系下的变换.

如前面介绍图优化的过程,先需要确定求解的值,在这就是camera2track(也就是原手眼标定求解的值),base2target用于把追踪器坐标系转化到屏幕坐标系下,需要注意的是,这里的track给的位移与现实中的位移没有对应,有个缩放关系,在这我们也需要求解这个Scale值,这样就有三个值需要求解,然后假定标定过程使用的Track变换相对真实值有细微误差,针对每个Track记录变换也当做一个需要优化的值,这个处理有兴趣的可以详细参考基于重投影误差最小化的手眼标定,能正确的处理Track异常记录,经实际测试,对于结果的正确性会有很大提高.

需要的结果是二个变换+一个缩放,和中间Track记录变换的优化,变换对应的变量直接使用g2o内置的VertexSE3,截取重要代码如下,有兴趣可以自己去看g2o里的源码,而缩放只需要简单封装一个double就行,简单来说,顶点最主要的是实现方法oplusImpl,告诉顶点如果更新参数.

C++ 复制代码
// 内置钢体变换顶点 
class G2O_TYPES_SLAM3D_API VertexSE3 : public BaseVertex<6, Isometry3> {
 public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW;

  virtual void setToOriginImpl() { _estimate = Isometry3::Identity(); }
  virtual void oplusImpl(const double* update) {
    Eigen::Map<const Vector6> v(update);
    Isometry3 increment = internal::fromVectorMQT(v);
    _estimate = _estimate * increment;
  }
}
// 封装缩放的顶点
class ScaleVertex : public g2o::BaseVertex<1, double> {
 public:
  ScaleVertex() {}

  virtual void setToOriginImpl() { _estimate = 1.0; }
  virtual void oplusImpl(const double* update) {    
    _estimate += update[0];
  }
  virtual bool read(std::istream& is) {
    is >> _estimate;
    return true;
  }
  virtual bool write(std::ostream& os) const {
    os << _estimate;
    return true;
  }
};

确定顶点后,然后就是确定边,就如前面所说,边是确定如何优化顶点与测量值的误差变小的,这里使用重投影确定角点UV与测量UV的误差,角点的三维位置与测量UV分别由建立屏幕坐标系时确定以及OPENCV查找角点得到,边的computeError记录了如何把角点的三维位置转化到摄像机下的位置,并得到UV,然后与测量UV比较的过程.

C++ 复制代码
class ProjectionHandEdge
    : public g2o::BaseUnaryEdge<2, Eigen::Vector2d, g2o::VertexSE3> {
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW
 public:
  ProjectionHandEdge()
      : g2o::BaseUnaryEdge<2, Eigen::Vector2d, g2o::VertexSE3>() {
    resizeParameters(1);
    installParameter(lensModelPar, 0);
  }

  virtual bool read(std::istream& is) { return false; }

  virtual bool write(std::ostream& os) const { return false; };

  virtual void computeError() override;
  g2o::Vector3 point = {};

 protected:
  LensModelParameter* lensModelPar = nullptr;
};

void ProjectionHandEdge::computeError() {
  const g2o::VertexSE3* target2cameraVec =
      dynamic_cast<const g2o::VertexSE3*>(_vertices[0]);
  Eigen::Isometry3d target2camera = target2cameraVec->estimate();
  // 点在相机下位置
  g2o::Vector3 cameraPos = target2camera * point;
  // UV
  g2o::Vector2 cuv = lensModelPar->map(cameraPos);
  // 误差由观测值减预测值
  _error = measurement() - cuv;
}

// 使用小孔相机模型确定3维顶点的投影平面
g2o::Vector2 lensMap(const LensModel& lensModel, const g2o::Vector3& pos,
                     double scale) {
  double x = pos[0] / pos[2];
  double y = pos[1] / pos[2];
  double fx = lensModel.focalLength.x;
  double fy = lensModel.focalLength.y;
  double cx = lensModel.focalCenter.x;
  double cy = lensModel.focalCenter.y;
  double k1 = lensModel.K1;
  double k2 = lensModel.K2;
  double k3 = lensModel.K3;
  double p1 = lensModel.P1;
  double p2 = lensModel.P2;
  // 径向畸变
  double r2 = x * x + y * y;
  double r4 = r2 * r2;
  double r6 = r4 * r2;
  // 切向畸变
  double a1 = 2 * x * y;
  double a2 = r2 + 2 * x * x;
  double a3 = r2 + 2 * y * y;
  double cdist = 1.0 + k1 * r2 + k2 * r4 + k3 * r6;
  x = x * cdist + p1 * a1 + p2 * a2;
  y = y * cdist + p1 * a3 + p2 * a1;
  // UV
  double u = fx * x + cx;
  double v = fy * y + cy;
  return g2o::Vector2(u * scale, v * scale);
}

每次记录对应的Track变换优化对应设计如下,已经每次记录track2base(track在Track坐标系下的变换),和根据需要求解的变量camera2track,base2target,scale以及摄像机在屏幕坐标系下的变换,得到求解出来的track2base,根据测试出来的track2base,比较误差.

C++ 复制代码
// track姿态优化
class HandEyeEdge : public g2o::BaseMultiEdge<6, Eigen::Isometry3d> {
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW
 public:
  HandEyeEdge();

  virtual bool read(std::istream& is);

  virtual bool write(std::ostream& os) const;

  virtual void computeError() override;
};
HandEyeEdge::HandEyeEdge() { resize(4); }

bool HandEyeEdge::read(std::istream& is) {
  Vector7 est = {};
  bool state = internal::readVector(is, est);
  _measurement = internal::fromVectorQT(est);
  return readInformationMatrix(is);
}

bool HandEyeEdge::write(std::ostream& os) const {
  internal::writeVector(os, internal::toVectorQT(measurement()));
  return writeInformationMatrix(os);
}

Eigen::Isometry3d getScaleIsometry3d(const Eigen::Isometry3d& src,
                                     double scale) {
  Eigen::Isometry3d result = src;
  result.translation() = scale * src.translation();
  return result;
};

void HandEyeEdge::computeError() {
  // 摄像机相对Track
  const g2o::VertexSE3* camera2trackVec =
      dynamic_cast<const g2o::VertexSE3*>(_vertices[0]);
  // 追踪器坐标系变换到角点坐标系
  const g2o::VertexSE3* base2targetVec =
      dynamic_cast<const g2o::VertexSE3*>(_vertices[1]);
  // 角点
  const g2o::VertexSE3* target2cameraVec =
      dynamic_cast<const g2o::VertexSE3*>(_vertices[2]);
  // 缩放
  const ScaleVertex* scaleVec = dynamic_cast<const ScaleVertex*>(_vertices[3]);
  // 当前值
  Eigen::Isometry3d camera2track = camera2trackVec->estimate();
  Eigen::Isometry3d base2target = base2targetVec->estimate();
  Eigen::Isometry3d target2camera = target2cameraVec->estimate();
  double scale = scaleVec->estimate();
  // 优化量计算得到的Track
  Eigen::Isometry3d base2track = camera2track * target2camera * base2target;
  // 转化成世界尺度下
  Eigen::Isometry3d measurementScale = getScaleIsometry3d(_measurement, scale);
  // 比较计算的Track与测量的Track姿态
  Eigen::Isometry3d delta = measurementScale * base2track;
  _error = g2o::internal::toVectorMQT(delta);
}

同上面重投影边一样,在computeError把根据这些结果求得的track变换与测量的track变换比较误差,最后我们把所有已知数据与测量值组成图如下.

C++ 复制代码
CameraTrack HandEyeOptimizer::compute(const HandEyeParamet& handEyeParamet,
                                      const LensModel& lensModel,
                                      const CameraTrack& cameraTrack) {
  handEyePar = handEyeParamet;
  eigen::ECameraTrack camtrack = {};
  eigen::toCameraTrack(cameraTrack, camtrack);
  Eigen::Isometry3d camera2track = Eigen::Isometry3d::Identity();
  Eigen::Isometry3d base2target = Eigen::Isometry3d::Identity();
  camera2track.matrix() = camtrack.camera2track;
  base2target.matrix() = camtrack.base2target;
  // 设置HandEyeEdge的变化率
  Eigen::MatrixXd handeyeInf = Eigen::MatrixXd::Identity(6, 6);
  // 移动部分
  handeyeInf.topLeftCorner(3, 3) *= 0.01;
  // 旋转部分
  handeyeInf.bottomRightCorner(3, 3) *= 1.0;
  // 优化器
  SparseOptimizer optimizer;
  using LinearSolver =
      g2o::LinearSolverDense<g2o::BlockSolverX::PoseMatrixType>;
  OptimizationAlgorithmGaussNewton* solver =
      new g2o::OptimizationAlgorithmGaussNewton(
          std::make_unique<g2o::BlockSolverX>(
              std::make_unique<LinearSolver>()));
  // 设置优化方法
  optimizer.setAlgorithm(solver);
  // 设置镜头参数
  LensModelParameter* lensPar = new LensModelParameter(lensModel);
  lensPar->uvScale = handEyeParamet.uvScale;
  lensPar->setId(0);
  optimizer.addParameter(lensPar);
  // 优化量camera2Track
  VertexSE3* c2tVec = new VertexSE3();
  c2tVec->setEstimate(camera2track);
  c2tVec->setId(0);
  // c2tVec->setFixed(true);
  optimizer.addVertex(c2tVec);
  // 优化量base2target
  VertexSE3* b2tVec = new VertexSE3();
  b2tVec->setEstimate(base2target);
  b2tVec->setId(1);
  // b2tVec->setFixed(true);
  optimizer.addVertex(b2tVec);
  ScaleVertex* scaleVec = new ScaleVertex();
  scaleVec->setEstimate(cameraTrack.scale);
  scaleVec->setId(2);
  scaleVec->setFixed(handEyePar.bFixScale);
  optimizer.addVertex(scaleVec);
  // 每条记录(包含摄像机变换,Track变换,角点UV与三维位置)
  for (const TrackCorners& trackCorner : trackCorners) {
    aoce::Mat4x4d trackPose = trackCorner.trackPose;
    aoce::Mat4x4d cameraPose = trackCorner.cameraPose;
    Eigen::Isometry3d track2base = Eigen::Isometry3d::Identity();
    track2base.matrix() = eigen::toMat(trackPose);
    // track转化成真实世界坐标系中
    Eigen::Isometry3d track2baseScale =
        getScaleIsometry3d(track2base, cameraTrack.scale);
    // 角点相对摄像机坐标系的转换
    Eigen::Isometry3d camera2target = Eigen::Isometry3d::Identity();
    if (trackCorner.cameraPose.valid()) {
      camera2target.matrix() = eigen::toMat(trackCorner.cameraPose);
    } else {
      camera2target = base2target * track2baseScale * camera2track;
    }
    Eigen::Isometry3d target2camera = camera2target.inverse();
    g2o::VertexSE3* t2cVec = new g2o::VertexSE3();
    t2cVec->setEstimate(target2camera);
    t2cVec->setId(optimizer.vertices().size());
    // t2cVec->setFixed(true);
    optimizer.addVertex(t2cVec);
    // 设置Track姿态边
    HandEyeEdge* handEyeEdge = new HandEyeEdge();
    handEyeEdge->vertices()[0] = c2tVec;
    handEyeEdge->vertices()[1] = b2tVec;
    handEyeEdge->vertices()[2] = t2cVec;
    handEyeEdge->vertices()[3] = scaleVec;
    // 比较实测与观测的Track数据
    handEyeEdge->setMeasurement(track2base);
    handEyeEdge->setInformation(handeyeInf);
    handEyeEdge->setId(optimizer.edges().size());
    if (handEyePar.robustHandEye) {
      g2o::RobustKernelHuber* kerner = new g2o::RobustKernelHuber();
      kerner->setDelta(handEyePar.handEyeDelta);
      handEyeEdge->setRobustKernel(kerner);
    }
    optimizer.addEdge(handEyeEdge);
    // 经测试,UV使用的粒度小时,精度会提升
    vec2d sizeInv = {
        handEyeParamet.uvScale / trackCorner.pointCorners.imageSize.x,
        handEyeParamet.uvScale / trackCorner.pointCorners.imageSize.y};
    for (int32_t i = 0; i < trackCorner.pointCorners.count; i++) {
      vec2f corner = *(trackCorner.pointCorners.corners + i);
      vec3f point = *(trackCorner.pointCorners.points + i);
      // 设置投影边
      ProjectionHandEdge* proEdge = new ProjectionHandEdge();
      proEdge->setMeasurement({corner.x * sizeInv.x, corner.y * sizeInv.y});      
      proEdge->setInformation(Eigen::Matrix2d::Identity() * 0.01);
      proEdge->vertices()[0] = t2cVec;
      proEdge->setParameterId(0, 0);
      proEdge->setId(optimizer.edges().size());
      proEdge->point = {point.x, point.y, point.z};
      if (handEyePar.projectionHand) {
        g2o::RobustKernelHuber* kerner = new g2o::RobustKernelHuber();
        kerner->setDelta(handEyePar.projectionHand);
        proEdge->setRobustKernel(kerner);
      }
      optimizer.addEdge(proEdge);
    }
  }
  // 执行优化
  bool bInit = optimizer.initializeOptimization();
  if (!bInit) {
    logMessage(LogLevel::warn,
               "ZoomScaleOptimizer::computePoseZoom init optimizer failed");
    return cameraTrack;
  }
  // optimizer.setVerbose(true);
  optimizer.optimize(10);
  // 检测
  camtrack.camera2track = c2tVec->estimate().matrix();
  camtrack.base2target = b2tVec->estimate().matrix();
  camtrack.scale = scaleVec->estimate();
  CameraTrack result = {};
  eigen::toCameraTrack(camtrack, result);
  return result;
}

整个过程经过优化器多次迭代就能得到更优结果,相对于原始手眼标定的结果来说,优点不少,原始的数据越多,其直接组合一起计算结果很差,只能从各种组合计算结果使用重投影确定最优值,一般最多只选择其中不大于五条记录会是一个最好的结果,大部分记录被排除计算,数据量比较少的情况下,又不能保证结果在10个像素以内,使用图优化的方法,在记录少的情况就能得到非常优秀的结果,在记录多的情况下,更能保证更多的数据得到平均最优的结果,代入以前记录的各组记录数据,在图优化的情况下,能保证所有结果都能得到更好的结果.

变焦标定

在标定Track之后,知道镜头在某个焦段的内外参,扩展到变焦镜头,当镜头的zoom变化后,内参如何变化?

一般相机内参fx/fy,cx/cy,考虑畸变系数K1,K2,P1,P2,K3的畸变模型,与zoom有关的是fx/fy,cx/cy影响不大,畸变P1,P2现在相机在工艺上上,值非常小,对畸变的影响非常小,K3影响也不太大,故需要考虑的fx/fy,K1,K2这四个参数,考虑fx/fy比值固定,只需要考虑fx,k1,k2这三个参数在变焦镜头的zoom变化后,如何变化?

相机zoom变化后,内参不做变化,可以看到匹配的特征误差越来越大,也就是反投影的误差变大,误差优化参数,还得是图优化.

相比上一个手眼标定的图优化过程的点边模型,这个模型会简单不少,假定fx,k1,k2是zoom变化的曲线(ax^2+bx+c=y)变化,最后结果就是求得fx,k1,k2对应的a,b,c的值.对应有二种思路,一种是根据每个zoom下的图,反投影图优化得到fx,k1,k2,最后再拟合多个fx,k1,k2各自的曲线,第二种是直接把各自曲线所有zoom下的图然后反投影图优化一起优化,现二种最终结果相关不大,在定焦三个误差内,变焦后各zoom平均在10个像素,后面也还需要持续优化.

这里列一种处理的部分代码.

C++ 复制代码
void ZoomCurveEdge::computeError() {
  const CurveVertex* fxVer = dynamic_cast<const CurveVertex*>(_vertices[0]);
  const CurveVertex* k1Ver = dynamic_cast<const CurveVertex*>(_vertices[1]);
  const CurveVertex* k2Ver = dynamic_cast<const CurveVertex*>(_vertices[2]);
  const ScaleVertex* scaleVer = dynamic_cast<const ScaleVertex*>(_vertices[3]);

  const g2o::Vector3 fxCurve = fxVer->estimate();
  const g2o::Vector3 k1Curve = k1Ver->estimate();
  const g2o::Vector3 k2Curve = k2Ver->estimate();
  const double sacle = scaleVer->estimate();

  LensModel lensModel = lensModelPar->lensModel;
  double aspectRatio = lensModel.focalLength.y / lensModel.focalLength.x;
  double fx = getCurveVal(fxCurve, sacle);
  lensModel.focalLength.x = fx;
  lensModel.focalLength.y = fx * aspectRatio;
  lensModel.K1 = getCurveVal(k1Curve, sacle);
  lensModel.K2 = getCurveVal(k2Curve, sacle);
  g2o::Vector2 estValue = lensMap(lensModel, point);
  // 误差由观测值减预测值
  _error = measurement() - estValue;
}
bool ZoomLensOptimizer::compute(const ZoomOptParamet& paramet) {
  optParamet = paramet;
  // 开始优化
  SparseOptimizer optimizer;
  // LinearSolverDense LinearSolverEigen LinearSolverPCG
  using LinearSolver =
      g2o::LinearSolverEigen<g2o::BlockSolverX::PoseMatrixType>;
  // OptimizationAlgorithmGaussNewton OptimizationAlgorithmLevenberg
  // OptimizationAlgorithmDogleg
  OptimizationAlgorithmGaussNewton* solver =
      new g2o::OptimizationAlgorithmGaussNewton(
          std::make_unique<g2o::BlockSolverX>(
              std::make_unique<LinearSolver>()));
  // 设置优化方法
  optimizer.setAlgorithm(solver);
  // 设置参数
  LensModelParameter* lensPar = new LensModelParameter(lensModel);
  lensPar->setId(0);
  optimizer.addParameter(lensPar);
  // 曲线
  CurveVertex* fxCurve = new CurveVertex();
  fxCurve->setEstimate({0, 0, lensModel.focalLength.x});
  fxCurve->setId(0);
  optimizer.addVertex(fxCurve);
  CurveVertex* k1Curve = new CurveVertex();
  k1Curve->setEstimate({0, 0, lensModel.K1});
  k1Curve->setId(1);
  k1Curve->setFixed(optParamet.fixDistort);
  optimizer.addVertex(k1Curve);
  CurveVertex* k2Curve = new CurveVertex();
  k2Curve->setEstimate({0, 0, lensModel.K2});
  k2Curve->setId(2);
  k2Curve->setFixed(true);
  optimizer.addVertex(k2Curve);
  std::vector<ScaleVertex*> fxList;
  // 添加点与边
  for (const CameraZoom& poseZoom : poseZooms) {
    Eigen::Isometry3d target2camera = Eigen::Isometry3d::Identity();
    target2camera.matrix() = eigen::toMat(invPoseMat(poseZoom.cameraPose));
    // 假定变焦环的数据并不严谨(推理不出来,只能固定)
    ScaleVertex* scaleValue = new ScaleVertex();
    scaleValue->setId(optimizer.vertices().size());
    scaleValue->setEstimate(poseZoom.zoomScale);
    scaleValue->setFixed(true);
    optimizer.addVertex(scaleValue);
    //
    vec2d sizeInv = {1.0 / poseZoom.pointCorners.imageSize.x,
                     1.0 / poseZoom.pointCorners.imageSize.y};
    // 反投影
    for (int32_t i = 0; i < poseZoom.pointCorners.count; i++) {
      vec2f corner = *(poseZoom.pointCorners.corners + i);
      vec3f point = *(poseZoom.pointCorners.points + i);
      // 点在相机下位置
      g2o::Vector3 cornerPos = {point.x, point.y, point.z};
      g2o::Vector3 cameraPos = target2camera * cornerPos;
      // 设置边
      ZoomCurveEdge* zoomLensEdge = new ZoomCurveEdge();
      zoomLensEdge->vertices()[0] = fxCurve;
      zoomLensEdge->vertices()[1] = k1Curve;
      zoomLensEdge->vertices()[2] = k2Curve;
      zoomLensEdge->vertices()[3] = scaleValue;
      zoomLensEdge->setMeasurement(
          {corner.x * sizeInv.x, corner.y * sizeInv.y});
      zoomLensEdge->setInformation(Eigen::Matrix2d::Identity());
      zoomLensEdge->setParameterId(0, 0);
      zoomLensEdge->setId(optimizer.edges().size());
      zoomLensEdge->point = cameraPos;
      if (true) {
        g2o::RobustKernelHuber* kerner = new g2o::RobustKernelHuber();
        kerner->setDelta(1.0);
        zoomLensEdge->setRobustKernel(kerner);
      }
      optimizer.addEdge(zoomLensEdge);
    }
  }
  // 执行优化
  bool bInit = optimizer.initializeOptimization();
  optimizer.optimize(10);
  fxPar = fxCurve->estimate();
  k1Par = k1Curve->estimate();
  k2Par = k2Curve->estimate();
  // 计算误差
  LensModel slensModel = lensModel;
  for (CameraZoom& poseZoom : poseZooms) {
    Eigen::Vector2d offset = {0, 0};
    if (getLensModel(poseZoom.zoomScale, slensModel)) {
      Mat4x4d target2camera = poseZoom.cameraPose.inverse();
      offset = projectOffset(target2camera, slensModel, poseZoom.pointCorners);
    }
  }
  return true;
}

这个结果还需优化,在写到这里的时候仔细想了下,主要可能有几点,一是fx/fy在变焦下比值是否有相对大的变化,二是上述曲线是否有更科学的模型,三是现k2代入后,结果大部分结果更差,导致k2现在用固定的,但是焦距变化比较大后,K2的变化还是比较明显的,后面有机会再想想改进吧.

整个跟踪相关的算法差不多就是这个样子,其实算法占的比例并不大,主要是工程上的各种问题,如各种相机采集(Decklink,MF...),Redspy,Mosys硬件接入,以及给的是欧拉角,如何确定顺序,追踪数据如何通过LiveLink数据发送到UE,不同空间坐标系的变换,FBXMesh的导入与导出,各个模块如何有序的组合,动态链接库与UE使用冲突,等等这些细节问题才是更麻烦的.