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原手眼标定里求位移相关逻辑.

    // 解决追踪器与摄像机使用不同尺寸问题.
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里面,新的代码如下.

    // 解决追踪器与摄像机使用不同尺寸问题.
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.

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

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,告诉顶点如果更新参数.

// 内置钢体变换顶点
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比较的过程.

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,比较误差.

// 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变换比较误差,最后我们把所有已知数据与测量值组成图如下.

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个像素,后面也还需要持续优化.

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

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使用冲突,等等这些细节问题才是更麻烦的.

LED虚拟拍摄-跟踪算法的更多相关文章

  1. KCF跟踪算法 入门详解

    一.算法介绍 KCF全称为Kernel Correlation Filter 核相关滤波算法.是在2014年由Joao F. Henriques, Rui Caseiro, Pedro Martins ...

  2. TLD目标跟踪算法

    1. 简介 TLD目标跟踪算法是Tracking-Learning-Detection算法的简称.这个视频跟踪算法框架由英国萨里大学的一个捷克籍博士生Zdenek Kalal提出.TLD将传统的视频跟 ...

  3. matlab工具箱之人眼检测+meanshift跟踪算法--人眼跟踪

    Viola-Jones 人眼检测算法+meanshift跟踪算法 这次的代码是对视频中的人眼部分进行检测加跟踪,检测用的是matlab自带的人眼检测工具箱 下面是matlab官网介绍这个算法的一些东西 ...

  4. 比微软kinect更强的视频跟踪算法--TLD跟踪算法介绍

    转自:http://blog.csdn.net/carson2005/article/details/7647500 TLD(Tracking-Learning-Detection)是英国萨里大学的一 ...

  5. TLD视觉跟踪算法(转)

    源:TLD视觉跟踪算法 TLD算法好牛逼一个,这里有个视频,是作者展示算法的效果,http://www.56.com/u83/v_NTk3Mzc1NTI.html.下面这个csdn博客里有人做的相关总 ...

  6. Video Target Tracking Based on Online Learning—TLD单目标跟踪算法详解

    视频目标跟踪问题分析         视频跟踪技术的主要目的是从复杂多变的的背景环境中准确提取相关的目标特征,准确地识别出跟踪目标,并且对目标的位置和姿态等信息精确地定位,为后续目标物体行为分析提供足 ...

  7. TLD(Tracking-Learning-Detection)一种目标跟踪算法

    原文:http://blog.csdn.net/mysniper11/article/details/8726649 视频介绍网址:http://www.cvchina.info/2011/04/05 ...

  8. 基于MeanShift的目标跟踪算法及实现

    这次将介绍基于MeanShift的目标跟踪算法,首先谈谈简介,然后给出算法实现流程,最后实现了一个单目标跟踪的MeanShift算法[matlab/c两个版本] csdn贴公式比较烦,原谅我直接截图了 ...

  9. TLD视觉跟踪算法

    TLD算法好牛逼一个,这里有个视频,是作者展示算法的效果,http://www.56.com/u83/v_NTk3Mzc1NTI.html.下面这个csdn博客里有人做的相关总结,感觉挺好的,收藏了! ...

  10. Real-Time Compressive Tracking,实时压缩感知跟踪算法解读

    这是Kaihua Zhang发表在ECCV2012的paper,文中提出了一种基于压缩感知(compressive sensing)的单目标跟踪算法,该算法利用满足压缩感知(compressive s ...

随机推荐

  1. js实现打字机效果(完整实例)

    在上篇css高斯模糊的效果基础上用js实现一个打字机效果: 上图: 代码: <!DOCTYPE HTML> <html lang="en-US"> < ...

  2. nmcli device status状态为unmanaged

    遇到报错:(device lo not available because device is strictly unmanaged) nmcli device status 查看设备状态 nmcli ...

  3. Ajax 请求总共有八种 Callback

    1)onSuccess 2)onFailure 3)onUninitialized 4)onLoading 5)onLoaded 6)onInteractive 7)onComplete 8)onEx ...

  4. 5G MEC 之本地分流实现方式

    目录 文章目录 目录 前言 LADN ULCL IPv6 Multi-homing(BP) 典型应用场景 前言 最近同事发表了一片非常棒的文章,笔者在此之上进行了补充,转发至此与大家分享. 本地分流作 ...

  5. 智能勘探 | AIRIOT智慧油田管理解决方案

      石油勘探和开采地处偏远地区,涉及面广且生产规模大.特殊的作业环境下,使得工作人员作业条件艰苦,仅靠人工值守难度很大,不可避免的遇到一系列硬核挑战: 1.设备维护难度较高: 2.采油厂分布地域广.分 ...

  6. layui合并单元格

    在别人的基础上解决了多列合并和同一个页面多个表格的问题 1 //合并单元格 2 function merge(id,res, columsName, columsIndex) { 3 4 var da ...

  7. sass @extend(继承)指令详解

    在设计网页的时候常常遇到这种情况:一个元素使用的样式与另一个元素完全相同,但又添加了额外的样式. 通常会在 HTML 中给元素定义两个 class,一个通用样式,一个特殊样式. 普通CSS的实现 接下 ...

  8. 微信小程序报错“Component “页面路径“ does not have a method “ 方法名“ to handle event tap的

    我遇到这个问题的原因是,data属性把methods包括在里面了,把methods当成了一个字段,所以就相当于没写methods属性,里面的方法就识别不了了.解决办法就是把data的后括号打在meth ...

  9. Android OpenMAX(七)OMX Service

    上一篇文章我们分析了Android使用OMXStore来管理平台上的软件和硬件编解码组件,这一篇我们再向上一层了解应用层是如何获取调用OMXStore管理组件的.本篇文章代码参考自: framewor ...

  10. UIScrollView 在Autolayout下使用的一些问题

    一.UIScrollView 双指放大手势,双击放大实现 在设置UIScrollView的frame后.maxZoomScale 和 minZoomScale之后,UIScrollView会自然支持双 ...