雙目測距與三維重建的OpenCV實現問題集錦(二)雙目定標與雙目校正

查志強發表於2016-01-27

【原文:http://blog.csdn.net/chenyusiyuan/article/details/5963256

三、雙目定標和雙目校正

雙目攝像頭定標不僅要得出每個攝像頭的內部引數,還需要通過標定來測量兩個攝像頭之間的相對位置(即右攝像頭相對於左攝像頭的三維平移 t 和旋轉 R 引數)。

clip_image002

圖6

要計算目標點在左右兩個檢視上形成的視差,首先要把該點在左右檢視上兩個對應的像點匹配起來。然而,在二維空間上匹配對應點是非常耗時的,為了減少匹配搜尋範圍,我們可以利用極線約束使得對應點的匹配由二維搜尋降為一維搜尋。

      clip_image004  clip_image006

圖7

而雙目校正的作用就是要把消除畸變後的兩幅影象嚴格地行對應,使得兩幅影象的對極線恰好在同一水平線上,這樣一幅影象上任意一點與其在另一幅影象上的對應點就必然具有相同的行號,只需在該行進行一維搜尋即可匹配到對應點。

clip_image008

圖8

 

1. 關於cvStereoCalibrate的使用

如果按照 Learning OpenCV 的例程,直接通過cvStereoCalibrate來實現雙目定標,很容易產生比較大的影象畸變,邊角處的變形較厲害。最好先通過cvCalibrateCamera2() 對每個攝像頭單獨進行定標,再利用cvStereoCalibrate進行雙目定標。這樣定標所得引數才比較準確,隨後的校正也不會有明顯的畸變。我使用的程式主要基於Learning OpenCV 的例程ch12_ex12_3.cpp,其中主要部分如下:

    //////////////////////////////////////////////////////////////////////////
    // 是否首先進行單目定標?
    cvCalibrateCamera2(&_objectPoints, &_imagePoints1, &_npoints, imageSize,    
        &t_M1, &t_D1, NULL, NULL, CV_CALIB_FIX_K3);
    cvCalibrateCamera2(&_objectPoints, &_imagePoints2, &_npoints, imageSize,    
        &t_M2, &t_D2, NULL, NULL, CV_CALIB_FIX_K3);
    //////////////////////////////////////////////////////////////////////////
    // 進行雙目定標
    cvStereoCalibrate( &_objectPoints, &_imagePoints1,
        &_imagePoints2, &_npoints,
        &t_M1, &t_D1, &t_M2, &t_D2,
        imageSize, &t_R, &t_T, &t_E, &t_F,
        cvTermCriteria(CV_TERMCRIT_ITER+
        CV_TERMCRIT_EPS, 100, 1e-5));  // flags為預設的CV_CALIB_FIX_INTRINSIC

上面的t_M1(2), t_D1(2) 分別是單目定標後獲得的左(右)攝像頭的內參矩陣3*3)和畸變引數向量(1*5);t_R, t_T 分別是右攝像頭相對於左攝像頭的旋轉矩陣(3*3)和平移向量(3*1), t_E是包含了兩個攝像頭相對位置關係的Essential Matrix(3*3),t_F 則是既包含兩個攝像頭相對位置關係、也包含攝像頭各自內參資訊的Fundamental Matrix(3*3)。

clip_image010

圖9

 

2. cvStereoCalibrate 是怎樣計算 Essential Matrix 和 Fundamental Matrix 的?

首先我們以Learning OpenCV第422頁為基礎,討論下 Essential Matrix 和 Fundamental Matrix 的構造過程,再看看 OpenCV 是如何進行計算的。

clip_image012

圖10

 注:原文中對pl、p和ql、q物理意義和計算公式的表述有誤,已修正。(2011-04-12)

(1)Essential Matrix

如上圖所示,給定一個目標點P,以左攝像頭光心Ol為原點。點P相對於光心Ol的觀察位置為Pl,相對於光心Or的觀察位置為Pr。點P在左攝像頭成像平面上的位置為pl,在右攝像頭成像平面上的位置為pr注意Pl、Pr、pl、p都處於攝像機座標系,其量綱是與平移向量T相同的(pl、p在影象座標系中對應的畫素座標為 ql、q)。

假設右攝像頭相對於左攝像頭的相對位置關係由旋轉矩陣R和平移向量T表示,則可得:Pr = R(Pl-T)。

現在我們要尋找由點P、Ol和Or確定的對極平面的表示式。注意到平面上任意一點x與點a的連線垂直於平面法向量n,即向量 (x-a) 與向量 n 的點積為0:(x-a)·n = 0。在Ol座標系中,光心Or的位置為T,則P、Ol和Or確定的對極平面可由下式表示:(Pl-T)T·(Pl×T) = 0。

Pr = R(Pl-T) 和 RT=R-1 可得:(RTPr)T·(Pl×T) = 0。

另一方面,向量的叉積又可表示為矩陣與向量的乘積,記向量T的矩陣表示為S,得:Pl×T = SPl

clip_image014

圖11

那麼就得到:(Pr)TRSPl = 0。這樣,我們就得到Essential Matrix:E = RS。

通過矩陣E我們知道Pl和Pr的關係滿足:(Pr)TEPl = 0。進一步地,由 pl = fl*Pl/Zl 和 pr = fr*Pr/Zr 我們可以得到點P在左右兩個攝像機座標系中的觀察點 p和 p應滿足的極線約束關係為:(pr)TEpl = 0。

注意到 E 是不滿秩的,它的秩為2,那麼 (pr)TEpl = 0 表示的實際上是一條直線,也就是對極線。

(2)Fundamental Matrix

由於矩陣E並不包含攝像頭內參資訊,且E是面向攝像頭座標系的。實際上我們更感興趣的是在影象畫素座標系上去研究一個畫素點在另一檢視上的對極線,這就需要用到攝像機的內參資訊將攝像頭座標系和影象畫素座標系聯絡起來。在(1)中,pl和pr是物理座標值,對應的畫素座標值為ql和qr,攝像頭內參矩陣為M,則有:p=M-1q。從而:(pr)TEpl = 0 à qrT(Mr-1)TE Ml-1ql = 0。這裡,我們就得到Fundamental Matrix:F = (Mr-1)TE Ml-1。並有 qrTFql = 0。

(3)OpenCV的相關計算

由上面的分析可見,求取矩陣E和F關鍵在於旋轉矩陣R和平移向量T的計算,而cvStereoCalibrate的程式碼中大部分(cvcalibration.cpp的第1886-2180行)也是計算和優化R和T的。在cvcalibration.cpp的第1913-1925行給出了計算R和T初始估計值的基本方法:

    /*
       Compute initial estimate of pose

       For each image, compute:
          R(om) is the rotation matrix of om
          om(R) is the rotation vector of R
          R_ref = R(om_right) * R(om_left)'
          T_ref_list = [T_ref_list; T_right - R_ref * T_left]
          om_ref_list = {om_ref_list; om(R_ref)]

       om = median(om_ref_list)
       T = median(T_ref_list)
    */

具體的計算過程比較繁雜,不好理解,這裡就不討論了,下面是計算矩陣E和F的程式碼:

 

    if( matE || matF )
    {
        double* t = T_LR.data.db;
        double tx[] =
        {
            0, -t[2], t[1],
            t[2], 0, -t[0],
            -t[1], t[0], 0
        };
        CvMat Tx = cvMat(3, 3, CV_64F, tx);
        double e[9], f[9];
        CvMat E = cvMat(3, 3, CV_64F, e);
        CvMat F = cvMat(3, 3, CV_64F, f);
        cvMatMul( &Tx, &R_LR, &E );
        if( matE )
            cvConvert( &E, matE );
        if( matF )
        {
            double ik[9];
            CvMat iK = cvMat(3, 3, CV_64F, ik);
            cvInvert(&K[1], &iK);
            cvGEMM( &iK, &E, 1, 0, 0, &E, CV_GEMM_A_T );
            cvInvert(&K[0], &iK);
            cvMatMul(&E, &iK, &F);
            cvConvertScale( &F, matF, fabs(f[8]) > 0 ? 1./f[8] : 1 );
        }
    }

 

3. 通過雙目定標得出的向量T中,Tx符號為什麼是負的?

@scyscyao:這個其實我也不是很清楚。個人的解釋是,雙目定標得出的T向量指向是從右攝像頭指向左攝像頭(也就是Tx為負),而在OpenCV座標系中,座標的原點是在左攝像頭的。因此,用作校準的時候,要把這個向量的三個分量符號都要換一下,最後求出的距離才會是正的。

clip_image016

圖12

但是這裡還有一個問題,就是Learning OpenCV中Q的表示式,第四行第三列元素是-1/Tx,而在具體實踐中,求出來的實際值是1/Tx。這裡我和maxwellsdemon討論下來的結果是,估計書上Q表示式裡的這個負號就是為了抵消T向量的反方向所設的,但在實際寫OpenCV程式碼的過程中,那位朋友卻沒有把這個負號加進去。”

clip_image018

圖13

scyscyao 的分析有一定道理,不過我覺得還有另外一種解釋:如上圖所示,攝像機C1(C2)與世界座標系相對位置的外部引數為旋轉矩陣R1(R2)和平移向量 t1(t2),如果下標1代表左攝像機,2代表右攝像機,顯然在平移向量的水平分量上有 t1x > t2x;若以左攝像機C1為座標原點,則可得到如上圖所示的旋轉矩陣R和平移向量t,由於t1x > t2x,則有 tx < 0。

為了抵消Tx為負,在矩陣Q中元素(4,3)應該加上負號,但在cvStereoRectify程式碼中並沒有新增上,這就使得我們通過 cvReprojectImageTo3D 計算得到的三維資料都與實際值反號了。

 

    if( matQ )
    {
        double q[] =
        {
            1, 0, 0, -cc_new[0].x,
            0, 1, 0, -cc_new[0].y,
            0, 0, 0, fc_new,
            0, 0, 1./_t[idx],
            (idx == 0 ? cc_new[0].x - cc_new[1].x : cc_new[0].y - cc_new[1].y)/_t[idx]
        };
        CvMat Q = cvMat(4, 4, CV_64F, q);
        cvConvert( &Q, matQ );
    }

 

為了避免上述反號的情況,可以在計算得到Q矩陣後,新增以下程式碼更改 Q[3][2] 的值。

 

// Q 是 Mat 型別矩陣,OpenCV2.1 C++ 模式
    Q.at<double>(3, 2) = -Q.at<double>(3, 2);    
// Q 是 double 陣列定義時
    double Q[4][4];
    CvMat t_Q = cvMat(4, 4, CV_64F, Q );
    cvStereoRectify(…);
    Q[3][2]=-Q[3][2];

 

4. 雙目校正原理及cvStereoRectify 的應用。

clip_image020

圖14

如圖14所示,雙目校正是根據攝像頭定標後獲得的單目內引數據(焦距、成像原點、畸變係數)和雙目相對位置關係(旋轉矩陣和平移向量),分別對左右檢視進行消除畸變和行對準,使得左右檢視的成像原點座標一致(CV_CALIB_ZERO_DISPARITY 標誌位設定時發生作用)、兩攝像頭光軸平行、左右成像平面共面、對極線行對齊。在OpenCV2.1版之前,cvStereoRectify 的主要工作就是完成上述操作,校正後的顯示效果如圖14(c) 所示。可以看到校正後左右檢視的邊角區域是不規則的,而且對後續的雙目匹配求取視差會產生影響,因為這些邊角區域也參與到匹配操作中,其對應的視差值是無用的、而且一般數值比較大,在三維重建和機器人避障導航等應用中會產生不利影響。

因此,OpenCV2.1 版中cvStereoRectify新增了4個引數用於調整雙目校正後影象的顯示效果,分別是 double alpha, CvSize newImgSize, CvRect* roi1, CvRect* roi2。下面結合圖15-17簡要介紹這4個引數的作用:

(1)newImgSize:校正後remap影象的解析度。如果輸入為(0,0),則是與原影象大小一致。對於影象畸變係數比較大的,可以把newImgSize 設得大一些,以保留影象細節。

(2)alpha:影象剪裁係數,取值範圍是-1、0~1。當取值為 0 時,OpenCV會對校正後的影象進行縮放和平移,使得remap影象只顯示有效畫素(即去除不規則的邊角區域),如圖17所示,適用於機器人避障導航等應用;當alpha取值為1時,remap影象將顯示所有原影象中包含的畫素,該取值適用於畸變係數極少的高階攝像頭;alpha取值在0-1之間時,OpenCV按對應比例保留原影象的邊角區域畫素。Alpha取值為-1時,OpenCV自動進行縮放和平移,其顯示效果如圖16所示。

(3)roi1, roi2:用於標記remap影象中包含有效畫素的矩形區域。對應程式碼如下:

 

02433     if(roi1)
02434     {
02435         *roi1 = cv::Rect(cvCeil((inner1.x - cx1_0)*s + cx1),
02436                      cvCeil((inner1.y - cy1_0)*s + cy1),
02437                      cvFloor(inner1.width*s), cvFloor(inner1.height*s))
02438             & cv::Rect(0, 0, newImgSize.width, newImgSize.height);
02439     }
02440     
02441     if(roi2)
02442     {
02443         *roi2 = cv::Rect(cvCeil((inner2.x - cx2_0)*s + cx2),
02444                      cvCeil((inner2.y - cy2_0)*s + cy2),
02445                      cvFloor(inner2.width*s), cvFloor(inner2.height*s))
02446             & cv::Rect(0, 0, newImgSize.width, newImgSize.height);
02447     }

 

clip_image022

圖15

clip_image024

圖16

clip_image026

圖17

在cvStereoRectify 之後,一般緊接著使用 cvInitUndistortRectifyMap 來產生校正影象所需的變換引數(mapx, mapy)。

 

//////////////////////////////////////////////////////////////////////////
// 執行雙目校正
// 利用BOUGUET方法或HARTLEY方法來校正影象
    mx1 = cvCreateMat( imageSize.height, imageSize.width, CV_32F );
    my1 = cvCreateMat( imageSize.height, imageSize.width, CV_32F );
    mx2 = cvCreateMat( imageSize.height, imageSize.width, CV_32F );
    my2 = cvCreateMat( imageSize.height, imageSize.width, CV_32F );
    double R1[3][3], R2[3][3], P1[3][4], P2[3][4], Q[4][4];
    CvMat t_R1 = cvMat(3, 3, CV_64F, R1);
    CvMat t_R2 = cvMat(3, 3, CV_64F, R2);
    CvMat t_Q = cvMat(4, 4, CV_64F, Q );
    CvRect roi1, roi2;

// IF BY CALIBRATED (BOUGUET'S METHOD)    
    CvMat t_P1 = cvMat(3, 4, CV_64F, P1);
    CvMat t_P2 = cvMat(3, 4, CV_64F, P2);
    cvStereoRectify( &t_M1, &t_M2, &t_D1, &t_D2, imageSize,
        &t_R, &t_T, &t_R1, &t_R2, &t_P1, &t_P2, &t_Q,
        CV_CALIB_ZERO_DISPARITY, 
        0, imageSize, &roi1, &roi2); 
// Precompute maps for cvRemap()
    cvInitUndistortRectifyMap(&t_M1,&t_D1,&t_R1,&t_P1, mx1, my1);
    cvInitUndistortRectifyMap(&t_M2,&t_D2,&t_R2,&t_P2, mx2, my2);

 

5. 為什麼cvStereoRectify求出的Q矩陣cx, cy, f都與原來的不同?

@scyscyao:在實際測量中,由於攝像頭擺放的關係,左右攝像頭的f, cx, cy都是不相同的。而為了使左右檢視達到完全平行對準的理想形式從而達到數學上運算的方便,立體校準所做的工作事實上就是在左右像重合區域最大的情況下,讓兩個攝像頭光軸的前向平行,並且讓左右攝像頭的f, cx, cy相同。因此,Q矩陣中的值與兩個instrinsic矩陣的值不一樣就可以理解了。”

注:校正後得到的變換矩陣Q,Q[0][3]、Q[1][3]儲存的是校正後左攝像頭的原點座標(principal point)cx和cy,Q[2][3]是焦距f。

 

(待續……)

5

相關文章