vtk是著名的开源三维渲染库,在三维渲染过程中的一个非常重要的内容就是相机即vtkCamera类的设置。在VTK中,相机的实质是一个观测点。VTK的官方Doc对vtkCamera写的十分简略,暗坑很多。在学习和使用vtkCamera的过程中,我走了很多弯路。在我的应用中,我希望能够根据现实中相机的Transform Matrix完全模拟设置vtkCamera。下面根据我的经验和理解,介绍一下vtkCamera,希望对被人能有帮助。

vtkCamera参数

上图是vtkCamera模型示意图,虽然这个图来自于另一个3D库的文档,但是原理一样。要注意的是虽然左边画了个摄像机,但是其实图示的包括焦点在内都是在相机内部的。下面利用这个图来解释一下vtkCamera的各个成员变量的含义。


WindowCenter

WindowCenter按照字面意思是窗口的中心。在相机制造过程中难免存在一定的装配误差,所以透镜的中心往往不能完全对准传感器的中心,而是有非常微小的误差。所以我们在用vtk相机模拟现实中相机时也应该体现这一现象。WindowCenter实质上就是成像的一个offset偏移量。在vtk中WindowCenter的坐标范围是([-1,+1], [-1,+1])。比方说,如果理想情况,图像成像中心(Focal Point)就在窗口的中心,那么WindowCenter就是(0, 0)。如果希望把成像平移到窗口的右上角,那么就应该设置成(1, 1)。根据官方文档,这个量一般在同一窗口显示多个渲染器时才需要设置。但实际上,如果你需要vtk完全模拟现实相机,也要考虑设置这个量,否则永远有一个偏移量。反过来,如果希望图像平移也可以修改这个参数。

这里要注意的是,WindowCenter的正方向是向右向上的。而成像平面的坐标系往往是向右向下的。小心正负号的设置。另外,平移的对象是图像,而不是在三维空间移动相机。


FocalPoint

FocalPoint即焦点位置。在现实相机中焦点在成像平面中心,即在光心的后面(与被摄物体在光心的两侧)。但是在建模时为了简化我们往往对称到光心前面(与被摄物体在光心同侧)。注意这里要求的是焦点的三维坐标,而不仅仅是焦距。因为焦点不仅确定了成像平面的位置,还与光心位置Position一同确定了相机镜头的朝向。


Position

这里的Position指的实际上是光心的位置,或者说对应小孔相机模型中小孔的位置。


ViewUp

ViewUp指图像的正方向。由Position和FocalPoint我们可以确定5个自由度,相机仍然可以沿着主光轴任意旋转。所以这里要指定正方向,即ViewUp。这里注意,ViewUp是一个方向向量,不存在位置,或者说起点永远在原点。


ViewAngle

ViewAngle是视角。默认是30°。其实这个是一个很重要的参数,它决定了图像中内容的比例大小。或者说,通过设置这一变量可以实现图像的放缩。


ClippingRange

ClippingRange即剪切平面,分为前后两个。只有在这两个剪切平面之间的内容才会被渲染和显示。默认值是(0.1,1000)。这个量一般不需要修改,而是在vtkRenderer对象中调用ResetCameraClippingRange()方法来自动重设渲染范围。如果你的图像显示不完整,但是稍微用鼠标旋转或平移一下又变完整了。建议试一下调用一次这个方法。


ParallelProjection

如果为True那么按照平行投影进行渲染,否则默认是按照透视投影PerspectiveProjection进行渲染。透视投影即近大远小的投影,平行投影即用平行光照射得到的投影,没有近大远小的透视效应。如图所示:


DirectionOfProjection

DirectionOfProjection即一个三维矢量,从光心位置Position指向焦点位置FocalPoint。


ViewPlaneNormal

ViewPlaneNormal是投影面(成像面)的法向量。与DirectionOfProjection矢量正好相反。


Distance

Distance即焦距,即上面两个向量的模长。如果使用SetDistance()会沿着主光轴移动焦点FocalPoint,从而使FocalPoint与Position达到给定距离。


ModelTransformMatrix

这个变换矩阵将移动场景里除了相机的所有物体,然后渲染。理论上可以通过“ 相机不动物体动” 获得与 “物体不动相机动”一样的图片。但是标准的方法应该还是使相机移动,这样更符合实际。


ViewTransform

这个矩阵是相机矩阵的逆矩阵。相机矩阵是相机坐标系(原点Position,Z轴指向FocalPoint,Y轴与ViewUp平行)相对于世界坐标系的位置,而这个矩阵是世界坐标系相对于相机坐标系的位置。如果相机矩阵

T=[0R00t1]T=[Rt0001]

那么ViewTransform为
T1=[0RT00RTt1]T1=[RTRTt0001]

单位正交矩阵的逆等于其转置矩阵。


CameraLightTransform

当我们设置了Position,FocalPoint和ViewUp后就会得到一个唯一的变换矩阵(相机矩阵),即从世界坐标系到相机坐标系的坐标变换矩阵,这个相机也就确定了唯一的位置。假设设置另一个相机起始值为Positon = (0, 0, 1),FocalPoint = (0, 0, 0),ViewUp = (0, 1, 0)。对这个相机施加一个怎样的变换,才能得到我们目前当前相机矩阵。这个变换就是CameraLightTransform。


成像过程

VTK背后的渲染过程非常复杂,这里只是简单解释一下原理。

人眼或相机的视野可以想象成一个圆椎体。这个圆椎体的尖端是我们的眼球中心或者说相机的光心,底端朝向被摄物体。这个视锥体将决定我们能看到什么,以及看到的物体的透视变形幅度是怎样的(见下图)。最右边图片中棍子的倾斜角度就是圆锥体的张角。所以在图像中,棍子恰好重合为一个圆点。

我们知道,可以通过顶点位置、张角和底面中心来唯一确定一个圆锥体。这里顶点位置对应光心Position,张角对应ViewAngle,底面中心就是焦点FocalPoint。(请看上面的模型图)这样一来,ViewAngle的计算也就一目了然了。

ViewAngle=2arctan1/2height of image plane|FocalPointPosition|180πViewAngle=2arctan1/2height of image plane|FocalPointPosition|180π

其中的成像平面高度“height of image plane”在现实相机中就对应传感器的高度。可能有人疑惑为什么不是焦距除以成像平面宽度。其实在vtk中我们也可以设置UseHorizontalViewAngle来决定是使用横向比例还是纵向比例。默认以及习惯上都是纵向比例。

所以,根据我们设置的Position,ViewAngle和FocalPoint VTK已经可以完全确定视锥体了。也就是说到目前为止,相机内到底拍到什么(或者说视野中到底能看到什么)已经确定了。注意,到目前为止,所有量都是没有单位的。没有单位也就意味着任意单位,你可以假设所有都是毫米,也可以说所有都是米甚至是英寸。那么如何将其转化为像素呢?这里就需要用到vtkRenderWindows。

在VTK中,相机vtkCamera可以视作胶片相机,也就是说分辨率可以达到无限高。而vtkRenderWindows对象的作用就是数字化。我们通过vtkRenderWindows::SetSize()来设置图像的分辨率。然后VTK会像切蛋糕一样,用一个长方形把视锥体底面套住(高度对齐),然后按照设置的分辨率把长方形内框住的内容切成一个一个的小像素,变成数字化的图片显示到屏幕上完成了渲染。下图为视锥体套上了一个高度对齐底面直径的渲染窗口RenderWindow。

由渲染窗口RenderWindow完成对图像的分割,形成一个又一个的像素点。如下图所示。(因为手头只有系统自带的画图工具,没有办法把成的像画在示意图里面,见谅。)

这一过程也就解释了,为什么增加VTK中的焦距时,图片内容没有放大的原因。当更改焦距(FocusPoint-Position)时,ViewAngle没有改变。所以对应的成像平面向外平移,视锥体底面成比例的放大了。vtkRenderWindows只能用一个更大的长方形套住视锥体体面。但是分辨率不变的,所以长方形被切分成同样多的小像素。虽然成像变大了,但是每个像素的大小也成比例变大了。所以最后表现在屏显图片上没有发生任何改变。

从相机矩阵到vtkCamera

如果已经知道相机外矩阵(可以通过标定Calibration获得)和硬件参数(焦距,传感器大小,传感器中心与镜头中心的偏移量),如何让vtkCamera完全模拟这个相机呢?根据上面的内容,这个问题应该不难解决。具体代码如下:

// 新建一个相机对象
vtkSmartPointer<vtkCamera> camera = 
        vtkSmartPointer<vtkCamera>::New();

// 根据相机外矩阵,设置相机(光心)位置
camera->SetPosition(camTrans.at<double>(0),
                    camTrans.at<double>(1),
                    camTrans.at<double>(2));

// 根据相机外矩阵,设置焦点位置。设置完这两项成像平面已经唯一确定。
camera->SetFocalPoint(camRot.at<double>(0, 2) + camTrans.at<double>(0),
                      camRot.at<double>(1, 2) + camTrans.at<double>(1),
                      camRot.at<double>(2, 2) + camTrans.at<double>(2));

// 根据相机外矩阵,设置成像y正方向。是否有负号取决于原始的相机坐标系中,y是朝向相机上方(正)还是下方(负)。
camera->SetViewUp(- camRot.at<double>(0, 1),
                  - camRot.at<double>(1, 1),
                  - camRot.at<double>(2, 1));

// 计算视角。注意这里我的焦距值focus是负数,所以前面加负号。VTK中ViewAngle用角度表示。
viewAngle = - 2 * atan((sensorSize.height / 2) / focus) * 180 / CV_PI;
camera->SetViewAngle(viewAngle);

// 计算窗口中心。xhyh分别是光学中心相对于传感器中心的偏移量。注意正方向规定的差异导致的负号。建议正负号都试一下。
windowCenter.x = -xh / (sensorSize.width / 2);
windowCenter.y = -yh / (sensorSize.height / 2);
camera->SetWindowCenter(windowCenter.x, windowCenter.y);

另外关于vtkRenderWindow的设置如下:

vtkSmartPointer<vtkRenderer> renderer =
        vtkSmartPointer<vtkRenderer>::New();
renderer->AddActor(Actor);
renderer->SetActiveCamera(camera);
renderer->SetBackground(1, 1, 1);
renderer->ResetCameraClippingRange(); //不加这一行可能会成像不完整。

vtkSmartPointer<vtkRenderWindow> renderWindow =
        vtkSmartPointer<vtkRenderWindow>::New();
renderWindow->AddRenderer(renderer);
renderWindow->SetSize(windowSize.width, windowSize.height);

图像出现偏移往往和WindowCenter的设置有关系,图像的透视角度(近大远小)有问题往往与焦距的设置有关系,如果图像出现了大小的放缩比例错误,就要看看是不是ViewAngle设置有误。这个规律可以帮助定位错误。