第四更,双目相机标定实践(完整过程)

本文主要是在前两篇博客的基础上做的更新,对双目摄像机做标定,并用作测量。理论基础知识请参考:

《相机参数标定(camera calibration)及标定结果如何使用》

https://blog.csdn.net/Aoulun/article/details/78768570

《第二更,相机参数标定基础:从小孔成像开始到单双目标定》

https://blog.csdn.net/Aoulun/article/details/104780993

关于单目相机标定的具体实践,可以参考我的另外一篇博客

《第三更,单目相机标定实践(完整过程)》

https://blog.csdn.net/Aoulun/article/details/104789585

 

本文所有代码和图片均已上传,可正常使用,关注公众号下载。如果您觉得本文对您帮助很大可关注公众号分享,或者打赏作者,您的支持是我持续创作的动力。

 

我们隔壁村是出名的高科技应用示范村,远近闻名。比如说,在家家户户安装太阳能发电设备,不仅解决了居民供电问题,还把多余的电卖出去,创收。又比如说,最先引进大数据创新应用,以解决农民工就业问题。效果怎么样暂且不说,但是这种向前言看起的精神的就值得学习。话说回来,隔壁村的村主任姓周,名建国。平时去他们村串门,都是周主任喊话。这次周主任听说我修好了我们村口的摄像头,特地找到我,聊了聊他们村的科技引进项目。其中一个重大课题就是解决村里巡逻车的自动测距问题。测什么距离呢?测量房屋建筑距离路边的距离。为什么要测这个距离,其实主要还是为了美丽乡村建设,要有新的气象。周主任问我有没有兴趣接下来。做成了之后,将会把我的大名贴在村口显眼的位置,已告天下。我思考了0.8932秒,果断接下了这个活。我做这个的目的就是为了出名。

 

双目标定

一、实验条件

二、实验内容

三、实验步骤

四、实验代码及图片

五、实验结果

 

01实验条件

 

主要的实验器材都在下面这两幅图中了

主要的实验器材在如下清单中显示

 

02实验内容

 

主要的实验内容包括:

1.搭建整个实验平台,观察摄像机拍照情况

2.标定网络摄像机,求取相机参数

3.求取两个相机的位置关系参数

4.构建三维深度图像

5.向周主任汇报

 

03实验步骤

(3.1)搭建实验平台

首先,我按照某康网络相机拍照的基本流程,搭建了平台。

摄像机采用电源适配器通电,两个摄像机目视平行放置。

数据传输采用1米长网线跟笔记本电脑链接。

根据某康开放的API,自己搭建了一个相机连接采集MFC窗口,窗口具有预览功能。

 

点击预览按钮,左侧窗口可以实时预览我老家农家小院储物间的内景。

单击抓图按钮,保存图像。由于我在代码里把路径写死了,所以,没有配置路径的窗口。

(3.2)标定相机

标定板的实际尺寸参考如下图所示。实际长度是,每个单元格的长度是2厘米。

 

(3.2.1)拍摄标定图片

我从拍摄的所有图片中,挑选了20幅作为标定用图。左边相机产生的图如下所示

右边相机标定图如下所示

很明显,左右两个相机的分辨率是不一样的。

 

(3.2.2)图片预处理

就像我们在前一篇博客种介绍的那样《第三更,单目相机标定实践(完整过程)》,把左右两边的图像都同时缩放到相同的分辨率。

相应的,把待矫正图片也做尺寸缩小处理。

 

(3.2.3)标定参数求取

具体理论内容可以参考另外两篇博客

《相机参数标定(camera calibration)及标定结果如何使用》

《第二更,相机参数标定基础:从小孔成像开始到单双目标定》

这里只说一说具体的步骤,详细的代码会在第四章贴出来。

这一部分可以参考《第三更,单目相机标定实践(完整过程)》,里面有详细过程。本博客一会把详细代码贴出来。

(a)读取所有标定图

采用cv2.imread(img_path)方法,非常之简单。

 

(b)寻找角点

寻找角点,采用cv2.drawChessboardCorners(),也是现成的。大家可以看下我的找角点结果,非常完美。

先看左边

 

再看右边

 

(c)计算参数矩阵

我们带着问题来看计算过程。

问:为什么需要拍摄那么多图片,一张不行吗?

博主在另外两篇博客当中也提到,根据张征友标定法,只需要求得B矩阵和H矩阵就可以换算出参数矩阵。而B矩阵和H矩阵的求解是通过最大似然估计来优化得到的。详细可参考:

https://www-users.cs.umn.edu/~hspark/CSci5980/zhang.pdf

那么优化过程必然是需要多幅图像共同作用的。在上面的文献中,作者给出了多少图像是合适的,这里不再赘述。

将上面步骤产生的所有角点都组合起来,作为函数输入,可以实现求解:

cv2.calibrateCamera()

返回结果中,就包含内参和畸变参数。

 

另外,标定的过程,实际上就是一个矩阵关系的转换过程。什么意思呢?

举个例子,根据实际的标定板模样,我在电脑里假定了一个多长多宽的假想标定板图像,而标定的过程就是,所有拍摄到的图像中的标定板,都应该往我假定的标定板靠。如果对不上,那么就有系数上的关系,这个系数上的关系就是我们要标定的参数。

这个理解在程序中也有体现。

 

(d)求取双相机关系矩阵

根据我们在理论那一章的总结,这一步主要是求取相机之间的关系矩阵,包括,旋转R,平移T,本质矩阵E和基础矩阵F。

cv2.stereoCalibrate()

需要注意的是,里面的最后一个参数flag,当去不同的值,其结果也不一样。这不是本文的重点,可自行上网搜索。

 

(3.3)矫正及距离求解

在矫正和距离求解的时候,首先对图像做畸变矫正。这个画面很美,无法直视。不过放大了看,该水平的,还是水平了。我想主要是因为两个相机不一样,导致的吧,后续会深入研究一下。

 

 

(3.4)求取距离深度图

这里主要用到两个函数

cv2.StereoBM_create().compute()

这个函数主要是用作立体匹配,具体的匹配算法可以参考其他博客,本文主要偏重于实验。

cv2.reprojectImageTo3D()

这个函数主要是根据上面函数产生的匹配差异图,生成3维的深度图。而我们需要的就是Z轴的坐标。先看个效果

 

我也看不到这是个啥东西。

 

(3.5)向村支书汇报

看到上面的效果,我陷入了深思。我到底该如何向周主任汇报呢?

首先让我想到的是配一个激光测距仪。

方便实惠。

周主任万一懂技术怎么办?

我再次陷入了沉思。

 

04实验代码及图片

 

代码已经上传到了公众号,可以关注下载。

环境的搭建,可以参考另外的博客。我是在tensorflow的环境下跑的。

《深度学习完全攻略!(连载六:CUDA10.1+tensorflow+VS+anaconda3安装)》

博主已经将,所有的图片都上传到了公众号。大家可以下载自己实验研究。




import argparse
from argparse import RawTextHelpFormatter
import numpy as np
import cv2 



# 分别找左右两幅图的角点
def stereo_calib_find_corners(imgL, imgR, rlt_dir_L, rlt_dir_R, img_idx, col, row):
	grayL = cv2.cvtColor(imgL, cv2.COLOR_BGR2GRAY)
	retL, cornersL = cv2.findChessboardCorners(grayL,(col,row), None)
	print("retL = ", retL)
	#为了得到稍微精确一点的角点坐标,进一步对角点进行亚像素寻找
	corners2L = cv2.cornerSubPix(grayL, cornersL, (5,5), (-1, -1), (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 10, 0.001)) 
	
	if retL == True:
		#保存角点图像
		sav_pathL = rlt_dir_L + "\\" + str(img_idx) + "_corner.jpg"
		cv2.drawChessboardCorners(imgL, (col,row), corners2L, retL)
		cv2.imwrite(sav_pathL, imgL)
		
	grayR = cv2.cvtColor(imgR, cv2.COLOR_BGR2GRAY)
	retR, cornersR = cv2.findChessboardCorners(grayR,(col,row), None)
	print("retR = ", retR)
	#为了得到稍微精确一点的角点坐标,进一步对角点进行亚像素寻找
	corners2R = cv2.cornerSubPix(grayR, cornersR, (5, 5), (-1, -1), (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 10, 0.001)) 
	
	if retR == True:
		#保存角点图像
		sav_pathR = rlt_dir_R + "\\" + str(img_idx) + "_corner.jpg"
		cv2.drawChessboardCorners(imgR, (col,row), corners2R, retR)
		cv2.imwrite(sav_pathR, imgR)
	
	return (retL, corners2L, retR, corners2R)



def stereo_calib_calibrate(img_dir_L,img_dir_R,row_num,col_num,calib_img_num, square_sz):
	w = 1440
	h = 1080
	all_cornersL = []
	all_cornersR = []
	patterns = []
	#标定相机,先搞这么一个假想的板子,标定就是把图像中的板子往假想的板子上靠,靠的过程就是计算参数的过程
	#比如说(0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
	pattern_points = np.zeros((7*6, 3), np.float32)
	pattern_points[:, :2] = np.mgrid[0:7, 0:6].T.reshape(-1, 2)
	pattern_points *= square_sz
	
	for i in range(1,calib_img_num+1):
		img_pathL = img_dir_L + "\\" + str(i) + ".jpg"
		img_pathR = img_dir_R + "\\" + str(i) + ".jpg"
		print (img_pathL)
		print (img_pathR)
		#读取图像
		imgL = cv2.imread(img_pathL)
		imgR = cv2.imread(img_pathR)
		# 把图像搞成统一大小1440*1080
		imgLL=cv2.resize(imgL,(w,h))
		imgRR=cv2.resize(imgR,(w,h))
		#提取角点
		rlt_dir_L = img_dir_L + "\\corners"
		rlt_dir_R = img_dir_R + "\\corners"
		retl, cornersl, retr, cornersr = stereo_calib_find_corners(imgLL, imgRR, rlt_dir_L, rlt_dir_R, i, col_num, row_num)
		
		#合并所有角点
		all_cornersL.append(cornersl)
		all_cornersR.append(cornersr)
		patterns.append(pattern_points)
		
	rmsL, cameraMatrixL, distCoeffsL, rvecsL, tvecsL = cv2.calibrateCamera(patterns, all_cornersL, (w, h), None, None)
	rmsR, cameraMatrixR, distCoeffsR, rvecsR, tvecsR = cv2.calibrateCamera(patterns, all_cornersR, (w, h), None, None)
	
	#计算两个相机之间的关系矩阵模型
	flags = 0
	flags |= cv2.CALIB_FIX_FOCAL_LENGTH

	stereocalib_criteria = (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 1, 1e-5)
	ret, Ml, dl, Mr, dr, R, T, E, F = cv2.stereoCalibrate(  patterns, 
															all_cornersL,
															all_cornersR, 
															cameraMatrixL, 
															distCoeffsL, 
															cameraMatrixR,
															distCoeffsR, 
															(w, h),
															criteria=stereocalib_criteria, 
															flags=flags)

	print('Intrinsic_mtx_l', Ml)
	print('dist_l', dl)
	print('Intrinsic_mtx_r', Mr)
	print('dist_r', dr)
	print('R', R)
	print('T', T)
	print('E', E)
	print('F', F)

	camera_model = dict([('Ml', Ml), ('Mr', Mr), ('dl', dl),
						('dr', dr), ('rl', rvecsL),
						('rr', rvecsR), ('R', R), ('T', T),
						('E', E), ('F', F)])
	return camera_model
	
def stereo_calib_correct(crct_img_L_dir, crct_img_R_dir, corct_img_num, cameraModel, rlt_dir):
	w = 1440
	h = 1080
	for i in range(1,corct_img_num):
		img_pathL = crct_img_L_dir + "\\" + str(i) + ".jpg"
		img_pathR = crct_img_R_dir + "\\" + str(i) + ".jpg"
		print (img_pathL)
		print (img_pathR)
		#读取图像
		imgL = cv2.imread(img_pathL)
		imgR = cv2.imread(img_pathR)
		# 把图像搞成统一大小1440*1080
		imgLL=cv2.resize(imgL,(w,h))
		imgRR=cv2.resize(imgR,(w,h))
		grayL = cv2.cvtColor(imgLL, cv2.COLOR_BGR2GRAY)
		grayR = cv2.cvtColor(imgRR, cv2.COLOR_BGR2GRAY)
		
		#极线矫正,也就是把两幅图的极线搞成水平,就像在博客中描述的那样,把任意位置的像平面,搞成两个平行的像平面
		Rl, Rr, Pl, Pr, Q, validPixROIl, validPixROIr =  cv2.stereoRectify(cameraModel['Ml'], cameraModel['dl'], cameraModel['Mr'], cameraModel['dr'], (w,h), cameraModel['R'], cameraModel['T'])
		'''
		mapl_1, mapl_2 = cv2.initUndistortRectifyMap(cameraModel['Ml'], cameraModel['dl'], Rl, Pl, (w,h), cv2.CV_32FC1)
		mapr_1, mapr_2 = cv2.initUndistortRectifyMap(cameraModel['Mr'], cameraModel['dr'], Rr, Pr, (w,h), cv2.CV_32FC1)
		rltl = cv2.remap(grayL, mapl_1, mapl_2, cv2.INTER_LINEAR)
		rltr = cv2.remap(grayR, mapr_1, mapr_2, cv2.INTER_LINEAR)
		'''
		newcameramtxL,roiL = cv2.getOptimalNewCameraMatrix(cameraModel['Ml'], cameraModel['dl'],(w,h),1,(w,h))
		newcameramtxR,roiR = cv2.getOptimalNewCameraMatrix(cameraModel['Mr'], cameraModel['dr'],(w,h),1,(w,h))
		dstL = cv2.undistort(grayL, cameraModel['Ml'], cameraModel['dl'], None, newcameramtxL)
		dstR = cv2.undistort(grayR, cameraModel['Mr'], cameraModel['dr'], None, newcameramtxR)
		
		#保存极限图
		recPath = rlt_dir + "\\e_line.jpg"
		rlt = np.concatenate((dstL, dstR), axis=1)
		rlt[::40, :] = 0
		cv2.imwrite(recPath, rlt)
		
		#生成深度图
		stereo = cv2.StereoBM_create(numDisparities=16, blockSize=15)
		disparity = stereo.compute(grayL, grayR) #用原始图像
	 
		disp = cv2.normalize(disparity, disparity, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U)
		# 将图片扩展至3d空间中,其z方向的值则为当前的距离
		threeD = cv2.reprojectImageTo3D(disparity.astype(np.float32)/16., Q)
		cv2.imwrite(rlt_dir + "\\depth.jpg", disp)
		
	return 1




if __name__ == "__main__":
	parser = argparse.ArgumentParser(description="读取标定的图片并保存结果",formatter_class=RawTextHelpFormatter)
	parser.add_argument("--img_dir_L",help="左边相机标定图片路径",type=str,metavar='', default="D:\\newdata\\cam_L")
	parser.add_argument("--img_dir_R",help="右边相机标定图片路径",type=str,metavar='', default="D:\\newdata\\cam_R")
	parser.add_argument("--crct_img_L_dir",help="测试图像路径",type=str,metavar='',default="D:\\newdata\\corct_L")
	parser.add_argument("--crct_img_R_dir",help="测试图像路径",type=str,metavar='',default="D:\\newdata\\corct_R")
	parser.add_argument("--rlt_dir",help="测试图像路径",type=str,metavar='',default="D:\\newdata\\rlt")
	parser.add_argument("--row_num",help="每一行有多少个角点,边缘处的不算",type=int,metavar='',default="7")
	parser.add_argument("--col_num",help="每一列有多少个角点,边缘处的不算",type=int,metavar='',default="6")
	parser.add_argument("--calib_img_num",help="多少幅图像",type=int,metavar='',default="20")
	parser.add_argument("--corct_img_num",help="多少幅图像",type=int,metavar='',default="2")
	parser.add_argument("--square_sz",help="标定板放个尺寸,单位毫米",type=int,metavar='',default="20")

	args=parser.parse_args()
	
	# 标定相机
	cameraModel = stereo_calib_calibrate(args.img_dir_L,args.img_dir_R,args.row_num,args.col_num,args.calib_img_num, args.square_sz)
	
	#矫正图片
	ret = stereo_calib_correct(args.crct_img_L_dir, args.crct_img_R_dir, args.corct_img_num, cameraModel, args.rlt_dir)


05实验结果

 

这篇文章偏重于实现,并没有过多的深入研究。而网上很多关于双目标定的文章也大体上差不多,有些比本文做的更详细。

由于本文的实验素材的限制,并不能得到好的结果,但,大致流程是可以用的。如果您有兴趣,可以用自己的图片,直接跑实验即可。

 

关于标定,到这里就结束了。希望您理解,并知道怎么做了。但是,这里面有很多知识点需要掌握。如果可能,博主会继续考虑更新立体匹配的知识点。

 

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值