2013年6月24日 星期一

EmguCV Image Process: Estimating Projective Relations in Images part1


EmguCV Image Process: Estimating Projective Relations in Images

參考OpenCV 2 Computer Vision Application Programming Cookbook第九章

介紹內容如下:

Calibrating a camera

Computing the fundamental matrix of an image pair

Matching images using random sample consensus

Computing a homography between two images

現行的許多照片、影像

都是由數位相機透過光經由鏡頭並投射在感光元件上來捕捉畫面的

因此這中間就產生了一些問題

關於從3D的現實空間,如何投射到2D的影像平面上

這之間的關係,成了很大的一個議題

接下來會介紹一系列與之相關的內容

像是如何透過多種不同的角度的影像

利用基本的投影關係來做電腦視覺的處理

利用不同的方法來增進之前介紹過的特徵配對


要先來介紹的就是:鏡頭矯正 (Calibrate a camera)

不同的攝影鏡頭會有不同的參數來矯正

這些參數通常只有在生產此鏡頭的廠商才會擁有

那如何利用一些電腦視覺的技術來回推這些參數

就是本節要介紹的

而OpenCV提供的方法,讓這非常非常複雜的數學運算

變成簡單的一項工作


 
可以發現在未經過鏡頭矯正的影像

棋盤都是扭曲變形的

這裡要透過鏡頭矯正把扭曲的棋盤恢復成正常的直線棋盤


首要要準備的就是一個棋盤影像(chessboard)

利用這個棋盤來對鏡頭做矯正

簡單來說OpenCV利用棋盤方正格子的角點產生一些3D畫面的點位

而這些點位只給定X跟Y座標,Z深度座標則設為0

以這些產生的點位與實際影像上的位置做投影的計算處理

擷取棋盤的角點位置非常的簡單

如下所示:
//input a chessboard image
Image<Gray, Byte> image = new Image<Gray, byte>("image.jpg");
//number of corners on the chessboard
Size boardSize = new Size(9, 6);
//Get the chessboard corners
PointF[] points = CameraCalibration.FindChessboardCorners(image, boardSize, Emgu.CV.CvEnum.CALIB_CB_TYPE.DEFAULT);
//Draw the corners
CameraCalibration.DrawChessboardCorners(image, boardSize, points);

EmguCV有直接實作CameraCalibration這個靜態類別

裡面提供了有關鏡頭矯正的靜態方法

這裡要記得提供棋盤的格子的數量

計算的方式是以格子與格子相連的角點位置

如下圖所示:

雖然標記的不是很明顯

但可以看出OpenCV所抓取的角點位置

因此要先計算這些角點位置的數量,才有辦法做角點偵測


這就是簡單的角點偵測的方式

但要做到鏡頭矯正可沒這麼容易

他需要一系列類似的棋盤影像

以不同的角度、方向來做擷取角點的動作

利用這些角點位置計算鏡頭的

內部參數(intrinsic parameters)與外部參數(extrinsic parameters)

其中內部參數包含了

鏡頭矩陣參數(camera matrix)與失真矩陣參數(distortion matrix)

而這兩個參數可用來取得反失真(undistort)的mapping影像

再利用mapping影像去把給定的影像作remapping的動作

才會得到矯正後的結果


因此這裡利用一個CameraCalibrator的類別來提供該功能

class CameraCalibrator
{
    //input points: the points in world coordinates
    List<MCvPoint3D32f[]> ObjectPoints;

    //the points positions in pixels
    List<PointF[]> ImagePoints;

    //output matrices
    IntrinsicCameraParameters IntrinsicParams;

    //flag to specify how calibration is done
    CALIB_TYPE Flag;

    //used in image undistortion
    Image<Gray, float> Map1, Map2;

    bool MustInitUndistort;

    public CameraCalibrator()
    {
        this.Flag = CALIB_TYPE.DEFAULT;
        this.MustInitUndistort = true;
        this.ImagePoints = new List<PointF[]>();
        this.ObjectPoints = new List<MCvPoint3D32f[]>(); 
    }

...

}
這個類別提供一些全域變數

包含紀錄3D點位的ObjectPoints

角點座標的ImagePoints

內部參數IntrinsicParams

矯正方法的列舉Flag

mapping影像Map1, Map2

跟一個控制初始反失真動作的布林變數MustInitUndistort

並在建構式中做初始的動作

/// <summary>
/// Open chessboard images and extract corner points
/// </summary>
/// <param name="fileList">image list path</param>
/// <param name="boardSize">number of corners on the chessboard</param>
/// <returns></returns>
public int AddChessboardPoints(string[] fileList, Size boardSize)
{
    //the points on the chessboard
    PointF[] imageCorners;
    MCvPoint3D32f[] objectCorners = new MCvPoint3D32f[boardSize.Height * boardSize.Width];

    //3D Scene Points:
    //Initialize the chessboard corners
    //in the chessboard reference frame
    //The corners are at 3D location (x,y,z) = (i,j,0)
    for (int i = 0; i < boardSize.Height; i++)
    {
        for (int j = 0; j < boardSize.Width; j++)
        {
            objectCorners[i * boardSize.Width + j] = new MCvPoint3D32f(i, j, 0.0f);
        }
    }

    //2D image points:
    Image<Gray, Byte> image;//to contain chessboard image
    int successes = 0;      
    //for all viewpoint
    for (int i = 0; i < fileList.Length; i++)
    {
        //Open the image
        image = new Image<Gray, Byte>(fileList[i]);
        //Get the chessboard corners
        imageCorners = CameraCalibration.FindChessboardCorners(image, boardSize, CALIB_CB_TYPE.DEFAULT);

        if (imageCorners == null) continue;
                        
        //Get subpixel accuracy on the corners
        CvInvoke.cvFindCornerSubPix(image, imageCorners, 
            boardSize.Height * boardSize.Width,
            new Size(5, 5), new Size(-1, -1),
            new MCvTermCriteria(30, 0.1));
        //If we have a good board, add it to our data
        if (imageCorners.Length == boardSize.Height * boardSize.Width)
        {
            //Add image and scene points form one view
            AddPoints(imageCorners, objectCorners);
            successes++;
        }
    }
    return successes;
}
接著這個方法

先利用給定的棋盤角點的數量

來產生配對的3D座標(X, Y, Z)

再把一系列的棋盤影像讀入

這邊是推薦10~20張不同角度、深度的影像

並利用CameraCalibration.FindChessboardCorners做角點偵測的動作

取得正確數量的角點位置後

把角點座標的集合與配對的3D座標的集合

丟入AddPoints的方法內

/// <summary>
/// Add scene points and corresponding image points
/// </summary>
/// <param name="imageCorners">2D image points from one view</param>
/// <param name="objectCorners">corresponding 3D scene points</param>
void AddPoints(PointF[] imageCorners, MCvPoint3D32f[] objectCorners)
{ 
    //2D image points from one view
    this.ImagePoints.Add(imageCorners);
    //corresponding 3D scene points
    this.ObjectPoints.Add(objectCorners);
}
把角點座標的集合與配對的3D座標的集合

加入到全域變數ImagePoints與ObjectPoints

接著就開始做鏡頭的矯正動作

/// <summary>
/// Calibrate the camera
/// </summary>
/// <param name="imageSize">image size</param>
/// <returns>return the re-projection error</returns>
public double Calibrate(Size imageSize)
{
    //undistorter must be reinitialized
    this.MustInitUndistort = true;

    MCvPoint3D32f[][] objectPoints = this.ObjectPoints.ToArray();
    PointF[][] imagePoints = this.ImagePoints.ToArray();
    this.IntrinsicParams = new IntrinsicCameraParameters();
    ExtrinsicCameraParameters[] extrinsicParams;

    return CameraCalibration.CalibrateCamera(
        objectPoints,         //the 3D points
        imagePoints,          //the image points
        imageSize,            //image size
        this.IntrinsicParams,       //output distortion matrix
        this.Flag,            //set options
        out extrinsicParams);  //output rotations and translations
}
這邊使用CameraCalibration.CalibrateCamera這個方法

要帶入配對的3D座標集合、角點座標集合、影像大小、

內部參數、矯正方法列舉、以及外部參數

這裡要注意的是全域變數ObjectPoints與ImagePoints

是採用List這個型別來處理集合

但在CameraCalibration.CalibrateCamera方法中

是要接收二維矩陣的型別

因此要透過一個ToArray()的轉換

而CameraCalibration.CalibrateCamera會回傳一個double的數值

這個數值的值越小,表示矯正的過程越好

數值越大,可能之後矯正出來的效果就會有問題

處理完後,IntrinsicParams裡面就會存放著

鏡頭矩陣參數(camera matrix)跟失真矩陣參數(distortion matrix)

分別是IntrinsicParams.IntrinsicMatrix與IntrinsicParams.DistortionCoeffs

/// <summary>
/// remove distortion in an image (after calibration)
/// </summary>
/// <param name="image"></param>
/// <returns></returns>
public Image<Gray, Byte> Remap(Image<Gray, Byte> image)
{
    Image<Gray, Byte> undistorted = new Image<Gray, byte>(image.Size);
    if (this.MustInitUndistort)//called once per calibration
    {
        this.Map1 = new Image<Gray, float>(image.Size);
        this.Map2 = new Image<Gray, float>(image.Size);
        CvInvoke.cvInitUndistortRectifyMap(
            this.IntrinsicParams.IntrinsicMatrix, //computed camera matrix
            this.IntrinsicParams.DistortionCoeffs,//computed distortion matrix 
            IntPtr.Zero,                          //optional rectification (none)
            this.IntrinsicParams.IntrinsicMatrix, //camera matrix to generate undistorted
            this.Map1,                            //the mapping funtions 
            this.Map2);                           //the mapping funtions 
                
        this.MustInitUndistort = false;
    }

    CvInvoke.cvRemap(image, undistorted, Map1, Map2, (int)INTER.CV_INTER_LINEAR, new MCvScalar(0));
    return undistorted;
}
接著提供反失真的處理

一開始先初始一張與來源影像一樣大的結果影像

與初始兩張與來源影像一樣大但是深度為32bit的mapping影像

接著透過CvInvoke.cvInitUndistortRectifyMap這個方法

帶入鏡頭矩陣參數(camera matrix)、失真矩陣參數(distortion matrix)

第三個參數實際上是一個3X3的矩陣

由於這裡的條件下用不到於是帶入IntPtr.Zero
(程式會轉成帶入單位矩陣(identity matrix))

第四個參數在EmguCV的文件上是:The new camera matrix

也就是輸出影像的鏡頭矩陣參數(camera matrix)

在參考書籍OpenCV 2 Computer Vision Application Programming Cookbook的範例

是帶入一個空的矩陣集合 cv::Mat()

這裡我嘗試了很多次

無論是帶入空的或是單位矩陣,都會有問題

直至查到OpenCV的文件寫說:

In case of a monocular camera, newCameraMatrix is usually equal to cameraMatrix

這一切才豁然開朗

直接帶入原本的鏡頭矩陣參數即可

而後兩個參數就是mapping的影像


之後在呼叫CvInvoke.cvRemap這個方法

帶入原始影像image、以及結果影像undistorted、

兩個mapping影像、及補差值的方法,補差值的顏色

//input a chessboard image
Image<Gray, Byte> image = new Image<Gray, byte>("chessboardImage\image1.jpg");
//chessboard images folder
string[] fileList = Directory.GetFiles("chessboardImage\");
CameraCalibrator cameraCalibrator = new CameraCalibrator();
cameraCalibrator.AddChessboardPoints(fileList, boardSize);
Console.WriteLine(cameraCalibrator.Calibrate(image.Size));
Image<Gray, Byte> result = cameraCalibrator.Remap(image);
CameraCalibrator類別的使用方式如上

把結果影像印出來

有看出其中差異了嗎?

把兩張影像放在一起比較看看

經過鏡頭矯正過後

鏡頭的桶狀變形就消失了~

原本的棋盤是扭曲變形的

矯正過後就會恢復正常,呈現直線狀態

這就是camera calibration


而對於攝影成像利用矩陣描述的部分

可以參考維基百科


下一篇介紹如何透過兩張不同角度或不同攝影機拍攝的影像求出fundamental matrix

沒有留言:

張貼留言