这一讲主要内容就是了解摄像机的成像模型以及OpenCV的使用。
1.四种坐标系
坐标系 | 基本描述 |
世界坐标系 | 因为摄像机和物体可以随便摆放在空间中的任何位置,所以我们必须用一个固定的坐标系来描述空间中任何物体的位置和摄像机的位置和朝向,这个基准坐标系我们称之为世界坐标系。在计算机视觉中,我们通常把世界坐标系定义为摄像机坐标系或者所观测的物体的中心。 |
摄像机坐标系 | 摄像机坐标系的原点是摄像机的光心,X、Y轴分别平行于图像坐标系的x、y轴,Z轴与摄像机的主光轴重合。 |
图像像素坐标系 | 我们以标准电视信号的形式将摄像机采集的图像输入计算机,经过数模转换器转换后该图像变为数字图像。在计算机内每幅数字图像都是以数组形式存储,数组每个元素的值是该图像点的灰度值。定义直角坐标系,图像平面的左上角为原点,用(u, v) 来表示每一像素的坐标,是指该像素在数组中的列数和行数。单位是像素 |
图像物理坐标系 | 因为图像像素坐标只表示了图像点的像素在数组中的列数和行数,并没有用物理单位了表示该像素在坐标系中的位置,所以我们建立了用物理尺寸为单位(如毫米)来表示的坐标系,称之为图像物理坐标系。原点 (图像主点)位于图像平面与摄像机光轴的交点处,以毫米为单位。 |
各个坐标系示意图:
各个坐标系间转换关系如下图:
数学模型:
2.图像去畸变
书中介绍了图像的径向畸变和切向畸变的模型,同时提出了两种去畸变的方法,并给出了第一种的代码实现,效果如图(上图为去畸变前,下图为去畸变后):
效果非常明显,简单来说就是:该直的不弯了。。。但是缺点也很明显,图像边界有一部分信息在去畸变的过程中丢失了,这是所使用的去畸变算法导致的。第一次看示例代码,有点困惑,和我预期的去畸变方法不太一样,先简述一下他的思路:读取图像后,定义一个和原图像尺寸相同的空图像,遍历这个空图像每一个像素,计算当前像素对应在归一化平面的位置,此位置在经过成像畸变后对应的像素坐标,将原图中这个坐标的值赋到当前像素下。但这个过程中必然存在一个问题,就是原图像中有一部分像素点去畸变后范围超出了原有的图像尺寸,因此出现了信息丢失。
文中提到的另一种方法:从原图像的像素出发,计算他在无畸变情况下的坐标,这种方法听上去更直观,同时可以确保图像信息不会丢失。但实践过程中出现了诸多问题,首先不可避免的存在原图像中有一部分像素点去畸变后范围超出了原有的图像尺寸,因此要保留所有信息,必须要扩充图像尺寸,增加一定的行和列(其实第一种去畸变方法同样适用)。此外,最关键的是,切向畸变模型中包含有xy项,因此难以求解。为了做一个简单的测试,我选择忽略切向畸变,仅消除径向畸变,并扩充一定的行和列以保留全部信息。初测结果如下图:
去除畸变的效果还是很明显,弯曲的直线都恢复了,边缘的信息也没有丢失。不过问题更明显:未去除的切向畸变很明显是枕型畸变,边缘的黑色是扩充后没有图像信息的部分,甚至出现了一部分割裂。同时,图像中存在黑色的条纹,主要是计算过程中对最终的坐标值简单的向下取整导致的。使用图像膨胀简单处理一下:
膨胀后的图像效果好了一些,条纹基本不见了,枕型畸变依然存在。以上的测试只是出于好奇,因为我个人第一时间想到的去畸变方法就是第二种,但实际操作下来,第一种方法还是有明显优势,而实际应用当中更多的会选择使用OpenCV中自带的去畸变函数,实际结果更接近第一种,但可以通过设置参数达到更好的效果,这里不再赘述了。
测试代码如下:只是在ch5/imageBasics/undistortImage.cpp源文件内添加了一部分内容
#include
#include
using namespace std;
string image_file = "/home/svv12138/Downloads/slambook2-master/ch5/imageBasics/distorted.png"; // 请确保路径正确
int main(int argc, char **argv) {
// 本程序实现去畸变部分的代码。尽管我们可以调用OpenCV的去畸变,但自己实现一遍有助于理解。
// 畸变参数
double k1 = -0.28340811, k2 = 0.07395907, p1 = 0.00019359, p2 = 1.76187114e-05;
// 内参
double fx = 458.654, fy = 457.296, cx = 367.215, cy = 248.375;
cv::Mat image = cv::imread(image_file, 0); // 图像是灰度图,CV_8UC1
int rows = image.rows, cols = image.cols;
cout<= 0 && v_distorted >= 0 && u_distorted < cols && v_distorted < rows) {
image_undistort.at(v, u) = image.at((int) v_distorted, (int) u_distorted);
} else {
image_undistort.at(v, u) = 0;
}
}
}
for (int v = 0; v < rows; v++)
{
for (int u = 0; u < cols; u++)
{
//计算当前像素畸变前的坐标
double x = (u - cx) / fx, y = (v - cy) / fy;
double r = sqrt(x * x + y * y);
double x_undistorted=x/(1 + k1 * r * r + k2 * r * r * r * r);
double y_undistorted=y/(1 + k1 * r * r + k2 * r * r * r * r);
double u_undistorted = fx * x_undistorted + cx;
double v_undistorted = fy * y_undistorted + cy;
//扩充后修改边界检测条件
if (u_undistorted >= -add_line/2 && v_undistorted >= -add_line/2 && u_undistorted < cols+add_line/2 && v_undistorted < rows+30/2) {
//简单取整赋值(最临近插值)
image_undistort2.at((int)v_undistorted+add_line/2, (int)u_undistorted+add_line/2) = image.at(v, u);
} else {
image_undistort2.at(v, u) = 0;
}
}
}
//膨胀
cv::Mat struct1=cv::getStructuringElement(0,cv::Size(3,3));
cv::dilate(image_undistort2,image_undistort_dilate,struct1);
//图像存储
cv::imwrite("1.jpeg",image_undistort);
cv::imwrite("2.jpeg",image_undistort2);
cv::imwrite("3.jpeg",image_undistort_dilate);
// 画图去畸变后图像
cv::imshow("distorted", image);
cv::imshow("undistorted", image_undistort);
cv::imshow("undistorted2", image_undistort2);
cv::imshow("膨胀后", image_undistort_dilate);
cv::waitKey();
return 0;
}