前言
记录了学习图像去畸变原理以及结合双线性插值完成去畸变编程
一、去畸变原理
前置知识——区分归一化坐标与像素坐标
相机成像一共有三个平面:真实世界(如果不考虑深度,可以将其看成一个平面)、相机成像平面(我理解就是相机中类似于胶卷的部分)、像素平面(就是电脑中看到的像素格子组成的平面)。去畸变公式是建立在归一化坐标平面上的,有必要了解归一化成像平面到像素坐标的映射关系。
其中 X Z \frac{X}{Z} ZX代表的是由真实世界到归一化成像平面的归一化坐标。 f x 、 f y 、 c x 、 c y f_x、f_y、c_x、c_y fx、fy、cx、cy代表的是相机的内参,因为从归一化平面到像素坐标需要有一个采样的过程,这个过程会涉及到像素的缩放和平移,内参描述的就是采样过程的缩放与平移关系。
去畸变公式
去畸变公式建立在归一化平面上,这里直接搬十四讲的公式:
其中
x
、
y
x、y
x、y代表的是去畸变之后的图像像素在归一化坐标上的坐标值,
r
r
r代表的是在归一化平面上去畸变之后的图像像素离中心点的距离,也就是
r
2
=
x
2
+
y
2
r^2=x^2+y^2
r2=x2+y2,由此计算出在归一化平面上对应未去畸变图像(也就是原图)的坐标值,之后再通过内参找到对应的像素的像素值,完成一个像素的去畸变。
二、去畸变代码实现
因为去畸变要求有相机内参,这里直接用高翔十四讲的图片以及内参
#pragma once
#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
#include"Bilinear_Interpolation.h"//这是自己写的双线性插值方法的头文件
#include <iostream>
using namespace std;
using namespace cv;
Mat &undisorted(const Mat &initialImage,const vector<double> intrinsic,const vector<double> disort) {
Mat temp;
initialImage.copyTo(temp);
if (intrinsic.size()+disort.size()<8) {
printf("%d parameters received, but 8 parameters needed! disorted failed, return original Image\n", intrinsic.size() + disort.size());
return temp;
}
//double intrinisic[4] = { 458.654,457.296,367.215,248.375 };//double intrinsic[4]={fx,fy,cx,cy};
//double disort[4] = { -0.28340811,0.07395907,0.00019359,1.76187114e-05 };//double disort[4]={k1,k2,p1,p2};
Mat disortedImage = Mat(initialImage.rows, initialImage.cols, initialImage.type());
Mat nearestInterImage = Mat(initialImage.rows, initialImage.cols, initialImage.type());
for (int v = 0; v < initialImage.rows; v++) {
for (int u = 0; u < initialImage.cols; u++) {
double X_ = (u - intrinsic.at(2)) / intrinsic.at(0), Y_ = (v - intrinsic.at(3)) / intrinsic.at(1);
//归一化平面的坐标
double R = sqrt(X_*X_+Y_*Y_);
double X_disorted = X_ * (1 + disort.at(0) * R * R + disort.at(1) * R * R * R * R) + 2 * disort.at(2) * X_ * Y_ + disort.at(3) * (R * R + 2 * X_ * X_);
double Y_disorted = Y_ * (1 + disort.at(0) * R * R + disort.at(1) * R * R * R * R) + 2 * disort.at(2) * (R*R+2*Y_ * Y_) + 2*disort.at(3) * X_*Y_;
//去畸变
double u_disorted = intrinsic.at(0) * X_disorted + intrinsic.at(2);
double v_disorted = intrinsic.at(1) * Y_disorted + intrinsic.at(3);
//重新映射到像素平面
//双线性插值计算去畸变的像素值
disortedImage.at<uchar>(v, u) = Bilinear_Inter(u_disorted,v_disorted,initialImage);
//邻域插值
if (u_disorted >= 0 && v_disorted >= 0 && u_disorted < initialImage.cols && v_disorted < initialImage.rows) {
nearestInterImage.at<uchar>(v, u) = initialImage.at<uchar>((int)v_disorted, (int)u_disorted);
}
else {
nearestInterImage.at<uchar>(v, u) = 0;
}
}
}
cv::imshow("nearestInterpolation",nearestInterImage);
cv::imshow("bilinearInterpolation",disortedImage);
cv::imshow("initialImage", initialImage);
cv::imwrite("nearestInterpolation.png",nearestInterImage);
cv::imwrite("bilinearInterpolation.png",disortedImage);
waitKey(0);
return disortedImage;
}
三、双线性插值
这里只提一下双线性插值和邻域插值,不提其他更高阶的插值算法。
为什么要有插值算法
前面说到去畸变,说的是让去畸变之后的图像像素与原图像像素一一对应赋值这样一件事。比如说在待去畸变图像坐标上有一个点 ( 0 , 0 ) (0,0) (0,0),对应于原图像中点 ( 50 , 70 ) (50,70) (50,70),那么很简单,只需要:
distortedImage.at<uchar>(0,0)=initialImage.at<uchar>(50,70);
但是实际上有时候 ( 0 , 0 ) (0,0) (0,0)经过畸变参数计算可能对应到 ( 50.37 , 70 , 87 ) (50.37,70,87) (50.37,70,87),很显然坐标值不是整数,上面直接赋值就做不到了,因此需要插值算法来计算这个不存在的像素值是多少。
邻域插值
一个想法很简单,那就是直接取整,比如令 ( 50.37 , 70.87 ) (50.37,70.87) (50.37,70.87)的像素值等于 ( 50 , 70 ) (50,70) (50,70)点的像素值,这个方法简单,但是会有缺点,就是可能会使得图像线条出现阶梯状,这个可以在后面体现。
双线性插值
这个方法会考虑到像素值周围所有像素值对它的贡献,也就是一个权重的概念,一个很自然的想法就是像素点离它越近,对它的像素值贡献就越大。
对于双线性插值,可以这样理解,首先先算出图中两个蓝色点的像素值,两个蓝色点的坐标应该是
(
50.37
,
70
)
、
(
50.37
,
71
)
(50.37,70)、(50.37,71)
(50.37,70)、(50.37,71)。对于上面的蓝色点,其像素值应该等于
A
×
r
2
+
B
×
r
2
A\times r_2+B\times r_2
A×r2+B×r2,因为A离蓝色点比较近,它的贡献比较大,所以要乘以
r
2
r_2
r2而不是
r
1
r_1
r1。这样算出两个蓝色的像素值之后,同理可以在竖直方向上把我们要算的点
(
50.37
,
70.87
)
(50.37,70.87)
(50.37,70.87)的像素值计算出来。
#pragma once
#include<iostream>
#include<opencv2/core/core.hpp>
using namespace std;
using namespace cv;
uchar Bilinear_Inter(double u,double v,const Mat &image) {
if((int)u>=0&&(int)u<=image.cols-2)
if ((int)v >= 0 && (int)v <= image.rows - 2) {
uchar
LT = image.at<uchar>((int)v, (int)u),
LB=image.at<uchar>((int)v+1, (int)u),
RT=image.at<uchar>((int)v, (int)u+1),
RB=image.at<uchar>((int)v+1, (int)u+1);
double dLx, dTy, dRx, dBy;
dLx = u-(int)u;
dRx = (int)u-u+1;
dTy = v - (int)v;
dBy = (int)v - v + 1;
uchar TP = dLx * RT + dRx * LT;
uchar TB = dLx * RB + dRx * LB;
return TP * dBy + TB * dTy;
}
else
return 0;
}
原图
去畸变邻域插值结果
去畸变双线性插值结果
可以看到邻域插值中对一些类似于线条的部分会出现阶梯状的变化,这个就是邻域插值的缺陷,如果把插值的阶次进一步提高,可以预想插值的结果会非常匹配原图像