视觉SLAM十四讲 读书编程笔记 Chapter5 相机与图像

相机模型

  1. 使用针孔和畸变两个模型来描述相机的整个投影过程,把外部的三维点投影到相机内部的成像平面,就构成了相机的内参数。

  2. 针孔相机模型
    在这里插入图片描述
    在相机坐标系下,设物点P坐标为[X,Y,Z]T,像点P’坐标为[X’,Y’,Z’]T,显然Z’=f,那么根据相似三角形,有:
    Z/f = -X/X’ = -Y/Y’
    这是初中物理的知识,其中负号表示像是倒着的,像的正倒对于CCD传感器没有任何实际意义,因此将负号去掉:
    Z/f = X/X’ = Y/Y’,整理得:X’=fX/Z Y’=fY/Z
    在这里插入图片描述

  3. 内参矩阵:像素坐标系与物理成像平面坐标系互相转换
    像素坐标系定义:原点位于左上角,u轴向右与x轴平行,v轴向下与y轴平行。
    像素坐标系与成像平面之间,相差了一个缩放和一个原点的平移
    设像素坐标在u轴上缩放了alpha倍,在v轴上缩放了beta倍原点平移了[cx,cy]T
    像点P’与像素坐标[u,v]T的关系为:
    u = alpha X’ + cx
    v = beta X’ + cy
    令fx = alpha f , fy = beta f ,那么:
    u = fxX/Z + cx
    v = fyY/Z + cy
    写成矩阵形式为:在这里插入图片描述
    按照传统习惯,将Z挪到左侧:
    在这里插入图片描述
    其中K就是相机的内参矩阵,它与焦距f,原点的平移[cx,cy]T,u轴上的缩放alpha,v轴上的缩放beta有关。

  4. 内参矩阵&外参矩阵:世界坐标系向像素坐标系的转换
    在这里插入图片描述
    这里的Pw=[Xw,Yw,Zw,1]T,是齐次坐标;
    TPw=[Xc,Yc,Zc,1]T,表示的也是齐次坐标;
    这里TPw隐含了一次齐次坐标向非其次坐标转换的过程,转换完之后,不区分记号Pc=TPw=[Xc,Yc,Zc]T
    如果将Pc也进行归一化,上图中式子两边同除Z(实质上是Zc),就变成了齐次坐标等式:Puv=KTPw ,
    此时Pc=[Xc/Zc,Yc/Zc,1]T,成为归一化坐标,它位于相机前方z=1处的平面上,该平面也被称为归一化平面

  5. 径向畸变
    径向畸变是由透镜形状引入的,分为桶形失真和枕形失真。
    设[x,y]T是归一化的平面点坐标,[xdistorted,ydistorted]T是畸变后点的坐标。
    xdistorted = x(1 + k1r2 + k2r4 + k3r6)
    ydistorted = y(1 + k1r2 + k2r4 + k3r6)
    畸变较小的图像中心区域,畸变纠正主要是k1在起作用;而对于畸变较大的边缘区域,主要是k2在起作用。普通相机用这两个系数就可以很好的纠正径向畸变,对于畸变很大的摄像头,比如鱼眼镜头,可以加入k3畸变项对径向畸变进行纠正。

  6. 切向畸变
    切向畸变是由于透镜和成像平面不严格平行而引入的。
    在这里插入图片描述
    切向畸变可以用两个参数p1,p2来进行纠正:
    xdistorted = x + 2p1xy + p2(r2+2x2)
    ydistorted = y + 2p2xy + p1(r2+2y2)

  7. 径向畸变和切向畸变共同作用
    假设相机坐标系中有一点P(X,Y,Z),通过径向畸变和切向畸变的五个系数,可以找到这个点在像素平面上的正确位置:
    (1)将相机坐标系下的三维点投影到归一化图像平面。设它的归一化坐标为[x,y]T.
    (2)对归一化平面上的点进行径向畸变和切向畸变纠正。给定归一化坐标,可以求出原始图像上的坐标
    xdistorted = x(1 + k1r2 + k2r4 + k3r6) + 2p1xy + p2(r2+2x2)
    ydistorted = y(1 + k1r2 + k2r4 + k3r6) + 2p2xy + p1(r2+2y2)
    (3)将纠正后的点通过内参数矩阵投影到像素平面,得到该点在图像上的正确位置。
    u = fxxdistorted + cx
    v = fyydistorted + cy

实际使用中,存在两种去畸变处理方法。
方法一:先对整张图像进行去畸变处理,得到去畸变之后的图像,然后用针孔模型讨论空间点的投影问题。
方法二:考虑图像中的某个点,按照去畸变的方程,讨论其去畸变后的空间位置。

双目相机原理简介

在这里插入图片描述
如图所示,左眼像素PL(uL,vL),右眼像素PR(uR,vR).
假设左眼像素和右眼像素的v是相同的,即相机只在水平方向发生旋转或平移。那么沿着x方向OLPL的距离为uL - cx,沿着x方向ORPR的距离为cx - uR
根据三角形PPLPR和三角形POLOR的相似关系,有:
(z-f)/z = (b-(uL-cx)-(cx-uR))/b
(z-f)/z = (b-(uL-uR))/b
设视差d=uL-uR,那么:
(z-f)/z = (b-d)/b
整理得:
z = fb/d
可见,视差与距离成反比:视差越大,距离越近。
而又因为视差最小为一个像素,所以,双目深度存在一个极限值,由fb决定。fb越大,设备能测到的距离就越远。

实践:图像的存取与访问

  1. 安装opencv的依赖
sudo apt-get install build-essential libgtk2.0-dev libvtk5-dev libjpeg-dev libtiff4-dev libjasper-dev libopenexr-dev libtbb-dev

在ubuntu16.04上出现报错
在这里插入图片描述
按照提示改成:

sudo apt-get install build-essential libgtk2.0-dev libvtk5-dev libjpeg-dev libtiff5-dev libjasper-dev libopenexr-dev libtbb-dev
  1. opencv源码编译
    opencv本身也是一个cmake工程,make之后,要调用如下命令将其安装在机器上
sudo make install
  1. 一些问题说明
    我下载的opencv源码版本为opencv-3.3.0.zip,使用的操作系统是ubuntu16.04
    在执行make时,出现如下报错:
    .
    在这里插入图片描述解决办法如下:
sudo apt-get autoremove libtiff5-dev
sudo apt-get install libtiff5-dev

然后再make就可以成功了。

代码:
imageBasics,cpp

//2019.08.07
#include <iostream>
#include <chrono>
using namespace std;

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>

int main(int argc, char** argv)
{
	//读取argv[1]指定的图像
	cv::Mat image;
	image = cv::imread(argv[1]);//cv::imread函数读取指定路径下的图像
	//判断图像文件是否正确读取
	if(image.data == nullptr)//数据不存在,可能是文件不存在
	{
		cerr << "文件" << argv[1] << "不存在." << endl;
		return 0;
	}
	
	//若文件读取顺利,首先输出一些基本信息
	cout<<"图像宽为"<<image.cols<<", 高为"<<image.rows<<", 通道数为"<<image.channels()<<endl;
	cv::imshow("image",image);//用cv::imshow()显示图像
	//判断image的类型
	if(image.type() != CV_8UC1 && image.type() != CV_8UC3)
	{
		//图像类型不符合要求
		cout << "请输入一张彩色图或灰度图."<<endl;
		return 0;
	}
	
	//遍历图像
	//使用std::chrono来给算法计时
	chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
	for( size_t y=0; y<image.rows;y++)
	{
		for( size_t x=0;x<image.cols;x++)
		{
			//访问位于x,y处的像素
			//用cv::Mat::ptr获得图像的行指针
			unsigned char* row_ptr = image.ptr<unsigned char>(y);//row_ptr是第y行的头指针
			unsigned char* data_ptr = &row_ptr[x*image.channels()];//data_ptr指向待访问的像素数据
			//输出该像素的每个通道,如果是灰度图就只有一个通道
			for(int c=0; c != image.channels();c++)
			{
				unsigned char data = data_ptr[c];//data为I(x,y)第c个通道的值
			}
		}
	}
	chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
	chrono::duration<double> time_used = chrono::duration_cast<chrono::duration<double>>(t2-t1);
	cout <<"遍历图像用时:"<<time_used.count()<<"秒。"<<endl;
	
	
	//关于cv::Mat的拷贝
	//直接赋值并不会复制数据
	cv::Mat image_another = image;
	//修改image_another会导致image发生变化
	image_another(cv::Rect(0,0,100,100)).setTo(0);//将左上角100×100的块置零
	cv::imshow("image",image);
	cv::waitKey(0);
	
	//使用clone函数来复制数据
	cv::Mat image_clone = image.clone();
	image_clone(cv::Rect(0,0,100,100)).setTo(255);
	cv::imshow("image",image);
	cv::imshow("image_clone",image_clone);
	cv::waitKey(0);

	//销毁掉所有窗口
	cv::destroyAllWindows();
	return 0;


}

CMakeLists.txt

#2019.08.07
# 添加C++标准文件
set(CMAKE_CXX_FLAGS "-std=c++11")

#寻找opencv库
find_package(OpenCV REQUIRED)

#添加头文件
include_directories(${OpenCV_INCLUDE_DIRS})


add_executable(imageBasics imageBasics.cpp)

#链接OpenCV库
target_link_libraries(imageBasics ${OpenCV_LIBS})

运行结果:
在这里插入图片描述
在这里插入图片描述

实践:点云拼接

安装PCL

PCL安装命令

sudo add-apt-repository ppa:v-launchpad-jochen-sprickerhof-de/pcl
sudo apt-get update
sudo apt-get install libpcl-all

在ubuntu16.04上运行报错,原因是源被移除。

解决办法:下载PCL源码,手动编译安装,步骤如下
(1)从github上下载pcl源码,下载地址为https://github.com/PointCloudLibrary/pcl
下载完成后,得到pcl-master.zip文件,解压

(2)安装相应依赖库

    sudo apt-get update
    sudo apt-get install git build-essential linux-libc-dev
    sudo apt-get install cmake cmake-gui 
    sudo apt-get install libusb-1.0-0-dev libusb-dev libudev-dev
    sudo apt-get install mpi-default-dev openmpi-bin openmpi-common  
    sudo apt-get install libflann1.8 libflann-dev
    sudo apt-get install libeigen3-dev
    sudo apt-get install libboost-all-dev
    sudo apt-get install libvtk5.10-qt4 libvtk5.10 libvtk5-dev
    sudo apt-get install libqhull* libgtest-dev
    sudo apt-get install freeglut3-dev pkg-config
    sudo apt-get install libxmu-dev libxi-dev 
    sudo apt-get install mono-complete
    sudo apt-get install qt-sdk openjdk-8-jdk openjdk-8-jre

(2)在pcl-master文件夹下新建文件夹build
(3)在build文件夹下编译

cmake  -DCMAKE_BUILD_TYPE=None  -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_GPU=ON  -DBUILD_apps=ON  -DBUILD_examples=ON  -DCMAKE_INSTALL_PREFIX=/usr ..
make -j2  
sudo make install  

(4)可选,PCL可视化相关包安装

sudo apt-get install libopenni-dev 
sudo apt-get install libopenni2-dev

大功告成!

数据说明

在slambook/ch5/joinMap中准备了5对图像,在color/下有1.png到5.png共5张RGB图,而在depth/下有5张对应的深度图。同时,pose.txt文件给出了5张图像相机位姿(以TWC形式)。位姿记录的形式是平移向量加旋转四元数:[x,y,z,qx,qy,qz,qw]

点云拼接

(1)根据内参计算一对RGB-D图像对应的点云。
(2)根据各张图的相机位姿,把点云加起来,组成地图。
完整代码:
joinMap.cpp

//2019.08.08
#include <iostream>
#include <fstream>
using namespace std;
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <Eigen/Geometry>
#include <boost/format.hpp>//for formating strings
#include <pcl/point_types.h>
#include <pcl/io/pcd_io.h>
//#include <pcl/visualization/pcl_visualizer.h>//注释掉,因为没有编译显示相关的包

int main()
{
	vector<cv::Mat> colorImgs, depthImgs;	//彩色图和深度图
	vector<Eigen::Isometry3d, Eigen::aligned_allocator<Eigen::Isometry3d>> poses;//相机位姿(欧式变换矩阵4×4)
	
	ifstream fin("../pose.txt");
	if(!fin)
	{
		cerr<<"请在有pose.txt的目录下运行此程序"<<endl;
		return 1;
	}

	//读取5组彩色图、深度图和相机位姿
	for(int i=0;i<5;i++)
	{
		boost::format fmt("../%s/%d.%s");//图像文件格式
		colorImgs.push_back(cv::imread((fmt%"color"%(i+1)%"png").str() ));
		depthImgs.push_back(cv::imread((fmt%"depth"%(i+1)%"pgm").str(), -1 ));//使用-1读取原始图像
		
		double data[7] = {0};
		for(auto& d:data)
			fin>>d;
		
		//构造位姿矩阵T		
		Eigen::Quaterniond q(data[6],data[3],data[4],data[5]);//文件中位姿的记录形式是[x,y,z,qx,qy,qz,qw],而Eigen构造的四元数要把实部放在最前面
		Eigen::Isometry3d T(q);//旋转用四元数表示
		T.pretranslate(Eigen::Vector3d(data[0],data[1],data[2]));//设置平移向量
		
		poses.push_back(T);
	}
	
	//计算点云并拼接
	//相机内参
	double cx = 325.5;
	double cy = 253.5;
	double fx = 518.0;
	double fy = 519.0;
	double depthScale = 1000.0;

	cout<<"正在将图像转换为点云..."<<endl;
	//定义点云使用的格式,这里用XYZRGB
	typedef pcl::PointXYZRGB PointT;
	typedef pcl::PointCloud<PointT> PointCloud;

	//新建一个点云
	PointCloud::Ptr pointCloud(new PointCloud);
	//遍历每一组数据
	for(int i=0;i<5;i++)
	{
		cout<<"转换图像中:"<<i+1<<endl;
		cv::Mat color = colorImgs[i];
		cv::Mat depth = depthImgs[i];
		Eigen::Isometry3d T = poses[i];
		//遍历每一个像素
		for(int v=0; v<color.rows;v++)
		{
			for(int u=0;u<color.cols;u++)
			{
				unsigned int d = depth.ptr<unsigned short>(v)[u];
				if(d==0) continue;//为0表示没有测量到深度
				Eigen::Vector3d point;
				point[2] = double(d)/depthScale;
				point[0] = (u-cx)*point[2]/fx;
				point[1] = (v-cy)*point[2]/fy;
				Eigen::Vector3d pointWorld = T*point;//这里的T表示的是Twc,是将相机坐标系转换为世界坐标系的外参矩阵

				PointT p;
				p.x = pointWorld[0];
				p.y = pointWorld[1];
				p.z = pointWorld[2];
				p.b = color.data[v*color.step+u*color.channels()];
				p.g = color.data[v*color.step+u*color.channels() + 1];
				p.r = color.data[v*color.step+u*color.channels() + 2];
				pointCloud->points.push_back(p);
				
			}
		}
	}


	pointCloud->is_dense = false;
	cout <<"点云共有"<<pointCloud->size()<<"个点."<<endl;
	pcl::io::savePCDFileBinary("map.pcd",*pointCloud);


	return 0;

}

值得注意的是,必须注释掉头文件的最后一行,因为在上面pcl的安装中没有编译显示相关的包。
//#include <pcl/visualization/pcl_visualizer.h>
CMakeLists.txt

#2019.08.29
cmake_minimum_required( VERSION 2.8 )
project( joinMap )

set( CMAKE_BUILD_TYPE Release )
set( CMAKE_CXX_FLAGS "-std=c++11 -O3" )

# opencv 
find_package( OpenCV REQUIRED )
include_directories( ${OpenCV_INCLUDE_DIRS} )

# eigen 
include_directories( "/usr/include/eigen3/" )

# pcl 
find_package( PCL REQUIRED COMPONENT common io )
include_directories( ${PCL_INCLUDE_DIRS} )
add_definitions( ${PCL_DEFINITIONS} )

add_executable( joinMap joinMap.cpp )
target_link_libraries( joinMap ${OpenCV_LIBS} ${PCL_LIBRARIES} )

运行结果:
编译运行之后,在build文件夹下会生成map.pcd文件。
然后,输入如下命令显示这个map.pcd文件:

pcl_viewer map.pcd

结果如下:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值