본문 바로가기

Image Processing

[영상처리/C++/OpenCV] Calibration

카메라 캘리브레이션을 공부하고 코드실습을 진행해 보았습니다. 

 

Calibration

실제 눈으로 보게되는 세상은 3차원으로 이루어져 있지마나 카메라를 통해 얻는 이미지는 2차원으로 구성되어있습니다. 3차원의 점들이 2차원 이미지 상에서 어디에 투영되는지는  당시의 카메라의 위치 및 방향 사용되는 렌즈, 렌즈와 이미지 센서와의 거리, 렌즈와 이미지 센서가 이루는 각 등 카메라 내부의 영향을 크게 받습니다. 내부적인 영향을 줄여 3차원 -> 2차원, 2차원 -> 3차원으로의 정확한 계산을 하기 위해 내부 요인의 파라미터 값을 구하는 과정을 Calibration이라고 합니다.

 

2차원 이미지는 3차원 공간상의 점들을 perspective projection을 통해 얻어집니다. 핀홀 카메라 모델에서는 3차원 점들이 투영되는 과정, 변환관계를 다음 식으로 정리 합니다.

3D to 2D

 

카메라는 내부 파라미터, 외부 파라미터를 가지고 있습니다. 이번 캘리브레이션 실습은 내부 파라미터를 요구하는 과정이기에 외부 파라미터에 대한 설명은 간단하게 진행하겠습니다. 외부 파라미터는 카메라 좌표계와 월드 좌표계 사의 변환 관계로 회전, 평행이동 변환을 의미하는 파라미터입니다. 외부 파라미터는 내부 파라미터와 달리 고유 파라미터가 아니기 때문에 방향,위치, 월드 좌표계의 정의에 따라서 달라집니다. 따라서 내부 파라미터를 구하고 외부 파라미터를 구하는 과정을 진행할 수 있습니다.

 

 

내부 파라미터는 3가지가 있습니다.

  • 초점 거리 --> 렌즈 중심과 이미지 센서와의 거리
  • 주점 --> 카메라 렌즈의 중심에서 센서에 내린 수선의 발읠 영상좌표
  • 비대칭 계수 --> 이미지 센서의 cell array의 y축이 기울어진 정도

 

Focal length(초점 거리)

카메라 모델에서 말하는 초점거리는 f로 픽셀 단위로 표한합니다. 이미지 센서의 셀 크기에 대한 상대적인 값으로 표현하기도 합니다. 예를 들어 이미지 셀의 크기가 0.1mm이고 카메라 초점거리가 f = 500 piexel이라는 것은 셀의 카메라 중심과 이미지 센서와의 거리가 50mm 라는 의미입니다. fx와 fy를 구별 하여 사용하지만 현대 카메라는 차이가 없어 공통의 값을 사용해도 무방하다고 합니다. 이미지의 해상도에 따라 셀의 크기가 달라지므로 초점거리 또한 달라집니다.

 

Principal point(주점)

주점은 cx,cy로 표현하며 레즌 중심에서 이미지 센서에 내린 수선의 발의 영상좌표입니다. 이미지 센서의 중심과는 다른 의미입니다. 영상기하학점관점에서 단순한 이미지 센터보다 principal point가 훨씬 중요합니다.

 

Skew coefficient

비대칭계수는 이미지 센서의 cell array의 y축이 기울어진 정도를 나타냅니다. 현대의 카메라들은 이러한 비대칭 에러가 거의 없기 때문에 0의 값을 사용합니다.

 

코드 실습

체크 보드를 통한 캘리브레이션을 진행해 보았습니다. Zhang's method라고 불리는 이 기법을 통해서 외부 월드 좌표를 체크 보드를 통해서 설정하고 내부 파라미터를 구하는 과정을 진행해 보았습니다.

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/calib3d.hpp>
#include <librealsense2/rs.hpp>
#include <iostream>
#include <iomanip>

using namespace std;
using namespace cv;

#define CHESS_WIDHT 8
#define CHESS_HEIGHT 6
#define SQUAER_SIZE 30 // 30mm


int main(int argc, char** argv)
{
    vector<vector<Point3f>> object_point; // 모든 이미지에 대한 world 좌표
    vector<Point3f> obj; // 이미지당 world 좌표 , 같은 이미지를 사용하므로 다 같음, 평면 이미지 object이기 때문에 Z=0
    for (int y = 0; y < CHESS_HEIGHT; y++) {
        for (int x = 0; x < CHESS_WIDHT; x++) {
            obj.push_back(Point3f((float)x * SQUAER_SIZE, (float)y * SQUAER_SIZE, 0));
        }
    }
    
    vector<vector<Point2f>> Left_image_point;       // image 좌표계
    vector<vector<Point2f>> Right_image_point;

    string input_path = "C:/Users/dhwor/source/repos/imread,imshow/Calibration/dataset";
    string output_path = "C:/Users/dhwor/source/repos/imread,imshow/Calibration/ChessBoard/";
    vector<string> str;
    int num = 0;

    glob(input_path, str, false);   // 경로 내의 파일을 string으로 저장

    for(int idx=0; idx<str.size();idx+=2){
        Mat frame1,frame2 ,gray1, gray2;
        frame1 = imread(str[idx]);
        frame2 = imread(str[idx + 1]);

        cvtColor(frame1, gray1, COLOR_BGR2GRAY);
        cvtColor(frame2, gray2, COLOR_BGR2GRAY);

        vector<Point2f> Lcorners;        // Cheesboard corner   
        vector<Point2f> Rcorners;

        bool Lstate = findChessboardCorners(gray1, Size(8, 6), Lcorners, 3); 
        bool Rstate = findChessboardCorners(gray2, Size(8, 6), Rcorners, 3); 

        if(Lstate && Rstate){
            
            object_point.push_back(obj);

            cornerSubPix(gray1, Lcorners, Size(5, 5), Size(-1, -1),
                TermCriteria(TermCriteria::EPS + TermCriteria::MAX_ITER, 30, 0.001)); // revise corner
            // input, original output-> revise output , SearchSize,

            cornerSubPix(gray2, Rcorners, Size(5, 5), Size(-1, -1),
                TermCriteria(TermCriteria::EPS + TermCriteria::MAX_ITER, 30, 0.001));

            drawChessboardCorners(frame1, Size(8, 6), Lcorners, true);
            drawChessboardCorners(frame2, Size(8, 6), Rcorners, true);

            Left_image_point.push_back(Lcorners);
            Right_image_point.push_back(Rcorners);

            string Loutput = output_path + to_string(num) + "_Left.jpg";
            imwrite(Loutput, frame1);
            string Routput = output_path + to_string(num) + "_Right.jpg";
            imwrite(Routput, frame2);
            
            num++;
            cout << num << endl;
        }
    }

    Mat LCamera_matrix, LdistCoeffs, Lrvecs, Ltvecs;
    
    Mat RCamera_matrix, RdistCoeffs, Rrvecs, Rtvecs;
    
    calibrateCamera(object_point, Left_image_point, Size(640, 480), LCamera_matrix, LdistCoeffs, Lrvecs , Ltvecs);
    calibrateCamera(object_point, Right_image_point, Size(640, 480), RCamera_matrix, RdistCoeffs, Rrvecs, Rtvecs);

    // Left
    cout << "--------------Left result-------------" << endl;
    cout << "Camera Matrix " << endl;
    cout << LCamera_matrix << endl;
    cout << "DistCoeffs " << endl;
    cout << LdistCoeffs << endl;
    cout << "Roataiton" << endl;  // Rodrigues notation
    cout << Lrvecs << endl;
    cout << "Translation" << endl;
    cout << Ltvecs << endl;
    cout << endl;

    // Right
    cout << "--------------Right result------------" << endl;
    cout << "Camera Matrix " << endl;
    cout << RCamera_matrix << endl;
    cout << "DistCoeffs " << endl;
    cout << RdistCoeffs << endl;
    cout << "Roataiton" << endl;
    cout << Rrvecs << endl;
    cout << "Translation" << endl;
    cout << Rtvecs << endl;

    return 0;
}

 

 

 

 

일정한 간격의 체크보드를 사용합니다. 체크의 크기를 임의로 정해서 사용하게 됩니다.

 

체크 보드로 카메라 캘리브레이션을 통해 얻은 내부 파라미터와 Realsense d435 내장함수로 얻은 내부 파라미터를 비교해 보았습니다. 3 x 3 행렬 형태로 내부 파라미터를 획득 할 수 있습니다. Realsense d435 RGB 카메라가 left, right 2개가 있어서 2대의 카메라에 대해서 진행했습니다.

그리고나서 내장함수로 얻은 값들이 입니다.

Left, Right 순

큰 차이가 없음을 볼 수 있습니다.