关于单目视觉中相机标定的一些理解
开场白
大江东去,浪淘尽,千古风流人物。。。是的,您猜的没错,今天我们不讲三国,讲讲单目视觉中3个坐标系的故事;
概要
- 坐标转换
- 畸变校正
- 总结
- 引用
坐标转换
话说天下大势,合久必分分久必合。事情从盘古开天辟地,不对是一个叫L.达盖尔的小伙子,这个小伙子骨骼精奇,成天吃饱喝足没啥事就折腾出了个叫做截图器的东西,快门在手,天下我有。虽然已经是两百多年前的事,但我们每天都用“单反穷三代 摄影毁一生”表达着对这位开山之人的无尽思念;
古语有云,有些人走了,却没想让我们也活着----是的,说的就是他,这个小伙子走了,却给我们留下3个坐标系,使我终日不得开心颜。这3个让头疼的玩意分别是:世界坐标系、相机坐标系、图像坐标系。他三的关系是这样的:
在我看来,他们是这么想的:
1. 世界坐标系:我是旁观者,你暗恋她我管不着,但是你的一点小动作我都看得到----用笛卡尔三维坐标系客观反应事物的位置;
2. 相机坐标系:当局者迷,旁观者清,我就喜欢图像坐标系,管他们怎么看----用相机的眼睛看世界,我是世界的中心,全世界都是我的;
3. 图像坐标系:我这么多愁善感,动不动就畸变,你们还是离我远点的好----图像像素位置的参考系,比如常见的以图像左上角所为像素原点;
坐标系转化
刚体变换:世界坐标系–>相机坐标系
旁观者(世界坐标系)问当事人(相机坐标系):她到底有啥好的呢,让你如此痴迷?
当事人说:只因为在人群中多看了她一眼。你过来,站我这个位置看看就知道了。
我们想把世界坐标系下的坐标转换到相机坐标下的坐标,如下图所示,可以通过刚体变换的方式。这个过程包括:旋转、跳跃,平移,so easy。
一个空间坐标系有6个自由度,原点坐标(x,y,z)以及3根轴的方向;喊着口号,我们先做旋转:
旋转结束后两个坐标系的3个坐标轴方向就一样了,统一思想,一心向党。接着把两个坐标系的原点重合在一起,步调统一,听党指挥,能打战,打胜战;总结起来是这样的:
透视变换:相机坐标系–>图像坐标系
旁观者(世界坐标系)凑了过来,当事人(相机坐标系)说:来,闭上一只眼睛,朝着那边看。
透视变换属于透视投影关系,从3D转换到2D。以坐标原点Oc作为透视中心,使用三角形比例关系进行长度转化;
我们已经将3维空间的事物转化到了2维图像中。花花世界是线性的,图像是离散的,所以还需要进行离散化操作,也就是像素化。上面图片反应的事最理想的状态,事实却不然,这个过程会发生畸变。畸变的事情我们稍等继续分解。转化结束后,遗留下一个问题,相机坐标系是中心对称的,也就是说得到的图像坐标也是中心对称的,不符合广大人民的价值观,所以还需要将坐标系原点从图像中央移到图像左上角,这个过程没什么特别的不详细说明了;
总结上面的所有过程是这样的,多么优雅的公式,非常完美。
插上一嘴,整个转化过程也可以分为两部分:外部转化、内部转化:
-
外部转化:世界坐标轴转为相机坐标系。所使用参数称为“外部参数”;
-
内部转化:从相机坐标系到最终离散图像获取的过程,包括畸变校正;所使用参数称为“内部参数”;
哈哈,陌上人如玉,君子世无双。
畸变校正
以上三者的关系也就讲的差不多了,但是姑娘所指的畸变是什么呢?这就得接着说一下畸变的故事了。由于本人水平有限,只能基于Halcon中的面阵相机畸变给大家大致介绍一下。
到目前接触的比较多的畸变有:径向畸变、切向畸变、薄棱镜畸变;
- 径向畸变
沿着透镜半径方向分布的畸变,产生原因是光线在原理透镜中心的地方比靠近中心的地方更加弯曲,这种畸变在普通廉价的镜头中表现更加明显,径向畸变主要包括桶形畸变和枕形畸变两种。
- 切向畸变
切向畸变主要由镜头安装导致,当透镜不完全平行于图像平面的时候产生切向畸变
- 薄棱镜畸变
薄棱镜畸变一般由镜头设计和加工安装误差导致,一般情况下,可忽略此畸变。
Halcon畸变校正
接下来,我们使用Halcon继续介绍。Halcon内将畸变分为面阵相机畸变和线扫相机畸变两大类型:
面阵相机畸变又可以细分为8类,分类情况请见下图,看着就非常复杂的样子。在使用Halcon做畸变校正时需要明确畸变类型。
上图中每个参数的含义请参考calibrate_cameras算子。
畸变校正步骤
请看具体代码:
(代码连接)
create_calib_data ('calibration_object', 1, 1, CalibDataID) //生成标定操作句柄
CalTabDescrFile := 'caltab_big.descr'
set_calib_data_calib_object (CalibDataID, 0, CalTabDescrFile) //明确标定板类型
StartCamPar := [0.008,0,0.0000086,0.0000086,384,288,768,576]
//明确畸变类型,初始化系统内部参数
set_calib_data_cam_param (CalibDataID, 0, 'area_scan_division', StartCamPar)
NumImages := 10
read_image (Images, 'calib/calib-3d-coord-' + [1:NumImages]$'02d')
for I := 1 to NumImages by 1
select_obj (Images, Image, I)
dev_display (Image)
Message := 'Find calibration plate in\nall calibration images (' + I + '/' + NumImages + ')'
disp_message (WindowHandle1, Message, 'window', 12, 12, 'black', 'true')
* Find the calibration plate
find_calib_object (Image, CalibDataID, 0, 0, I - 1, [], []) //寻找标定板
get_calib_data (CalibDataID, 'camera', 0, 'init_params', StartCamPar)
get_calib_data_observ_points (CalibDataID, 0, 0, I - 1, Row, Column, Index, Pose)
get_calib_data_observ_contours (Contours, CalibDataID, 'caltab', 0, 0, I - 1)
gen_cross_contour_xld (Cross, Row, Column, 6, 0.785398)
dev_set_color ('green')
dev_display (Contours)
dev_set_color ('yellow')
dev_display (Cross)
endfor
disp_continue_message (WindowHandle1, 'black', 'true')
stop ()
calibrate_cameras (CalibDataID, Error) //计算畸变系数
get_calib_data (CalibDataID, 'camera', 0, 'params', CamParam) //获取内部参数
gen_empty_obj (Maps)
for I := 1 to NumImages by 1
select_obj (Images, Image, I)
* Obtain the pose of the calibration table
get_calib_data (CalibDataID, 'calib_obj_pose', [0,I - 1], 'pose', Pose) //获取外部参数
set_origin_pose (Pose, -1.125, -1.0, 0, PoseNewOrigin) //设置图像坐标原点
* Generate map
gen_image_to_world_plane_map (MapSingle, CamParam, PoseNewOrigin, 768, 576, 900, 800, 0.0025, 'bilinear')
//生成表述畸变关系的投影图
concat_obj (Maps, MapSingle, Maps)
endfor
clear_calib_data (CalibDataID)
*
* Map the images
*
dev_open_window (0, 391, 900 / 2, 800 / 2, 'black', WindowHandle2)
set_display_font (WindowHandle2, 14, 'mono', 'true', 'false')
Button := 0
NumImage := 1
for I := 1 to NumImages by 1
dev_set_window (WindowHandle1)
dev_set_part (0, 0, 575, 767)
dev_clear_window ()
select_obj (Images, Image, I)
dev_display (Image)
select_obj (Maps, MapSingle, I)
map_image (Image, MapSingle, ImageMapped) //使用投影图计算畸变校正后的图片
dev_set_window (WindowHandle2)
dev_set_part (0, 0, 799, 899)
dev_clear_window ()
dev_display (ImageMapped)
Message := 'Calibration image (' + I + '/' + NumImages + ')'
disp_message (WindowHandle1, Message, 'window', 12, 12, 'black', 'true')
Message := 'Mapped image'
disp_message (WindowHandle2, Message, 'window', 12, 12, 'black', 'true')
if (I < NumImages)
disp_continue_message (WindowHandle1, 'black', 'true')
stop ()
endif
endfor
来看一眼运行结果,效果卡卡赞:
总结
本人水平水平实在有限,希望对各位有所帮助,若有不足之处请各位不惜赐教。
故事结尾姑娘依然没松口,以上解释比较简单,畸变的世界还是相当复杂的,还需要不断学习:衣沾不足惜,但使愿无违。以上就是今天的全部故事了。