錄影機是3D世界和2D圖像之間的一種映射。
利用齊次坐标表示3D空間坐标 X(X,Y,Z,1) ,2D圖像空間坐标 x(x,y,1) ,存在一個3X4的矩陣 P ,滿足:
x=PX (1−1)
如果已知足夠數量的x與X一一對應的點,便可以計算出矩陣 P 。
為了更進一步了解 P 的具體含義,需要弄清楚針孔錄影機的幾何特性。
考慮空間點到一張平面上的中心投影,假設投影中心位于歐式坐标系的原點,而平面 Z=f 被稱為圖像平面或聚焦平面。根據三角形相似原理,從世界坐标到圖像坐标的中心投影是:
(X,Y,Z)T−>(fX/Z,fY/Z)T (1−2)
這是從三維歐氏空間到二維歐氏空間的一個映射。并且假定圖像平面的坐标原點在主點上,(錄影機中心到圖像平面的垂線稱為錄影機的主軸,而主軸與圖像平面的交點稱為主點,如上圖中點p)。一般情況下,存在主點偏置。
(X,Y,Z)T−>(fX/Z+px,fY/Z+py)T (1−3)
其中, (px,py)T 是在圖像坐标系下主點的坐标。
參考圖5.2,公式(1-3)用齊次坐标可以表示成:
Z⎡⎣⎢xy1⎤⎦⎥=⎡⎣⎢f000f0pxpy1000⎤⎦⎥⎡⎣⎢⎢⎢XYZ1⎤⎦⎥⎥⎥ (1−4)
需要強調的是,坐标 [X,Y,Z,1]T 是以錄影機中心為原點建立的坐标下表示的三維空間中的點,并且Z軸為錄影機的主軸方向,此時可記 Xcam=[X,Y,Z,1]T ,該坐标系稱為錄影機坐标系。一般情況下,空間點采用不同的歐氏坐标系表示,稱為世界坐标系。錄影機坐标與世界坐标系通過旋轉和平移相聯系。如圖:
如果 X˜ 表示世界坐标系中一點的非齊次坐标, X˜cam 是以錄影機坐标系表示的同一點,則有 X˜cam=R(X˜−C˜) 。其中, C˜ 表示錄影機中心在世界坐标系中的坐标, R 是一個3X3的旋轉矩陣,表示錄影機坐标系的方位,該方程在齊次坐标下可以寫成
Xcam=[R0T−RC˜1]⎡⎣⎢⎢⎢XYZ1⎤⎦⎥⎥⎥=[R0T−RC˜1]X (1−5)
将公式(1-4)與公式(1-5)合并,
x=KR[I |−C˜]X (1−6) .
其中,
K=⎡⎣⎢f000f0pxpy1⎤⎦⎥ (1−7)
根據(1-1)式,錄影機矩陣可分解為:
P=K[R|t] ,t=−RC˜ (1−8)
上面推導的針孔錄影機模型假設圖像坐标在兩個軸向上有等尺度的歐氏坐标。在CCD錄影機模型中,像素不可能為正方形,如果圖像坐标以像素來測量,則需要在每個方向上引入非等量的尺度因子。具體地說,如果在x,y方向上圖像坐标機關距離的像素數分别是 mx,my ,那麼由世界坐标系到像素坐标系的變換将由(1-7)式左乘一個附加的因子 diag(mx,my,1) 而得到。是以,一個CCD錄影機标定矩陣的一般形式為:
K=⎡⎣⎢fx000fy0x0y01⎤⎦⎥ (1−9)
其中, fx=f∗mx,fy=f∗my 分别把錄影機的焦距換算成x,y方向上的像素量綱,同理, x0=(x0,y0)T 是用像素量綱表示的主點, x0=mx∗px,y0=my∗py 。
至此,錄影機的映射已介紹完畢,需要提醒一點的是,在上圖5.2中,建立的圖像坐标y軸方向是朝上的,其與實際的圖像坐标系y軸是反向的。
在OpenCV中,給出了錄影機标定的方法,通過示例,可以準确地計算出矩陣 P , K , R 以及 t 。為了更深刻地了解旋轉矩陣 R 以及位移向量 t ,可以在OpenGL中繪制出參考标定闆的位置,然後通過 R 以及 t 繪制出對應不同視角下的相機。
關于OpenCV相機的标定看這裡。
關于OpenGL的Transformation 看這裡。
OpenCV标定程式是sources\samples\cpp目錄下的calibration.cpp,使用的圖像資料是 left01.jpg ~ left14.jpg ,共13張圖檔。
如圖,識别到的棋盤點的順序如圖編号所示,由于點的坐标是以圖檔左上角為原點,是以我将坐标原點修改為圖像的左下角,此時,圖像坐标系與上述圖5.2中的保持一緻。在calibration.cpp中代碼修改如下:
...
//line
if( mode == CAPTURING && found &&
(!capture.isOpened() || clock() - prevTimestamp > delay**CLOCKS_PER_SEC) )
{
vector<Point2f> points;
for (int i = ; i < pointbuf.size(); i++)
{
Point2f p = pointbuf[i];
points.push_back(Point2f(p.x, imageSize.height - p.y));
}
imagePoints.push_back(points);//存儲更新坐标系後的點
prevTimestamp = clock();
blink = capture.isOpened();
}
...
同樣,在定義棋盤點的空間坐标時,選取的坐标系與圖5.2保持一緻,進而設定點46為坐标系原點,點1的三維坐标即為(0,5,0),點9的坐标為(8,5,0)(squareSize = 1)。在calibration.cpp中修改代碼如下:
...
//line
case CHESSBOARD:
case CIRCLES_GRID:
for( int i = ; i < boardSize.height; i++ )
for( int j = ; j < boardSize.width; j++ )
corners.push_back(Point3f(float(j*squareSize),
float((boardSize.height - - i)*squareSize), ));
break;
...
指令運作标定程式:
E:\OpenCV\Sample\camCalib\bin\bin>cameraCalib.exe -w 9 -h 6 -o camera.yml -oe imagelist.xml
生成的camera.yml包括錄影機的内參數以及在不同視角下的旋轉和位移。旋轉通過單一的旋轉角 θ 和所圍繞的機關向量方向 v^=(x,y,z) 來定義,在OpenCV 中的Rodrigues方法可以看到詳細說明:
此時,因為角-軸表示密切關聯于四元數表示。依據軸和角,四元數可以給出為正規化四元數 Q :
Q=(xi+yj+zk)sin(θ/2)+cos(θ/2)
然後,根據四元數與歐拉角的關系,計算出繞Z軸、Y軸、X軸的旋轉角度 ψ,θ,φ 。
由公式(1-8),不難求出錄影機的中心坐标, C=−R−1t 。
代碼如下:
...
int i = cameraId;
Mat camPos = tvecs[i]; //擷取第i個相機的位移向量
Mat camRot = rvecs[i]; //擷取第i個相機的旋轉向量
Mat rotation;
cv::Rodrigues(camRot, rotation);
Mat cameraPos = -rotation.inv()*camPos; // C = -R^-1*t
std::cout << "Cam Position: " << cameraPos << std::endl;
float w, x, y, z;
w = cos(norm(camRot) / );
x = sin(norm(camRot) / )*camRot.ptr<double>()[] / norm(camRot);
y = sin(norm(camRot) / )*camRot.ptr<double>()[] / norm(camRot);
z = sin(norm(camRot) / )*camRot.ptr<double>()[] / norm(camRot);
std::cout << w << " " << x << " " << y << " " << z << std::endl;
//四元素轉歐拉角
cameraAngle[] = atan2( * (w*x + z*y), - * (y*y + x*x));
cameraAngle[] = asin( * (w*y - x*z));
cameraAngle[] = atan2( * (w*z + y*x), - * (z*z + y*y));
//歐拉角反推旋轉矩陣,進行驗證
Matrix3 Rx, Ry, Rz;
Rx.set(, , , , cos(cameraAngle[]), -sin(cameraAngle[]), , sin(cameraAngle[]), cos(cameraAngle[]));
Ry.set(cos(cameraAngle[]), , sin(cameraAngle[]), , , , -sin(cameraAngle[]), , cos(cameraAngle[]));
Rz.set(cos(cameraAngle[]), -sin(cameraAngle[]), , sin(cameraAngle[]), cos(cameraAngle[]), , , , );
Matrix3 R = Rz*Ry*Rx;
std::cout << rotation << std::endl;
std::cout << R << std::endl << std::endl;
...
利用計算得到的錄影機位置與姿态,處理如下:
...
glPushMatrix();
glTranslatef(cameraPos.at<double>(, ), cameraPos.at<double>(, ), -cameraPos.at<double>(, )); //OpenGL的Z軸與相機自身Z軸是反向的。
glRotatef(cameraAngle[] * / , , , );
glRotatef(cameraAngle[] * / , , , );
glRotatef(cameraAngle[] * / , , , );
drawCamera();
drawFrustum(FOV_Y, /, , );
glPopMatrix();
...
為了将錄影機參數檔案camera.yml 的相關資料讀入程式,提供了如下方法:
void readCamerasParams(const string filename, std::vector<Mat>& rvecs, std::vector<Mat>& tvecs)
{
FileStorage fs(filename, FileStorage::READ);
int nframes;
fs["nframes"] >> nframes;
Mat bigmat;
fs["extrinsic_parameters"] >> bigmat;
rvecs.resize(bigmat.rows);
tvecs.resize(bigmat.rows);
for (int i = ; i < bigmat.rows; i++ )
{
Mat r = bigmat(Range(i, i + ), Range(, ));
Mat t = bigmat(Range(i, i + ), Range(, ));
rvecs[i] = r.t();
tvecs[i] = t.t();
}
for (int i = ; i < rvecs.size();i++)
{
std::cout << rvecs[i] << " ," << tvecs[i] << std::endl;
}
}
結果展示:
0号相機_left01.jpg:
5号相機_left06.jpg:
全部相機: