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平行)相對於世界坐標系的位置,而這個矩陣是世界坐標系相對於相機坐標系的位置。如果相機矩陣

那么ViewTransform為

單位正交矩陣的逆等於其轉置矩陣。
CameraLightTransform
當我們設置了Position,FocalPoint和ViewUp后就會得到一個唯一的變換矩陣(相機矩陣),即從世界坐標系到相機坐標系的坐標變換矩陣,這個相機也就確定了唯一的位置。假設設置另一個相機起始值為Positon = (0, 0, 1),FocalPoint = (0, 0, 0),ViewUp = (0, 1, 0)。對這個相機施加一個怎樣的變換,才能得到我們目前當前相機矩陣。這個變換就是CameraLightTransform。
成像過程
VTK背后的渲染過程非常復雜,這里只是簡單解釋一下原理。
人眼或相機的視野可以想象成一個圓椎體。這個圓椎體的尖端是我們的眼球中心或者說相機的光心,底端朝向被攝物體。這個視錐體將決定我們能看到什么,以及看到的物體的透視變形幅度是怎樣的(見下圖)。最右邊圖片中棍子的傾斜角度就是圓錐體的張角。所以在圖像中,棍子恰好重合為一個圓點。
。
我們知道,可以通過頂點位置、張角和底面中心來唯一確定一個圓錐體。這里頂點位置對應光心Position,張角對應ViewAngle,底面中心就是焦點FocalPoint。(請看上面的模型圖)這樣一來,ViewAngle的計算也就一目了然了。
其中的成像平面高度“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完全模擬這個相機呢?根據上面的內容,這個問題應該不難解決。具體代碼如下:
1 // 新建一個相機對象
2 vtkSmartPointer<vtkCamera> camera =
3 vtkSmartPointer<vtkCamera>::New(); 4
5 // 根據相機外矩陣,設置相機(光心)位置
6 camera->SetPosition(camTrans.at<double>(0), 7 camTrans.at<double>(1), 8 camTrans.at<double>(2)); 9
10 // 根據相機外矩陣,設置焦點位置。設置完這兩項成像平面已經唯一確定。
11 camera->SetFocalPoint(camRot.at<double>(0, 2) + camTrans.at<double>(0), 12 camRot.at<double>(1, 2) + camTrans.at<double>(1), 13 camRot.at<double>(2, 2) + camTrans.at<double>(2)); 14
15 // 根據相機外矩陣,設置成像y正方向。是否有負號取決於原始的相機坐標系中,y是朝向相機上方(正)還是下方(負)。
16 camera->SetViewUp(- camRot.at<double>(0, 1), 17 - camRot.at<double>(1, 1), 18 - camRot.at<double>(2, 1)); 19
20 // 計算視角。注意這里我的焦距值focus是負數,所以前面加負號。VTK中ViewAngle用角度表示。
21 viewAngle = - 2 * atan((sensorSize.height / 2) / focus) * 180 / CV_PI; 22 camera->SetViewAngle(viewAngle); 23
24 // 計算窗口中心。xh和yh分別是光學中心相對於傳感器中心的偏移量。注意正方向規定的差異導致的負號。建議正負號都試一下。
25 windowCenter.x = -xh / (sensorSize.width / 2); 26 windowCenter.y = -yh / (sensorSize.height / 2); 27 camera->SetWindowCenter(windowCenter.x, windowCenter.y);
另外關於vtkRenderWindow的設置如下:
1 vtkSmartPointer<vtkRenderer> renderer =
2 vtkSmartPointer<vtkRenderer>::New(); 3 renderer->AddActor(Actor); 4 renderer->SetActiveCamera(camera); 5 renderer->SetBackground(1, 1, 1); 6 renderer->ResetCameraClippingRange(); //不加這一行可能會成像不完整。
7
8 vtkSmartPointer<vtkRenderWindow> renderWindow =
9 vtkSmartPointer<vtkRenderWindow>::New(); 10 renderWindow->AddRenderer(renderer); 11 renderWindow->SetSize(windowSize.width, windowSize.height);
圖像出現偏移往往和WindowCenter的設置有關系,圖像的透視角度(近大遠小)有問題往往與焦距的設置有關系,如果圖像出現了大小的放縮比例錯誤,就要看看是不是ViewAngle設置有誤。這個規律可以幫助定位錯誤。