1. 程式人生 > >OpenCV 張正友標定法的實現

OpenCV 張正友標定法的實現

  關於張正友標定法的原理,網上的資料很多,本人雖然看了一些,但覺得還沒有到能講的非常清楚的程度,因此不在這裡做太多原理描述。有興趣瞭解細節的可以看張大神的原文,或者這篇文章

  需要大概知道的是,相機標定中內參、外參和畸變引數的概念。

  內參有五個,分別是:

  攝像頭拍攝到的物體和實際物體在x,y軸上的對映關係(兩個引數)。

  攝像頭中心和影象中心的偏移關係(兩個引數)。

  攝像頭和鏡頭安裝非完全垂直,存在一個角度的偏差。(一個引數)

  外參有六個,分別是x,y,z方向上的平移和旋轉。

  有了上面兩種引數,我們基本上知道攝像頭拍攝到的影象和現實事物的對應關係了,但“畸變”亦不能忽略。它是由於鏡頭質量等原因導致的2D點的偏移。舉個簡單的例子就是用攝像頭拍攝一個正方形,影象上會變成一個桶形或者其他的形狀。在張氏標定法中張大神用“極大似然法”去計算出畸變的各項引數(如果想加深理解,也可以參考本人之前寫過的一篇
相關的文章
)。

  到此,就介紹完了相機標定三個最為重要的概念。一般我們要處理攝像頭的畸變,只要求內參和畸變引數就可以了,而要做雙目標定則需要把外引數也求出來。

  本篇文章主要介紹用OpenCV自帶的張正友標定法相關的函式來對攝像頭進行標定(求取攝像頭內外引數和畸變引數)並對單張影象進行校正的方法。主要參考的是這篇文章(如同雷同,是我抄的)。

  需要自行準備標定板,長相如下(因為快遞比較慢,自己先做了一個,但因為這個的精度直接影響到後面標定的精確度,建議還是買一塊給力一點的):

這裡寫圖片描述

  標定的流程是:角點提取->相機標定

  一.角點提取

  會用到比較重要的函式是:

//用於提取標定板的內角點,也就是提取上圖中每四個黑白格中間的那些角點
bool findChessboardCorners( InputArray image, Size patternSize, OutputArray corners, int flags=CALIB_CB_ADAPTIVE_THRESH+CALIB_CB_NORMALIZE_IMAGE );

  有四個引數:

  第一個“Image”,是拍攝到的棋盤影象,也就是上圖那樣的影象;

  第二個“patternSize”,即每個棋盤圖上內角點的行列數,一般情況下,行列數不要相同,便於後續標定程式識別標定板的方向,像上面那樣的板子就是Size(7, 5),也就是每行7個角點,每列5個角點;

  第三個“corners”,用於儲存檢測到的內角點影象座標位置,一般用元素是Point2f的向量來表示;

  第四個“flage”:用於定義棋盤圖上內角點查詢的不同處理方式,有預設值。

  另外返回值很重要,它會告訴你是不是真的從圖中找到了角點。如果後面想做成自動標定的程式,這個非常有用;

  例如如果輸入的影象如上圖所示,而我們的第二個引數是Size(10, 5),則會返回錯誤;

//用於在初步提取的角點資訊上進一步提取亞畫素資訊,降低相機標定偏差,該方法專門用來獲取棋盤圖上內角點的精確位置。而比較普遍的提取亞畫素角點的方法是cornerSubPix,這裡不做贅述
bool find4QuadCornerSubpix(InputArray img, InputOutputArray corners, Size region_size);

  有五個引數:

  第一個“mage”,輸入的Mat矩陣,最好是8位灰度影象,檢測效率更高;

  第二個“corners”,初始的角點座標向量,同時作為亞畫素座標位置的輸出,所以需要是浮點型資料,一般用元素是Pointf2f/Point2d的向量來表示。也即輸入上面findChessboardCorners函式的第三個引數。

  第三個“winSize”,大小為搜尋視窗的一半;

  第四個“zeroZone”,死區的一半尺寸,死區為不對搜尋區的中央位置做求和運算的區域。它是用來避免自相關矩陣出現某些可能的奇異性。當值為(-1,-1)時表示沒有死區;

  第五個“criteria”,定義求角點的迭代過程的終止條件,可以為迭代次數和角點精度兩者的組合;

//用於畫出求得的角點,以便檢視是否標定正確
void drawChessboardCorners( InputOutputArray image, Size patternSize, InputArray corners, bool patternWasFound );  

  有四個引數:

  第一個“image”,8位灰度或者彩色影象;

  第二個“patternSize”,每張標定棋盤上內角點的行列數,即findChessboardCorners的第二個引數;

  第三個“corners”,角點座標向量,可用find4QuadCornerSubpix函式的第二個引數輸出做輸入;

  第四個“patternWasFound”,標誌位,用來指示定義的棋盤內角點是否被完整的探測到,true表示被完整的探測到,函式會用直線依次連線所有的內角點,作為一個整體,false表示有未被探測到的內角點,這時候函式會以(紅色)圓圈標記處檢測到的內角點;

  總的查詢角點的示例程式碼如下:

Mat imageInput = imread("1.bmp");
Size board_size = Size(7, 5);//標定板上每行、列的角點數
vector<Point2f> image_points_buf;//快取每幅影象上檢測到的角點
/*提取角點*/
if (!findChessboardCorners(imageInput, board_size, image_points_buf))
{
    cout << "can not find chessboard corners!\n"; //找不到角點  
    return;
}
else
{
    Mat view_gray;
    cvtColor(imageInput, view_gray, CV_RGB2GRAY);
    /*亞畫素精確化*/
    find4QuadCornerSubpix(view_gray, image_points_buf, Size(5, 5)); //對粗提取的角點進行精確化  
    drawChessboardCorners(view_gray, board_size, image_points_buf, true); //用於在圖片中標記角點  
    imshow("Camera Calibration", view_gray);//顯示圖片  
    waitKey(0);     
}

  二.相機標定

  利用上面獲取到的影象角點(理論上需要三張影象,即三組資料,事實上以10~20張為宜,因為這樣誤差會比較小),便可以用calibrateCamera函式做攝像頭標定,計算出攝像頭的內參、外參和畸變引數了。當然前面程式碼在本人只做了一張影象的角點提取,可以改成求多張的,程式碼如下:

Size board_size = Size(7, 5);//標定板上每行、列的角點數
vector<Point2f> image_points_buf;//快取每幅影象上檢測到的角點
vector<vector<Point2f>> image_points_seq; //儲存檢測到的所有角點
/* 提取角點 */
char filename[10];
for (size_t image_num = 1; image_num <= 14; image_num++)
{
    sprintf_s(filename, "%d.bmp", image_num);
    Mat imageInput = imread(filename);
    if (!findChessboardCorners(imageInput, board_size, image_points_buf))
    {
        cout << "can not find chessboard corners!\n"; //找不到角點  
        return;
    }
    else
    {
        Mat view_gray;
        cvtColor(imageInput, view_gray, CV_RGB2GRAY);
        /*亞畫素精確化*/
        find4QuadCornerSubpix(view_gray, image_points_buf, Size(5, 5)); //對粗提取的角點進行精確化  
        drawChessboardCorners(view_gray, board_size, image_points_buf, true); //用於在圖片中標記角點  
        image_points_seq.push_back(image_points_buf);//儲存亞畫素角點  
        imshow("Camera Calibration", view_gray);//顯示圖片  
        waitKey(500);//停半秒
    }
    imageInput.release();
}
double calibrateCamera(InputArrayOfArrays objectPoints,  
                       InputArrayOfArrays imagePoints,  
                       Size imageSize,  
                       CV_OUT InputOutputArray cameraMatrix,  
                       CV_OUT InputOutputArray distCoeffs,  
                       OutputArrayOfArrays rvecs, OutputArrayOfArrays tvecs,  
                       int flags=0, TermCriteria criteria = 
                       TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, DBL_EPSILON));  

  引數好多,有九個之多。。。

  第一個“objectPoints”,為世界座標系中的三維點。在使用時,應該輸入一個三維座標點的向量集合。一般我們假定標定板放在z=0的平面上,然後依據棋盤上單個黑白方塊的大小(也可以直接都取10,如果不需要很準確的對映到現實事物的話)可以計算出每個內角點的世界座標。

  第二個“imagePoints”,為每一個內角點對應的影象座標點。也即是上面求得的各張影象的角點集合;

  第三個“imageSize”,為影象的畫素尺寸大小,在計算相機的內參和畸變矩陣時需要使用到該引數;

  第四個“cameraMatrix”為相機的內參矩陣。輸入一個Mat cameraMatrix即可,如Mat cameraMatrix=Mat(3,3,CV_32FC1,Scalar::all(0));

  第五個“distCoeffs“為畸變矩陣。輸入一個Mat distCoeffs=Mat(1,5,CV_32FC1,Scalar::all(0));即可。

  第六個“rvecs”為旋轉向量;應該輸入一個Mat型別的vector,即vector<Mat>rvecs;

  第七個“tvecs”為位移向量,和rvecs一樣,應該為vector<Mat> tvecs;

  第八個“flags”為標定時所採用的演算法。有如下幾個引數(直接不寫則依據下面引數描述中沒設引數的情況進行):

CV_CALIB_USE_INTRINSIC_GUESS:使用該引數時,在cameraMatrix矩陣中應該有fx,fy,u0,v0的估計值。否則的話,將初始化(u0,v0)影象的中心點,使用最小二乘估算出fx,fy。 
CV_CALIB_FIX_PRINCIPAL_POINT:在進行優化時會固定光軸點。當CV_CALIB_USE_INTRINSIC_GUESS引數被設定,光軸點將保持在中心或者某個輸入的值。 
CV_CALIB_FIX_ASPECT_RATIO:固定fx/fy的比值,只將fy作為可變數,進行優化計算。當CV_CALIB_USE_INTRINSIC_GUESS沒有被設定,fx和fy將會被忽略。只有fx/fy的比值在計算中會被用到。 
CV_CALIB_ZERO_TANGENT_DIST:設定切向畸變引數(p1,p2)為零。 
CV_CALIB_FIX_K1,…,CV_CALIB_FIX_K6:對應的徑向畸變在優化中保持不變。 
CV_CALIB_RATIONAL_MODEL:計算k4,k5,k6三個畸變引數。如果沒有設定,則只計算其它5個畸變引數。

  第九個“criteria“是最優迭代終止條件設定。

  在使用該函式進行標定運算之前,需要對棋盤上每一個內角點的空間座標系的位置座標進行初始化,標定的結果是生成相機的內參矩陣cameraMatrix、相機的5個畸變係數distCoeffs,另外每張影象都會生成屬於自己的平移向量和旋轉向量。

  具體的實現程式碼如下:

Size image_size;//影象的尺寸
Size board_size = Size(7, 5);     //標定板上每行、列的角點數
vector<Point2f> image_points_buf;  //快取每幅影象上檢測到的角點
vector<vector<Point2f>> image_points_seq; //儲存檢測到的所有角點
/*提取角點*/
char filename[10];
for (size_t image_num = 1; image_num <= IMGCOUNT; image_num++)
{
    sprintf_s(filename, "%d.bmp", image_num);
    Mat imageInput = imread(filename);
    if (!findChessboardCorners(imageInput, board_size, image_points_buf))
    {
        cout << "can not find chessboard corners!\n";//找不到角點  
        return;
    }
    else
    {
        Mat view_gray;
        cvtColor(imageInput, view_gray, CV_RGB2GRAY);
        /*亞畫素精確化*/
        find4QuadCornerSubpix(view_gray, image_points_buf, Size(5, 5));//對粗提取的角點進行精確化  
        drawChessboardCorners(view_gray, board_size, image_points_buf, true);//用於在圖片中標記角點  
        image_points_seq.push_back(image_points_buf);//儲存亞畫素角點  
        imshow("Camera Calibration", view_gray);//顯示圖片  
        //waitKey(500);//停半秒
    }
    image_size.width = imageInput.cols;
    image_size.height = imageInput.rows;
    imageInput.release();
}
/*相機標定*/
vector<vector<Point3f>> object_points; //儲存標定板上角點的三維座標,為標定函式的第一個引數
Size square_size = Size(10, 10);//實際測量得到的標定板上每個棋盤格的大小,這裡其實沒測,就假定了一個值,感覺影響不是太大,後面再研究下
for (int t = 0; t<IMGCOUNT; t++)
{
    vector<Point3f> tempPointSet;
    for (int i = 0; i<board_size.height; i++)
    {
        for (int j = 0; j<board_size.width; j++)
        {
            Point3f realPoint;
            //假設標定板放在世界座標系中z=0的平面上
            realPoint.x = i*square_size.width;
            realPoint.y = j*square_size.height;
            realPoint.z = 0;
            tempPointSet.push_back(realPoint);
        }
    }
    object_points.push_back(tempPointSet);
}
//內外引數物件
Mat cameraMatrix = Mat(3, 3, CV_32FC1, Scalar::all(0));//攝像機內參數矩陣
vector<int> point_counts;// 每幅影象中角點的數量  
Mat distCoeffs = Mat(1, 5, CV_32FC1, Scalar::all(0));//攝像機的5個畸變係數:k1,k2,p1,p2,k3
vector<Mat> tvecsMat;//每幅影象的旋轉向量
vector<Mat> rvecsMat;//每幅影象的平移向量
calibrateCamera(object_points, image_points_seq, image_size, cameraMatrix, distCoeffs, rvecsMat, tvecsMat, 0);//攝像頭標定

  到此我們已經完成了標定的過程,得到了攝像頭的各個引數,後面就可以用這些得到的引數來做攝像頭的矯正了。

  矯正可以使用下面的函式:

void undistort( InputArray src, OutputArray dst,InputArray cameraMatrix,InputArray distCoeffs,InputArray newCameraMatrix=noArray() );  

  有五個引數:

  第一個“src”,輸入引數,代表畸變的原始影象;

  第二個“dst”,矯正後的輸出影象,跟輸入影象具有相同的型別和大小;

  第三個“cameraMatrix”為之前求得的相機的內參矩陣;

  第四個“distCoeffs”為之前求得的相機畸變矩陣;

  第五個“newCameraMatrix”,預設跟cameraMatrix保持一致;

  具體程式碼如下:

/*用標定的結果矯正影象*/
for (size_t image_num = 1; image_num <= IMGCOUNT; image_num++)
{
    sprintf_s(filename, "%d.bmp", image_num);
    Mat imageSource = imread(filename);
    Mat newimage = imageSource.clone();
    undistort(imageSource, newimage, cameraMatrix, distCoeffs);
    imshow("source", imageSource);//顯示圖片 
    imshow("drc", newimage);//顯示圖片  
    waitKey(500);//停半秒
    imageSource.release();
    newimage.release();
}

  到此就完成了攝像頭的標定和影象的矯正的整個流程,如果想要知道標定的效果如何評定,可以參考上文提到過的參考文章,本文的很多函式說明基本是照搬的,只是做了一些程式碼上的拆分。

  如果想要把標定的結果儲存下載後面直接用,則可以如下程式碼儲存:

/*儲存內參和畸變係數,以便後面直接矯正*/
ofstream fout("caliberation_result.txt");//儲存標定結果的檔案
fout << "相機內參數矩陣:" << endl;
fout << cameraMatrix << endl << endl;
fout << "畸變係數:\n";
fout << distCoeffs << endl << endl << endl;
fout.close();

  讀取標定檔案檔案程式碼如下:

char read[100];
double getdata;
Mat cameraMatrix = Mat(3, 3, CV_32FC1, Scalar::all(0));//攝像機內參數矩陣 
Mat distCoeffs = Mat(1, 5, CV_32FC1, Scalar::all(0));//攝像機的5個畸變係數:k1,k2,p1,p2,k3
ifstream fin("caliberation_result.txt");//讀取儲存標定結果的檔案,以供矯正
fin >> read;
fin.seekg(3, ios::cur);
for (size_t j = 0; j < 3; j++)
    for (size_t i = 0; i < 3; i++)  
    {
        fin >> getdata;
        cameraMatrix.at<float>(j, i) = getdata;
        fin >> read;
    }
fin >> read;
fin.seekg(3, ios::cur);
for (size_t i = 0; i < 5; i++)
{
    fin >> getdata;
    distCoeffs.at<float>(i) = getdata;
    fin >> read;
}   
fin.close();

  本文的全部程式碼如下:

#include "opencv2/core/core.hpp"  
#include "opencv2/imgproc/imgproc.hpp"  
#include "opencv2/calib3d/calib3d.hpp"  
#include "opencv2/highgui/highgui.hpp"  
#include <iostream>  
#include <fstream>  

#define IMGCOUNT 20

using namespace cv;
using namespace std;

void main()
{
    Size image_size;//影象的尺寸
    Size board_size = Size(9, 6);     //標定板上每行、列的角點數
    vector<Point2f> image_points_buf;  //快取每幅影象上檢測到的角點
    vector<vector<Point2f>> image_points_seq; //儲存檢測到的所有角點
    /*提取角點*/
    char filename[10];
    for (size_t image_num = 1; image_num <= IMGCOUNT; image_num++)
    {
        sprintf_s(filename, "%d.bmp", image_num);
        Mat imageInput = imread(filename);
        if (!findChessboardCorners(imageInput, board_size, image_points_buf))
        {
            cout << "can not find chessboard corners!\n";//找不到角點  
            return;
        }
        else
        {
            Mat view_gray;
            cvtColor(imageInput, view_gray, CV_RGB2GRAY);
            /*亞畫素精確化*/
            find4QuadCornerSubpix(view_gray, image_points_buf, Size(5, 5));//對粗提取的角點進行精確化  
            drawChessboardCorners(view_gray, board_size, image_points_buf, true);//用於在圖片中標記角點  
            image_points_seq.push_back(image_points_buf);//儲存亞畫素角點  
            imshow("Camera Calibration", view_gray);//顯示圖片  
            waitKey(500);//停半秒
        }
        image_size.width = imageInput.cols;
        image_size.height = imageInput.rows;
        imageInput.release();
    }
    /*相機標定*/
    vector<vector<Point3f>> object_points; //儲存標定板上角點的三維座標,為標定函式的第一個引數
    Size square_size = Size(10, 10);//實際測量得到的標定板上每個棋盤格的大小,這裡其實沒測,就假定了一個值,感覺影響不是太大,後面再研究下
    for (int t = 0; t<IMGCOUNT; t++)
    {
        vector<Point3f> tempPointSet;
        for (int i = 0; i<board_size.height; i++)
        {
            for (int j = 0; j<board_size.width; j++)
            {
                Point3f realPoint;
                //假設標定板放在世界座標系中z=0的平面上
                realPoint.x = i*square_size.width;
                realPoint.y = j*square_size.height;
                realPoint.z = 0;
                tempPointSet.push_back(realPoint);
            }
        }
        object_points.push_back(tempPointSet);
    }
    //內外引數物件
    Mat cameraMatrix = Mat(3, 3, CV_32FC1, Scalar::all(0));//攝像機內參數矩陣
    vector<int> point_counts;// 每幅影象中角點的數量  
    Mat distCoeffs = Mat(1, 5, CV_32FC1, Scalar::all(0));//攝像機的5個畸變係數:k1,k2,p1,p2,k3
    vector<Mat> tvecsMat;//每幅影象的旋轉向量
    vector<Mat> rvecsMat;//每幅影象的平移向量
    calibrateCamera(object_points, image_points_seq, image_size, cameraMatrix, distCoeffs, rvecsMat, tvecsMat, 0);//相機標定
    /*用標定的結果矯正影象*/
    for (size_t image_num = 1; image_num <= IMGCOUNT; image_num++)
    {
        sprintf_s(filename, "%d.bmp", image_num);
        Mat imageSource = imread(filename);
        Mat newimage = imageSource.clone();
        undistort(imageSource, newimage, cameraMatrix, distCoeffs);
        imshow("source", imageSource);//顯示圖片 
        imshow("drc", newimage);//顯示圖片  
        sprintf_s(filename, "%d_d.bmp", image_num);
        imwrite(filename, newimage);//顯示圖片  
        waitKey(500);//停半秒
        imageSource.release();
        newimage.release();
    }
    /*儲存內參和畸變係數,以便後面直接矯正*/
    ofstream fout("caliberation_result.txt");//儲存標定結果的檔案
    fout << "相機內參數矩陣:" << endl;
    fout << cameraMatrix << endl << endl;
    fout << "畸變係數:\n";
    fout << distCoeffs << endl << endl << endl;
    fout.close();

    ///*讀取之前標定好的資料直接矯正*/
    //char read[100];
    //double getdata;
    //Mat cameraMatrix = Mat(3, 3, CV_32FC1, Scalar::all(0));//攝像機內參數矩陣 
    //Mat distCoeffs = Mat(1, 5, CV_32FC1, Scalar::all(0));//攝像機的5個畸變係數:k1,k2,p1,p2,k3
    //ifstream fin("caliberation_result.txt");//讀取儲存標定結果的檔案,以供矯正
    //fin >> read;
    //fin.seekg(3, ios::cur);
    //for (size_t j = 0; j < 3; j++)
    //  for (size_t i = 0; i < 3; i++)  
    //  {
    //      fin >> getdata;
    //      cameraMatrix.at<float>(j, i) = getdata;
    //      fin >> read;
    //  }
    //fin >> read;
    //fin.seekg(3, ios::cur);
    //for (size_t i = 0; i < 5; i++)
    //{
    //  fin >> getdata;
    //  distCoeffs.at<float>(i) = getdata;
    //  fin >> read;
    //} 
    //fin.close();

    //char filename[10];
    //for (size_t image_num = 1; image_num <= IMGCOUNT; image_num++)
    //{
    //  sprintf_s(filename, "%d.bmp", image_num);
    //  Mat imageSource = imread(filename);
    //  Mat newimage = imageSource.clone();
    //  undistort(imageSource, newimage, cameraMatrix, distCoeffs);
    //  imshow("source", imageSource);//顯示圖片 
    //  imshow("drc", newimage);//顯示圖片  
    //  sprintf_s(filename, "%d_d.bmp", image_num);
    //  imwrite(filename, newimage);//顯示圖片  
    //  waitKey(500);//停半秒
    //  imageSource.release();
    //  newimage.release();
    //}
}