随着虚幻引擎在影视虚拟制作领域以及模拟仿真领域比如自动驾驶仿真等的不断深入应用,如何在虚幻引擎中模拟真实相机的机制已成”刚需“。 前不久的直播中,文磊和大家分享了如何在虚幻引擎中模拟真实相机的测光从而进行准确曝光设置等内容,而这篇文章会和大家介绍下如何在虚幻引擎4.24中进行真实相机的标定、畸变模拟与矫正。
首先来认识下这次的主角, 打开Plugins窗口,搜索“Lens Distortion”, 结果会如下图列出两个关于镜头畸变模拟的插件,今天我们主要聊聊最新的“OpenCV Lens Distortion”插件,至于两者的异同,我们会在下文中进行说明。将插件启用,然后重启项目,正式开始!
一、相机的标定
要模拟或者矫正相机畸变,首先需要进行相机标定,从而计算出相机内参。顾名思义,这款内置插件是基于OpenCV的相机模型来工作的。相机内参主要包括径向、切向畸变系数,纵、横向焦距以及光学中心在最终画面上的位置,这些都是由于实际相机或者镜头制造精度或者装配误差所造成的不完美。这款插件的标定是基于OpenCV默认的相机标定方法--张正友标定法。方法的具体的操作流程,网上资料很多,就不在此赘述。插件提供了两种方法进行相机标定。
1. 使用图片文件标定
一种方式是将一组包含棋盘格的图片输入给标定器(Calibrator)。这些图片上的棋盘格组合起来需要能基本覆盖整个画面,棋盘格画面需要清晰。采集时要缓慢转动或者索性在静止时截图来避免运动模糊导致的误差。下边的一组示例图片中存在着一些不足,一是每张图片上的棋盘格占画面比例过低,导致需要采集更多的图片;二是棋盘格没有覆盖到右上角区域,这容易导致计算出的内参在右上角出现较大误差; 三是每个区域不同角度的棋盘格张数有限,这也是潜在影响标定精度的因素之一。接下来看下具体如何实现,创建一个基于Actor的蓝图类,称之为”BP_CamDistortionMgr”。 接着添加一个”BeginCalibration”函数,实现如下图。核心是调用”Create Calibrator”创建标定器,“Board Width/Height” 分别指横向纵向的内部边数(格子数-1),“Square Size”即是每个格子的边长(厘米)。
创建另一个“Calibrate from Images” 函数,将一组图片逐一”喂“给标定器。
循环结束后,调用“EndCalibration”函数,计算相机内参。“EndCalibration”函数实现如下图,如果”Calculate Lens Parameters” 计算返回True,应该进一步检查“Margin of Error”来查看误差,如果误差也小于一个阈值(根据项目需求的一个经验值),那么认为标定成功,此时将“Lens Distortion Parameters”以及”Camera View Info”保存下来。
2. 使用视频实时标定
当然你也可以用另一种实时视频采集的方式来进行标定。由于手里没有采集卡这里以视频文件为例,按照官方文档, 创建一个Material来显示Media Texture上的内容。这里要注意如果将Media Texture 拖到一个Mesh上,默认创建的Material是将Media Texture Sample 输出到 Base Color Channel, 而这里因为后续“Draw Material to Render Target” 的需要,应该输出到Emissive Color Channel。然后创建一个Render Target, 格式采用RGBA8 即可, 大小和视频画面一致。然后通过“Draw Material to Render Target” 将Media Texture 转到 Render Target上 然后进一步”喂“给标定器。可以每次Tick调用“Calibrate Current Frame”也可以采用其他诸如按键等方式来触发。当采集了足够多的画面后,再调用上文的“EndCalibration”得到内参。
二、相机的畸变模拟与矫正
1. 预计算Displacement Map
插件从效率考虑,通过预计算将畸变模拟和矫正的uv offset预先存储在一张Displacement Map中,从而可以在运行时通过一次采样获得相应的uv offset值。Displacement Map可以通过以下蓝图来生成。此处的Render Target基于精度考量使用RGBA32f格式,其大小与输入视频尺寸相同或者等比例缩放。需要注意这里的Cropping Factor, 其决定在计算Displacment Map时,是否对其进行缩放使得矫正后的图像中不会出现黑色区域,反之则是保留所有原始图像的像素,但边缘会有黑色区域。
Cropping Factor = 0 | Cropping Factor = 1 |
2. 使用Post Process Material模拟或矫正畸变
有了Displacement Map后就可以很方便来模拟畸变或者矫正畸变了,创建一个Post Process Material,将其设在CineCamereActor的Post Porcess/Rendering Features/Post Process Materials里。如下图所示,Displacement Map 的Red、Green Channel上存储畸变矫正的uv offset, 而Blue、Alpha Channel则存储着畸变模拟的uv offset,设置ToggleDistortAndUnDistort设为0,1来切换矫正畸变还是模拟畸变。 Enable PP 设为0或1 控制开启或者关闭这个PP。比如我们需要将UE渲染出的虚拟画面模拟畸变从而能和实拍画面进行合成,那么应该将 EnablePP与ToggleDistortAndUndistort设为1。
最后在CineCameraActor的红框参数输入与实拍相机一致的参数,其中“Current Focal Length”应该使用标定得出的“Camera View Info”中的“Focal Length”值。
这里值得注意的是由于我们并没有将相机的内参比如Principal Point直接作用在相机的投影矩阵上, 这就可能导致有些物体会在遮挡阶段被错误的剔除,从而在某些情况下造成画面的边缘偶尔的显示错误。
三、进一步的思考
1. 关于易用性
不同于OpenCV计算得到的相机内参,插件计算得到的Fx,Fy, Cx,Cy都做了归一化处理,而不是以像素为单位,这样的好处是你可以更方便的处理相同长宽比的不同分辨率的图像,比如你校准的时候输入图像为1920x1080,那么你可以很方便的用同一套内参去模拟3840x2160或者960x540的情况。(当然前提是你并没有改变Focal Length,Focus Distance等会影响内参的参数)如果想要同一组相机内参模拟不同长宽比的分辨率的情况,那么你需要了解这不同分辨率在相机感光元件(CMOS/CCD)上所使用的成像面积之间的关系来进一步换算。
除此以外,插件也考虑到了OpenCV相机坐标系和UE坐标系的不同,已经做了相应转换。
2. 为什么用预计算Displacement Map的方式
之所以使用预计算的方式,显然是因为性能的考量。OpenCV文档上可以看到畸变矫正可以通过下面的公式来计算矫正后的像素坐标,显而易见没有必要在运行时来做,预计算后一次采样操作要轻松愉快的多。
(来自:https://docs.opencv.org/2.4/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html)
更重要的是如果要进行逆计算--畸变的模拟,你需要使用诸如牛顿迭代法等方式来求解,对GPU很不友好。这里插件的实现用了一个巧妙的方法用一个pass解决了畸变模拟和矫正的预计算。VS假设输入的Grid(默认32x32)的顶点是畸变后的坐标,然后通过采样预计算的“Undistort UV Displacement Map”,得到矫正后的位置,将矫正后的位置作为SvPosition传给PS,同时将原始的坐标作为Texcoord 传给PS, 在PS中将Texcoord坐标减去SvPosiiton 就得到了undistort to distort的uv offset; distort to undistort 则比较简单,PS 同时将SvPosition当作distort后的值,sample undistortDisplacementMap就能求得undistort的坐标,相减得到反向的uv offset。(参见\Engine\Plugins\Compositing\OpenCVLensDistortion\Shaders\Private\DisplacementMapGeneration.usf)3. 关于精度
从上文的解释可知,精度与输入图像的分辨率、
4. 两个Lens Distortion插件的异同
最后来回答文章开头的问题就更容易理解了,其实两者用的camera model是一样的,所以其背后的原理也是类似的,区别在于:(下文第一个表示“Lens Distortion”插件,第二个表示”OpenCV Lens Distortion”插件“)
- 第一个用的是个简化的模型,只包含了畸变系数的前几项,绝大多数情况下也够用了。
- 第一个插件并没有标定功能。
- 第一个插件利用Shader直接生成了Displacement map,相当于把第二个插件中”Create Undistort UVDisplacment Map”的工作也放在了shader中完成,而第二个插件则是调用OpenCV的API用CPU来完成的,所以效率上第一个会更胜一筹,但生成map毕竟只是预计算的一步,快慢其实并不很重要,当然如果不想预计算所需用到的所有Displacement Map,而是直接在runtime根据实时的内参动态来生成的话,第一个才是你的”菜“。
- 接着第三条,因为Shader计算浮点数精度为32位,而在OpenCV相应API内使用的是64位浮点精度来进行计算,再结合第1点可以说精度上第二个插件理论上会更好。