针孔相机的原理就是初中时代学的小孔成像,在这个模型中,会用到3种坐标系:世界坐标系,相机坐标系以及图像像素坐标系。如图所示。
将针孔相机模型转换成数学模型,具体表示为:
s p = K P c = K [ R ∣ t ] P w sp = K{P_c} = K[R|t]{P_w} sp=KPc=K[R∣t]Pw
其中, P w P_w Pw表示世界坐标系下的坐标, P c P_c Pc表示相机坐标系下的坐标, p p p表示图像像素坐标系下的坐标, K K K表示相机的内参矩阵。将上式写的更具体一些为
由于光线穿过镜头时会发生折射,因此实际成像位置与理论成像位置存在一些偏差,另外镜头安装误差也会导致成像位置发生变化,我们成这些为畸变。一般与针孔模型搭配使用的畸变模型就是著名的Brown畸变模型,模型包含5个参数,3个径向畸变参数和2个切向畸变参数,该模型的具体形式如下
x d = x ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) + 2 p 1 x y + p 2 ( r 2 + 2 x 2 ) y d = y ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) + 2 p 2 x y + p 1 ( r 2 + 2 y 2 ) {x_d} = x(1 + {k_1}{r^2} + {k_2}{r^4} + {k_3}{r^6}) + 2{p_1}xy + {p_2}({r^2} + 2{x^2}) \\ y_d = y(1 + {k_1}{r^2} + {k_2}{r^4} + {k_3}{r^6}) + 2{p_2}xy + {p_1}({r^2} + 2{y^2}) xd=x(1+k1r2+k2r4+k3r6)+2p1xy+p2(r2+2x2)yd=y(1+k1r2+k2r4+k3r6)+2p2xy+p1(r2+2y2)
其中, p = ( x , y , 1 ) p=(x, y, 1) p=(x,y,1)表示相机坐标系中的点归一化到单位焦平面上的坐标, p d = ( x d , y d , 1 ) p_d=(x_d, y_d, 1) pd=(xd,yd,1)表示加入畸变后的坐标。
上述公式可以认为是对点进行加畸变的过程,那么怎么去畸变呢,本人比较常用的方法有以下两种,都是通过优化的方法来求解,废话不多说,直接上代码
方法1:
template<typename DERIVED_P>
void RadialTangentialDistortion::undistort(const Eigen::MatrixBase<DERIVED_P> &p_d,
const Eigen::MatrixBase<DERIVED_P> &p_ud) const
{
EIGEN_STATIC_ASSERT_VECTOR_SPECIFIC_SIZE_OR_DYNAMIC(Eigen::MatrixBase<DERIVED_P>, 2)
Eigen::MatrixBase<DERIVED_P> &y_ud = const_cast<Eigen::MatrixBase<DERIVED_P> &>(p_ud);
Eigen::MatrixBase<DERIVED_P> &y_d = const_cast<Eigen::MatrixBase<DERIVED_P> &>(p_d);
y_ud.derived().resize(2);
y_d.derived().resize(2);
struct MLFunctor{
MLFunctor( const Eigen::Vector2d &imagePoint, double k1, double k2, double k3, double p1, double p2)
: imagePoint_(imagePoint), _k1(k1), _k2(k2), _k3(k3), _p1(p1), _p2(p2)
{
}
int operator( )(const Eigen::VectorXd &x, Eigen::VectorXd &fvec) const{
double xx = x[0] * x[0];
double yy = x[1] * x[1];
double xy = x[0] * x[1];
double r2 = xx + yy;
double r4 = r2 * r2;
double r6 = r4 * r2;
double d = 1 + _k1 * r2 + _k2 * r4 + _k3 * r6;
fvec[0] = x[0] * d + 2.0 * _p1 * xy + _p2 * (r2 + 2.0 * xx);
fvec[1] = x[1] * d + 2.0 * _p2 * xy + _p1 * (r2 + 2.0 * yy);
fvec = fvec - imagePoint_;
return 0;
}
int df( const Eigen::VectorXd &x, Eigen::MatrixXd &fjac ) const
{
double xx = x[0] * x[0];
double yy = x[1] * x[1];
double xy = x[0] * x[1];
double r2 = xx + yy;
double r4 = r2 * r2;
double r6 = r4 * r2;
fjac(0, 0) = 1 + (3 * xx + yy) * _k1 + (4 * xx * r2 + r4) * _k2 + (r6 + 6 * xx * r4) * _k3 + 2 * x[1] * _p1 + 6 * x[0] * _p2;
fjac(0, 1) = 2 * xy * _k1 + 4 * xy * r2 * _k2 + 6 * xy * r4 * _k3 + 2 * x[0] * _p1 + 2 * x[1] * _p2;
fjac(1, 0) = fjac(0, 1);
fjac(1, 1) = 1 + (xx + 3 * yy) * _k1 + (4 * yy * r2 + r4) * _k2 + (r6 + 6 * yy * r4) * _k3 + 6 * x[1] * _p1 + 2 * x[0] * _p2;
return 0;
}
int values() const {
return 2; }
int inputs() const {
return 2; }
Eigen::Vector2d imagePoint_;
double _k1;
double _k2;
double _k3;
double _p1;
double _p2;
};
Eigen::Vector2d target = y_d;
MLFunctor func(target, m_k1, m_k2, m_k3, m_p1, m_p2);
Eigen::LevenbergMarquardt< MLFunctor > lm(func);
double x = y_d[0];
double y = y_d[1];
Eigen::VectorXd res = Eigen::Vector2d(x, y);
lm.minimize(res);
y_ud = res;
}
方法2:
template <typename T>
void distortPointFun(T x_ud, T y_ud, T *x_d, T *y_d, T k1, T k2, T k3, T p1, T p2){
T xx = x_ud * x_ud;
T yy = y_ud * y_ud;
T xy = x_ud * y_ud;
T r2 = xx + yy;
T r4 = r2 * r2;
T r6 = r4 * r2;
T d = k1 * r2 + k2 * r4 + k3 * r6;
*x_d = x_ud * d + T(2.0) * p1 * xy + p2 * (r2 + T(2.0) * xx);
*y_d = y_ud * d + T(2.0) * p2 * xy + p1 * (r2 + T(2.0) * yy);
}
template <typename T>
void undistortPoint(T x_d, T y_d, T *x_ud, T *y_ud, T k1, T k2, T k3, T p1, T p2){
T epsilon = T(1e-10);
*x_ud = x_d;
*y_ud = y_d;
T x_tmp, y_tmp;
distortPointFun(*x_ud, *y_ud, &x_tmp, &y_tmp, k1, k2, k3, p1, p2);
int n = 0;
while(n < 20 && sqrt((*x_ud + x_tmp - x_d) * (*x_ud + x_tmp - x_d) + (*y_ud + y_tmp - y_d) * (*y_ud + y_tmp - y_d)) > epsilon){
*x_ud = x_d - x_tmp;
*y_ud = y_d - y_tmp;
distortPointFun(*x_ud, *y_ud, &x_tmp, &y_tmp, k1, k2, k3, p1, p2);
n++;
}
if(n >= 20){
*x_ud = x_d;
*y_ud = y_d;
}
}
3D点到2D点的转换步骤如下:
1)对给定的世界坐标系下的一点 P w = ( X w , Y w , Z w ) P_w=(X_w, Y_w,Z_w) Pw=(Xw,Yw,Zw),应用投影矩阵 T = [ R ∣ t ] T=[R|t] T=[R∣t],得到该点在相机坐标系下的坐标 P c = ( X c , Y c , Z c ) = T P w P_c=(X_c,Y_c,Z_c)=TP_w Pc=(Xc,Yc,Zc)=TPw。
2)将 P c P_c Pc归一化到焦平面上,得到 P u = ( x , y , 1 ) = ( X c / Z c , Y c / Z c , 1 ) P_u=(x, y, 1) = (X_c / Z_c, Y_c/Z_c,1) Pu=(x,y,1)=(Xc/Zc,Yc/Zc,1)。
3)应用畸变参数,得到带畸变的点 P d = ( x d , y d , 1 ) P_d=(x_d, y_d, 1) Pd=(xd,yd,1)。
4)应用内参得到该点在图像像素坐标系下的坐标 p = ( u , v , 1 ) = K P d p=(u, v, 1) = KP_d p=(u,v,1)=KPd
2D点到3D点的转换步骤如下:
1)给定图像上一点 p = ( u , v , 1 ) p=(u, v, 1) p=(u,v,1), 先将其转换到归一化焦平面上 P d = ( x d , y d , 1 ) = K − 1 p P_d=(x_d,y_d,1)=K^{-1}p Pd=(xd,yd,1)=K−1p。
2)进行去畸变,得到对应的无畸变的点 P u = ( x , y , 1 ) P_u=(x,y,1) Pu=(x,y,1)。
3)如果知道当前点的深度值d,那么就可以将点投影到相机坐标系下 P c = d p u P_c=dp_u Pc=dpu,否则就只能到此。
4)通过投影矩阵,将相机坐标系下的点转换到世界坐标系下 P w = R − 1 ( P c − t ) P_w=R^{-1}(P_c-t) Pw=R−1(Pc−t)。
多数情况下,都用BA算法来计算相机的内外参,这就需要知道雅可比矩阵,即投影误差对各待优化变量的偏导数组成的矩阵。所谓的投影误差,实际检测到点与投影到图像上的点之间的误差
e r r = p m − p err=p_m - p err=pm−p
其中 p m p_m pm表示检测到的角点。为了避免手撕公式,我使用了matlab直接来推导出结果,并且在推导公式时,没有考虑畸变项,因为公式太长了,懒得敲。
代码:
syms fx fy x y z cx cy k1 k2 k3 p1 p2
x_u = x / z;
y_u = y / z;
r2 = x_u^2+y_u^2;
r4 = r2^2;
r6 = r2^3;
%x_d = x_u * (1 + k1 * r2 + k2 * r4 + k3 * r6) + 2 * p1 * x_u * y_u + p2 * (r2 + 2 * x_u^2);
%y_d = y_u * (1 + k1 * r2 + k2 * r4 + k3 * r6) + 2 * p2 * x_u * y_u + p1 * (r2 + 2 * y_u^2);
x_d = x_u;
y_d = y_u;
u = fx * x_d + cx;
v = fy * y_d + cy;
alphaE_alphaK = - [diff(u, fx), diff(u, fy), diff(u, cx), diff(u, cy);
diff(v, fx), diff(v, fy), diff(v, cx), diff(v, cy)]
alphaE_alphaP = -[diff(u, x), diff(u, y), diff(u, z);
diff(v, x), diff(v, y), diff(v, z)]
alphaP_alphaR = [1, 0, 0, 0, z, -y;
0, 1, 0, -z, 0, x;
0, 0, 1, y, -x, 0]
alphaE_alphaP * alphaP_alphaR
∂ e r r ∂ δ ξ = ∂ e r r ∂ P c ∂ P c ∂ δ ξ { {\partial err} \over {\partial \delta \xi }} = { {\partial err} \over {\partial {P_c}}}{ {\partial {P_{\rm{c}}}} \over {\partial \delta \xi }} ∂δξ∂err=∂Pc∂err∂δξ∂Pc
其中, ∂ P c ∂ δ ξ { {\partial {P_{\rm{c}}}} \over {\partial \delta \xi }} ∂δξ∂Pc的推导后续会有专门篇幅进行总结,在这个先用起来再说。