天天看点

畸变模型的求解畸变模型求解

畸变模型求解

畸变模型

在上一篇博客,我提到了我们用的畸变模型大多数是布朗模型的一种近似(当然还存在很多畸变模型,如鱼眼模型、根据镜头特点提出的模型等等)。

同时,在上一篇博客也指出了以下两种模型其实是等价的:

x u n d i s t = f ( x d i s t , y d i s t ) (1) x_{undist} = f(x_{dist},y_{dist}) \tag{1} xundist​=f(xdist​,ydist​)(1)

x d i s t = g ( x u n d i s t , y u n d i s t ) (2) x_{dist} = g(x_{undist},y_{undist}) \tag{2} xdist​=g(xundist​,yundist​)(2)

对于opencv,其采用的是第二种,原因是利用反向映射进行去畸变,一方面避免求解上述二元高次方程,另一方面可以避免第一种模型去畸变后产生“奶酪图”。

那么,如果我们要想求解第二种模型的方程,应该怎么做呢?

在opencv中给出了一种迭代法求解的方法,且收敛速度非常快。答案就在函数

undistortPoints()

中。

undistortPoints()

对于一些场景,我们并不需要对整幅图进行去畸变。比如对于光流法中我们跟踪的一些点(a sparse set of points),我们只需要对这些点进行畸变矫正即可,相比全图去畸变,节约算力。

同时,该函数还是

projectPoints

的逆变换。因为投影时,3D点经过刚体变换后的坐标就是理想的坐标,经过透镜后的坐标才是畸变后的坐标,进而通过乘以内参矩阵,得到像素点坐标。

对于给定的畸变点 ( u , v ) (u,v) (u,v),计算其去畸变后的坐标点 ( x ′ ′ , y ′ ′ ) (x^{''},y^{''}) (x′′,y′′),主要步骤如下:

1、根据相机内参,将坐标转化到相机坐标系下,即

畸变模型的求解畸变模型求解

2、迭代求解畸变模型,得到去畸变后的坐标,即

畸变模型的求解畸变模型求解

3、再根据相机内参,将坐标系转化为像素坐标系,得到去畸变后的像素坐标,即

畸变模型的求解畸变模型求解

之所以要经过上述一圈,是因为我们在相机坐标系下,利用透镜前(无畸变)和透镜后(畸变)对应的坐标点进行优化得到畸变系数。

迭代法求解非线性方程组

对于相机坐标系下的畸变坐标 ( x d i s t , y d i s t ) (x_{dist},y_{dist}) (xdist​,ydist​),我们有如下畸变方程:

x d i s t = x ∗ 1 + k 1 ∗ r 2 + k 2 ∗ r 4 + k 3 ∗ r 6 1 + k 4 ∗ r 2 + k 5 ∗ r 4 + k 6 ∗ r 6 + p 1 ∗ ( 2 ∗ x ∗ y ) + p 2 ∗ ( r 2 + 2 ∗ x 2 ) + s 1 ∗ r 2 + s 2 ∗ r 4 (3) x_{dist} = x * \frac{1 + k1 * r^2 + k2 * r^4 + k3 * r^6}{1 + k4* r^2 + k5 * r^4 + k6 * r^6} + p1 * (2*x*y)+p2*(r^2+2*x^2)+s1*r^2+s2*r^4 \tag{3} xdist​=x∗1+k4∗r2+k5∗r4+k6∗r61+k1∗r2+k2∗r4+k3∗r6​+p1∗(2∗x∗y)+p2∗(r2+2∗x2)+s1∗r2+s2∗r4(3)

简化一下上式,令:

A = 1 + k 1 ∗ r 2 + k 2 ∗ r 4 + k 3 ∗ r 6 1 + k 4 ∗ r 2 + k 5 ∗ r 4 + k 6 ∗ r 6 B = p 1 ∗ ( 2 ∗ x ∗ y ) + p 2 ∗ ( r 2 + 2 ∗ x 2 ) + s 1 ∗ r 2 + s 2 ∗ r 4 C = p 1 ∗ ( r 2 + 2 ∗ y 2 ) + p 2 ∗ ( 2 ∗ x ∗ y ) + s 3 ∗ r 2 + s 4 ∗ r 4 (4) A = \frac{1 + k1 * r^2 + k2 * r^4 + k3 * r^6}{1 + k4* r^2 + k5 * r^4 + k6 * r^6} \\ B = p1 * (2*x*y)+p2*(r^2+2*x^2)+s1*r^2+s2*r^4 \\ C = p1 * (r^2+2*y^2)+p2*(2*x*y)+s3*r^2+s4*r^4 \tag{4} A=1+k4∗r2+k5∗r4+k6∗r61+k1∗r2+k2∗r4+k3∗r6​B=p1∗(2∗x∗y)+p2∗(r2+2∗x2)+s1∗r2+s2∗r4C=p1∗(r2+2∗y2)+p2∗(2∗x∗y)+s3∗r2+s4∗r4(4)

那么,畸变方程简化为:

x d i s t = x ∗ A + B (5) x_{dist} = x * A + B \tag{5} xdist​=x∗A+B(5)

迭代法求解非线性方程组,首先就是要分离出 x x x和 y y y,即

x = ( x d i s t − B ) / A y = ( y d i s t − C ) / A (6) x = (x_{dist} - B) / A \\ y = (y_{dist} - C) / A \tag{6} x=(xdist​−B)/Ay=(ydist​−C)/A(6)

这相当于我们利用畸变坐标,得到了去畸变后的坐标位置。但是,这个结果并不足够准确,需要进行多次迭代。

既然需要迭代进行求解,我们就需要设定迭代结束条件,即:

  • 满足残差
  • 达到最大迭代次数

残差

每一次迭代,我们通过公式(6)得到了“去畸变后的坐标位置”。

那么,将其带入到公式(2),我们就可以得到新的”畸变坐标位置“。

再借助相机内参,将其转化到像素坐标系下,这样我们就可以计算和真实的畸变坐标位置的欧氏距离作为最终的error,也就是重投影误差。

代码

void undistorPointCore(Point2f& point)
{
    // point 畸变点坐标
    cv::TermCriteria criteria = cv::TermCriteria(cv::TermCriteria::EPS, 50, 0.0001);
    double k[3] = { 8.754e-05,3.669e-07,-1.36e-07 }; // x
    //double k[3] = { 0.0001003,-6.175e-07,-1.186e-07 }; // y

    double xu, yu, xd = 0, yd = 0;
    xd = xu = point.x;
    yd = yu = point.y;

    double error = std::numeric_limits<double>::max();

    for (int j = 0; ; j++)
    {
		if ((criteria.type & cv::TermCriteria::COUNT) && j >= criteria.maxCount)
		{
			//cout << __LINE__ << " exceed max iterative num" << endl;
			break;
		}
		if ((criteria.type & cv::TermCriteria::EPS) && error < criteria.epsilon)
		{
			//cout << __LINE__ << " reach residual error!" << endl;
			break;
		}
        double r2 = xu * xu + yu * yu;
        double icdist = 1 + ((k[2] * r2 + k[1]) * r2 + k[0]) * r2;
        if (icdist < 0)  
        {
            xu = point.x;
            yu = point.y;
            break;
        }
        // xu yu 可以认为是去畸变后的坐标位置
        // xd yd 可以认为是畸变前的坐标位置
        // xd = xu(1+k1*rd^2+k2*rd^4+k3*rd^6)
        // rd = sqrt(xu * xu + yu * yu)
        xu = xd / icdist;
        yu = yd / icdist;

        // 然后再对上述去畸变后的坐标再加畸变,然后再转换到像素坐标系,并计算误差
        if (criteria.type & cv::TermCriteria::EPS)
        {
            double r4, r6, cdist;
            double xu0, yu0;

            r2 = xu * xu + yu * yu;
            r4 = r2 * r2;
            r6 = r4 * r2;
            cdist = 1 + k[0] * r2 + k[1] * r4 + k[2] * r6;
            xd0 = xu * cdist;
            yd0 = yu * cdist;

            error = sqrt(pow(xd0 - point.x, 2) + pow(yd0 - point.y, 2));
			//cout << j << " " << error << endl;
        }
    }

	cout << point.x << " " << point.y << " " << xu << " " << yu << endl;
	point.x = xu;
	point.y = yu;
    // point 去畸变点坐标
}
           

opencv中

undistortPoints()

在内部调用

cvUndistortPointsInternal()

,上述代码做了一些精简,比如:

  • 舍弃了大量的类型检查,以及Mat与cvMat之间的转化
  • 将畸变模型简化,只考虑三阶径向模型

这种方法在去畸变的时候,收敛速度非常快,一般几次就能得到去畸变后的坐标位置。opencv中提到,默认迭代次数为5次。

Default version of undistortPoints does 5 iterations to compute undistorted points

beacuse, criteria = cv::TermCriteria(cv::TermCriteria::EPS, 5, 0.01);

最后

刚开始在做反投影的时候,卡在了畸变方程的求解。于是乎,就在网上找了一个牛顿法求解非线性方程组的demo。相比牛顿法去求解,opencv中给出的这种方法,直观、收敛速度快、精度比较高,且实现起来比较容易(牛顿法需要求雅可比矩阵和海森矩阵)。

继续阅读