camera로부터의 입력 이미지를 OpenCV 함수로 받아 IplImage 형태로 저장한 것과 OpenGL 함수로 그린 그래픽 정보를 합성하기
Way #1.
OpenCV의 카메라 입력으로 받은 image frame을 texture로 만들어 OpenGL의 디스플레이 창에 배경 (평면에 texture mapping)으로 넣고 여기에 그래픽을 그려 display하는 방법
ref. http://cafe.naver.com/opencv/12266
테스팅 중 발견한 문제점: cvRetrieveFrame() 함수를 while 아래에서 돌려 cvShowImage() 함수로 보여 주는 대신 glutDisplayFunc() 함수에서 불러 glutMainLoop() 함수로 돌리면 시간이 많이 걸린다. (* cvGrabFrame() 함수의 경우는 괜찮음.)
Textures are simply rectangular arrays of data - for example, color data, luminance data, or color and alpha data. The individual values in a texture array are often called texels.
The data describing a texture may consist of one, two, three, or four elements per texel, representing anything from a modulation constant to an (R, G, B, A) quadruple.
A texture object stores texture data and makes it readily available. You can now control many textures and go back to textures that have been previously loaded into your texture resources.
2. lens distortion(kappa1, kappa2)을 가지고 rectification
패턴 인식이 성공적인 경우 당연히 카메라 캘리브레이션 결과가 정확해지며, 이로부터 가상의 물체를 합성하기 위해 필요한 object 또는 graphic coordinate을 실시간으로 계산할 수 있다. 현재 우리 프로그램에서 패턴 인식이 실패하는 원인은 직선 검출의 오차인데, 이 오차의 원인으로는 여러가지가 있지만 가장 큰 것은 렌즈 왜곡이다. (현재 렌즈 왜곡을 고려하지 않고 있다.) 그래서 실제로는 하나의 직선에 대해 여러 개 (2-3개)의 직선을 검출하며 (NMS 알고리즘만으로는 이 오차를 줄이는 데 한계를 보이고 있어), 이로부터 계산된 교차점들의 위치 좌표 오차는 cross ratio 계산에 결정적인 오차로 작용한다. 현재 방식의 패턴 생성과 패턴 인식은 cross ratios 값에 절대적으로 의존하고 있기 때문에 이 문제를 반드시 해결해야 한다. 그러므로 렌즈 왜곡을 고려하여 입력 이미지를 펴서 (rectification) 기존의 패턴 인식 알고리즘을 적용하자.
Learning OpenCV: 396p
"OpenCV provides us with a ready-to-use undistortion algorithm that
takes a raw image and the distortion coefficients from
cvCalibrateCamera2() and produces a corrected image (see Figure 11-12).
We can access this algorithm either through the function cvUndistort2(),
which does everything we need in one shot, or through the pair of
routines cvInitUndistortMap()
and cvRemap(),
which allow us to handle things a little more efficiently for video or
other situations where we have many images from the same camera. ( * We
should take a moment to clearly make a distinction here between
undistortion, which mathematically removes lens distortion, and rectifi
cation, which mathematically aligns the images with respect to each
other. )
(1) 고정되어 있는 것으로 가정한 카메라의 내부 파라미터 값들을 구하고 (2) 실시간으로 들어오는 이미지 프레임마다 카메라의 회전과 이동을 계산하기 위하여 Tsai 알고리즘을 쓰기로 하고, C 또는 C++로 구현된 소스코드 또는 라이브러리를 찾아서 붙여 보기로 한다.
Try #1.
처음에는 CMU의 Reg Willson가 C로 짠 Tsai Camera Calibration 코드 에서 필요한 부분을 include하여 쓰려고 했는데, C++ 문법에 맞지 않는 구식 C 문법으로 코딩된 부분이 많아서 고치는 데 애를 먹었다. (Xcode의 C++ 프로젝트에서 .c 파일을 include하면 compile은 되지만, linking error가 난다. 때문에 .c를 .cpp로 바꾸어야 함.) 그런데 결정적으로, "cal_main.cpp" 파일에 정의된, 캘리브레이션의 최종 결과값을 주는 함수들이 호출하는 optimization을 실행하는 함수 lmdif_()가 Fortan 파일 "lmdif.f"에 정의되어 있고, Fortran을 C로 변환해 주는 "f2c.h"에 의해 이것을 "lmdif.c"로 하여 가지고 있다는 문제가 있었다. lmdif.c를 lmdif.cpp 형태로 만들기 위해서는 Fortran 언어와 Fortran을 C++로 변환하는 방법을 알아야 하므로, 결국 포기했다.
Try #2.
Michigan State University Charles B. Owen의 Display-Relative
Calibration (DRC)을 구현한 DRC 프로그램( DRC.zip )에서 카메라 캘리브레이션에 Tsai의 알고리즘 libtsai.zip을 쓰고 있다. 이 라이브러리는 위의 C 코드를 C++로 수정하면서 "CTsai"라는 클래스를 사용하고 여러 함수들을 수정/보완/결합한 것인데, Visual Studio 용 프로젝트 프로그램을 만들면서 Windows 환경에 기반하여 MFC를 활용하였다. 그래서 이것을 나의 Mac OS X 기반 Xcode 프로젝트에서 그대로 가져다 쓸 수는 없다. 용법은 다음과 같다.
클래스 형태의 템플릿( CLmdif )으로 선언된 "lmdif"의 member function "Lmdif"를 호출할 때,
min/Lmdif.h:48
template<class T> class CLmdif : private CLmdif_
{
int Lmdif(T *p_user, bool (T::*p_func)(int m, int n, const double *parms, double *err),
int m, int n, double *x, double *fvec, double *diag, int *ipvt, double *qtf)
};
후자인 같은 member function, ncc_compute_exact_f_and_Tz_error()를 인자로 넣고 있고 (위 부분 코드들 중 오렌지 색 부분), 컴파일 하면 이 부분을 <unknown type>으로 인식하지 못 하겠다는 에러 메시지를 보낸다. 그리고 다음과 같은 형태를 추천한다고 한다.
function pointer의 형태가 틀린 모양인데, 오렌지색 부분을 그냥 함수가 아닌 어떤 class의 non-static member function을 가리키는 pointer로 &CTsai::ncc_compute_exact_f_and_Tz_error 이렇게 바꾸어 주면, 에러 메시지가 다음과 같이 바뀐다.
error: no matching function for call to 'CLmdif<CTsai>::Lmdif(CTsai* const, bool (*)(int, int, const double*, double*), int&, const int&, double [3], NULL, NULL, NULL, NULL)'
연두색 부분 대신 CTsai::ncc_compute_exact_f_and_Tz_error 이렇게 바꾸어 주면, 에러 메시지가 다음과 같다.
error: no matching function for call to 'CLmdif<CTsai>::Lmdif(CTsai* const, bool (&)(int, int, const double*, double*), int&, const int&, double [3], NULL, NULL, NULL, NULL)'
해결:
편법으로, class CLmdif를 클래스 형 템플릿이 아닌 그냥 클래스로 바꾸어서 선언하고 연두색 부분처럼 호출하면 에러는 안 나기에 일단 이렇게 넘어가기로 한다.
문제점#2.
코드에서 Windows OS 기반 MFC를 사용하고 있어 Mac OS X에서 에러가 난다.
해결:
MFC를 사용하는 "StdAfx.h"는 모두 주석 처리한다.
문제점#3.
Lmdif.h
... 기타 등등의 문제점들을 해결하고, 캘리브레이션을 수행한 결과가 맞는지 확인하자.
source code:
if (
CRimage.size() > 0 ) // if there is a valid point with its cross
ratio
{
correspondPoints(indexI, indexW, p, CRimage,
linesYorder.size(), linesXorder.size(), world, CRworld, dxList.size(),
dyList.size(), iplMatch, scale );
}
cvShowImage( "match", iplMatch );
cvSaveImage( "match.bmp", iplMatch );
아래 사진은 구해진 카메라 내부/외부 파라미터들을 가지고 (1) 실제 패턴의 점에 대응하는 이미지 프레임 (image coordinate) 상의 점을 찾아 (reprojection) 보라색 원으로 그리고, (2) 실제 패턴이 있는 좌표 (world coordinate)를 기준으로 한 graphic coordinate에 직육면체 cube를 노란색 선으로 그린 결과이다.
이미지 프레임과 실제 패턴 상의 점을 1 대 1로 비교하여 연결한 16쌍의 대응점
구한 카메라 파라미터를 가지고 실제 패턴 위의 점들을 이미지 프레임에 reproject한 결과 (보라색 점)와 실제 패턴의 좌표를 기준으로 한 그래픽이 이미지 프레임 상에 어떻게 나타나는지 그린 결과 (노란색 상자)
위 왼쪽 사진에서 보여지는 16쌍의 대응점들의 좌표값을 "이미지 좌표(x,y) : 패턴 좌표 (x,y,z)"로 출력한 결과:
camera parameter
focus = 3724.66
principal axis (x,y) = 168.216, 66.5731
kappa1 (lens distortion) = -6.19473e-07
skew_x = 1
대응점 연결에 오차가 없으면, 즉, 패턴 인식이 잘 되면, Tsai 알고리즘에 의한 카메라 파라미터 구하기가 제대로 되고 있음을 확인할 수 있다. 하지만, 현재 full optimization (모든 파라미터들에 대해 최적화 과정을 수행하는 것)으로 동작하게 되어 있고, 프레임마다 모든 파라미터들을 새로 구하고 있기 때문에, 속도가 매우 느리다. 시험 삼아 reprojection과 간단한 graphic을 그리는 과정은 속도에 큰 영향이 없지만, 그전에 카메라 캘리브레이션을 하는 데 필요한 계산 시간이 길다. 입력 프레임이 들어오는 시간보다 훨씬 많은 시간이 걸려 실시간 구현이 되지 못 하고 있다.
따라서, (1) 내부 파라미터는 첫 프레임에서 한 번만 계산하고 (2) 이후 매 프레임마다 외부 파라미터 (카메라의 회전과 이동)만을 따로 계산하는 것으로 코드를 수정해야 한다.
Test on the correspondences of feature points
특징점 대응 시험
교점의 cross ratio 값을 구하고, 그 값과 가장 가까운 cross ratio 값을 가지는 점을 패턴에서 찾아 대응시킨다.
Try #1. one-to-all
입력 영상에서 검출한 직선들로부터 생기는 각 교점에서 수평 방향으로 다음 세 개의 교점, 수직 방향으로 다음 세 개의 교점을
지나는 직선에 대한 cross ratio (x,y)값을
구한다. 이상적으로, 1에서 구한 cross ratio 값과 일치하는 cross ratio 값을 가지는 패턴의 격자점이 입력 영상의 해당
교차점과 실제로 대응하는 점이라고 볼 수 있다.
직선 검출에 오차나 오류가 적을 경우, 아래 테스트 결과에서 보듯 입력 영상의 교차점에 대해 실제 패턴의 직선을 1대 1로
즉각적으로 찾는다. 즉, 입력 영상의 한 점에서의 수평 방향 cross ratio 값에 대해 패턴의 모든 수평선들의 cross
ratio 값을 일일이 대조하여 가장 근접한 값을 가지는 직선을 대응시키는 방식이다. (아래 오른쪽 사진은 같은 방식으로 수직
방향 cross ratio 값을 가지고 대응되는 직선을 찾는 경우임.) (point-to-line)
수평선 위의 점들에 대한 cross ratio 값만 비교한 결과
수선 위의 점들에 대한 cross ratio 값만 비교한 결과
입력 영상에서 하나의 교차점의 x방향 cross ratio 값과 같은 cross ratio 값을 가지는 세로선을
실제 패턴에서 찾고, y방향 cross
ratio 값에 대해서 가로선을 찾으면, 패턴 위에 그 세롯선과 가로선이 교차하는 점 하나가 나온다. 입력 이미지 상의 한 점에 대해 패턴의 모든 직선을 (가로선의 개수+세로선의 개수) 번 비교하여 대응점을 연결하는 것이다. (point-to-point)
(패턴 인식이 성공적인 경우)
(잘못된 대응점 연결이 발생한 경우)
source code:
void matchXY ( vector<CvPoint2D32f> &p, vector<CvPoint2D32f> &CRimage, int numIx, int numIy, vector<CvPoint3D32f> &world, vector<CvPoint2D32f> &CRworld, int numPx, int numPy, IplImage* iplMatch, CvPoint2D32f scale )
{
for( int i = 0; i < numIx; i++ ) // points in x-direction on the input image
{
// check if x-component of the point is valid
if( -1 == CRimage[i*numIy+0].x )
{
cout << endl << "could not make matching in x-direction" << endl;
continue;
}
// CvScalar generateRandomColor(unsigned char thR, unsigned char thG, unsigned char thB) defined in "matching.h"
CvScalar colorMatch = generateRandomColor(50,50,50);
for( int j = 0; j < numIy; j++ ) // points in y-direction on the input image
{
// check if y-component of the point is valid
if( -1 == CRimage[i*numIy+j].y )
{
cout << endl << "could not make matching in y-direction" << endl;
continue;
}
// to find the x-index of the corresponding point
int indexPx = 0;
float errX_min = fabs( CRimage[i*numIy+j].x- CRworld[indexPx*numPy+0].x );
// search points in x-direction on the real pattern
for( int wx = 0; wx < numPx; wx++ )
{
float errX = CRimage[i*numIy+j].x - CRworld[wx*numPy+0].x;
if ( fabs(errX) < errX_min )
{
errX_min = fabs(errX);
indexPx = wx;
}
}
// to find the y-index of the corresponding point
int indexPy = 0;
float errY_min = fabs( CRimage[i*numIy+j].y - CRworld[0*numPy+indexPy].y );
// search points in y-direction on the real pattern
for( int wy = 0; wy < numPy; wy++ )
{
float errY = CRimage[i*numIy+j].y - CRworld[0*numPy+wy].y;
if ( fabs(errY) < errY_min )
{
errY_min = fabs(errY);
indexPy = wy;
}
}
// cout << endl << i << ", " << j << " point in the input frame is matched with "
// << indexPy << "-th point in the real pattern" << endl;
// draw the line to connect "world" point and "image" point
CvPoint pointImage = cvPoint(cvRound(p[i*numIy+j].x), cvRound(IMG_HEIGHT + p[i*numIy+j].y));
CvPoint pointPattern = cvPoint(cvRound(world[indexPx*numPy+indexPy].x*scale.x), cvRound(world[indexPx*numPy+indexPy].y*scale.y));
그러므로 현재는 (1) 입력 영상에서 한 직선 위에 있는 것으로 추산된 일련의 점들에서의 cross ratio 값들의 수치적
경향을 고려하지 않고 있으며, (2) 입력 영상에 실제 패턴의 어느 부분(위치나 범위)이 잡힌 것인지를 판단하지 않고 무조건 전체
패턴의 모든 격자점들에 대해서 cross ratio 값을 비교하고 있다.
Rahbar, K. and Pourreza, H. R. 2008. Inside looking out camera pose
estimation for virtual studio. Graph. Models 70, 4 (Jul. 2008),
57-75. DOI= http://dx.doi.org/10.1016/j.gmod.2008.01.001
현재 코드는 검은색 바탕에 흰색 사각형을 그리게 되는데, 소수 값을 정수 값 (픽셀의 위치 좌표)으로 변환하는 과정에서 오차가 발생한다. 얼핏 격자 무늬로 보이는 오른쪽 그림을 확대한 왼쪽 그림을 보면, 격자 사이가 벌어지거나 겹치는 부분이 생긴다는 것을 알 수 있다.
그리는 방법을 달리했더니 해결된다. 이미지와 같은 높이의 세로 막대들을 하얀색으로 먼저 그리고 나서, 이미지와 같은 너비의 가로 막대들을 하얀색으로 그리고, 막대들이 서로 교차하는 (하얀색으로 두 번 그린 셈인) 부분을 다시 검은색으로 그렸다. (이건 뭔... 컴퓨터 비전도 이미지 프로세싱도 아니고 그렇다고 컴퓨터 그래픽스라고 보기도 우습고, 중학교 수학 경시대회 난이도 중상 정도의 문제를 푸는 기분이다. )
세로선 40개, 가로선 30개로 생성된 패턴 (400x300pixels)
오른쪽의 패턴을 7배 확대한 영상
/* Draw grid pattern
using kyu's pattern generator with optimal cross ratios
2010, lym
// calculate cross ratios in the world coordinate on real pattern
void crossRatioWorld( vector<CvPoint2D32f>& CRworld, vector<CvPoint3D32f>& world, int dxListSize, int dyListSize, CvPoint2D32f scale )
{
// vector<CvPoint2D32f> crossRatioWorld; // cross ratios in the world coordinate on real pattern
float crX = -1.0, crY = -1.0;
for( int i = 0; i < dxListSize; i++ )
{
for( int j = 0; j < dyListSize; j++ )
{
CRworld.push_back(cvPoint2D32f(crX, crY));
}
}
// "cr[iP] = p1 * p3 / ((p1 + p2) * (p2 + p3))" in psoBasic.cpp: 316L
// that is (b-a)(d-c)/(c-a)(d-b) with 4 consecutive points, a, b, c, and d
float a, b, c, d;
// cross ratios in horizontal lines
for( int i = 0; i < dxListSize-3; i++ )
{
a = world[i*dyListSize].x;
b = world[(i+1)*dyListSize].x;
c = world[(i+2)*dyListSize].x;
d = world[(i+3)*dyListSize].x;
swPark_2000rti 439쪽: In the initial identification process, we first extract and identify vertical and horizontal lines of the pattern by comparing their cross-ratios, and then we compute the intersections of the lines. Theoretically with this method, we can identify feature points in every frame automatically, but several situations cause problems in the real experiments.
박승우_1999전자공학회지 94쪽: 초기 인식과정에서는 패턴 상의 교점을 인식하기 위해 패턴의 제작과정에서 설명한 것처럼 영상에서 구해진 가로선과 세로선의 Cross-ratio를 패턴의 가로선과 셀로선이 가지는 Cross-ratio와 비교함으로써 몇번째 선인지를 인식하게 된다. 이러한 방법을 이용해 영상으로부터 자동으로 특징점을 찾고 인식할 수 있지만, 실제 적용 상에서는 몇 가지 제한점이 따르게 된다.
0. NMS (Non Maximum Suppression)을 적용한 Hough transform에 의한 Line 찾기
OpenCV 라이브러리의 HoughLines2() 함수는 전에 기술한 바( http://leeway.tistory.com/801 )와 같이 실제 패턴에서는 하나의 직선 위에 놓인 점들에 대해 이미지 프레임에서 검출된 edges을 가지고 여러 개의 직선을 찾는 결과를 보인다. 이는 HoughLines2()
함수가 출력하는, 직선을 정의하는 두 파라미터 rho와 theta에 대해 ( x*cos(theta) + y*sin(theta) = rho ) 계산된 값들이 서로 비슷하게 나오는 경우에 최적값을 선별하는 과정을 거치지 않고 모든 값들을 그대로 내보내기 때문이다. 그래서 OpenCV의 이 함수를 이용하지 않고, 따로 Hough transform을 이용하여 선을 찾는 함수를 만들되 여기에 NMS (Non Maximum Suppression)를 적용하도록 해 보았다. 하지만 이 함수를 실시간 비디오 카메라 입력에 대해 매 프레임마다 실행시키면 속도가 매우 느려져 쓸 수 없었다. 그래서, 속도 면에서 월등한 성능을 보이는 OpenCV의 HoughLines2()
함수를 그대로 따 오고 대신 여기에 NMS 부분을 추가하여 수정한 함수를 매 입력 프레임마다 호출하는 방법을 택하였고, 실시간 처리가 가능해졌다. (->소스코드)
산출된 수직선들을 이미지 프레임의 왼쪽에서부터 오른쪽으로 나타난 순서대로 번호를 매기고 (아래 그림의 붉은색 번호), 수평선들을 위로부터 아래로 나타난 순서대로 번호를 매긴다 (아래 그림의 푸른색 번호). 이 과정에서 수직선의 경우 x절편, 수평선의 경우 y절편의 값을 기준으로 하여 계산하였다.
아래 코드에서 "line.x0"가 "line" 직선의 x절편임
// rearrange lines from left to right
void indexLinesY ( CvSeq* lines, IplImage* image )
{
// retain the values of "rho" & "theta" of found lines
int numLines = lines->total;
// line_param line[numLines]; 이렇게 하면 나중에 이 변수를 밖으로 빼낼 때 (컴파일 에러는 안 나지만) 문제가 됨.
line_param *line = new line_param[numLines];
for( int n = 0; n < numLines; n++ )
{
float* newline = (float*)cvGetSeqElem(lines,n);
line[n].rho = newline[0];
line[n].theta = newline[1];
}
// rearrange "line" array in geometrical order
float temp_rho, temp_theta;
for( int n = 0; n < numLines-1; n++ )
{
for ( int k = n+1; k < numLines; k++ )
{
float x0_here = line[n].rho / cos(line[n].theta);
float x0_next = line[k].rho / cos(line[k].theta);
if( x0_here > x0_next ) {
temp_rho = line[n].rho; temp_theta = line[n].theta;
line[n].rho = line[k].rho; line[n].theta = line[k].theta;
line[k].rho = temp_rho; line[k].theta = temp_theta;
}
}
}
// calculate the other parameters of the rearranged lines
for( int n = 0; n < numLines; n++ )
{
line[n].a = cos(line[n].theta);
line[n].b = sin(line[n].theta);
line[n].x0 = line[n].rho / line[n].a;
line[n].y0 = line[n].rho / line[n].b;
void indexLinesY( CvSeq* lines, IplImage* image ) 함수를 line_param*
indexLinesY( CvSeq* lines, IplImage* image )라고 바꾸어 structure로 선언한
line_param 형태의 배열을 출력하도록 하고, 이 출력값을 교점을 구하는 함수의 입력으로 하면
line_param
line[numLines];
이렇게 함수 안에서 선언했던 부분이 함수 밖으로 출력되어 다른 함수의 입력이 될 때 입력값이
제대로 들어가지 않는다. 다음과 같이 바꾸어 주어야 함.
이미지 프레임에서 찾은 수평선들을 보면 제일 위쪽의 직선이 0번이 아니라 4번부터 순번이 매겨져 있다. 프레임 바깥에 (위쪽에) 세 개의 직선이 더 있다는 뜻인데...
수직성 상의 edges 검출 영상
수평선 상의 edges 검출 영상
수직선들을 왼쪽부터 오른쪽으로, 수평선들을 위에서 아래로 정열한 결과
왼쪽 두 개는
line detection에 입력으로 쓰인 영상이고, 마지막 것은 이로부터 순서대로 정열한 직선을 규정하는 매개변수 출력값이다. 0번부터 3번 수평선의 y절편 값이 음수로 나타나고 있다.
2. 교점의 순서 매기기
격자 무늬의 직선들의 교점(intersections)을 과정1에서 계산한 직선의 순번을 이용하여 indexing한다. 빨간 세로선 0번과 파란 가로선 0번의 교점은 00번, 이런 식으로.
// index intersection points of lines in X and Y
CvPoint* indexIntersections ( line_param* lineX, line_param* lineY, int numLinesX, int numLinesY, IplImage* image )
// find intersections of lines, "linesX" & "linesY", and draw them in "image"
{
int numPoints = (numLinesX+1) * (numLinesY+1);
CvPoint *p = new CvPoint[numPoints]; // the intersection point of lineX[i] and lineY[j]
char txt[100]; // text to represent the index number of an intersection
입력 영상 input을 단일 채널 temp로 바꾸어 1차 DoG 필터링을 하여 검출된 edges를 양 방향 세기 비교와 NMS를 통해 수평 방향과 수직 방향으로 나눈 영상 detected edges를 입력으로 하여 Hough transform에 NMS를 적용하여 line detection을 한 결과를 input 창에 그리고, 이미지 프레임 좌표를 기준으로 검출된 직선들에 순서를 매겨 이로부터 교차점의 위치와 순번을 계산하여 input 창에 표시한다.
현재 상태의 문제점: (1) 패턴과 카메라 모두 정지하여 입력 영상(상좌)이 고정된 경우에, DoG 필터링한 결과(중)는 비교적 안정적이지만 수평, 수직 방향 세기 비교와 NMS를 통해 각 방향에 대해 뽑은 edges를 표시한 영상(하)은 프레임이 들어올 때마다 변화가 있다. 그래서 이 두 영상을 입력으로 하여 직선 찾기를 한 결과(상좌 빨간색 선들)와 이로부터 계산한 교차점들의 위치 및 순번(상좌 연두색 동그라미와 하늘색 숫자)도 불안정하다. (2) 또한 패턴과의 거리에 대해 카메라 렌즈의 초점이 맞지 않으면 결과가 좋지 않다.
/* Test: feature points identification in implementing a virtual studio
1) grid pattern design with cross ratios
2) lines detection by Hough transform with Non Maximum Suppression,
modifying cvHoughLines2() function in OpenCV library
#include <OpenCV/OpenCV.h> // framework on Mac
//#include <cv.h>
//#include <highgui.h>
//#include <cxmisc.h>
#include <iostream>
#include <vector>
using namespace std;
//#include "nms.h" // Non Maximum Suppression to extract vertical and horizontal edges separately
//#include "nmshough.h" // Hough transform with Non Maximum Suppression to detect lines
struct line_param // structure to contain parameters to define a line
{
// eqn of a line: a*x + b*y = rho, when a = cos(theta) & b = sin(theta)
float rho, theta;
float a, b;
float x0, y0; // x-intercept, y-intercepts
};
// rearrange vertical lines from left to right
void indexLinesY (vector<line_param>& line, CvSeq* lines, IplImage* image )
{
int numLines = lines->total; // total number of detected lines
line.resize(numLines); // define the size of "line" vector as "numLines"
char txt[100]; // text to represent the index number of an ordered line
// get "rho" and "theta" values of lines detected by Hough transform and NMS
for( int n = 0; n < numLines; n++ )
{
float* newline = (float*)cvGetSeqElem(lines,n);
line[n].rho = newline[0];
line[n].theta = newline[1];
}
// rearrange "line" array in geometrical order, that is, by values of x-intercept in the image frame coordinate
float temp_rho, temp_theta;
for( int n = 0; n < numLines-1; n++ )
{
for ( int k = n+1; k < numLines; k++ )
{
float x0_here = line[n].rho / cos(line[n].theta);
float x0_next = line[k].rho / cos(line[k].theta);
// rearrange horizontal lines from up to bottom
void indexLinesX (vector<line_param>& line, CvSeq* lines, IplImage* image )
{
int numLines = lines->total; // total number of detected lines
line.resize(numLines); // define the size of "line" vector as "numLines"
char txt[100]; // text to represent the index number of an ordered line
// get "rho" and "theta" values of lines detected by Hough transform and NMS
for( int n = 0; n < numLines; n++ )
{
float* newline = (float*)cvGetSeqElem(lines,n);
line[n].rho = newline[0];
line[n].theta = newline[1];
}
// rearrange "line" array in geometrical order, that is, by values of y-intercept in the image frame coordinate
float temp_rho, temp_theta;
for( int n = 0; n < numLines-1; n++ )
{
for ( int k = n+1; k < numLines; k++ )
{
float y0_here = line[n].rho / sin(line[n].theta);
float y0_next = line[k].rho / sin(line[k].theta);
// index intersection points of lines in X and Y
void indexIntersections (vector<CvPoint>& p, vector<line_param>& lineX, vector<line_param>& lineY, IplImage* image )
{ // find "p", intersections of lines, "linesX" & "linesY", and draw them in "image"
int numLinesX = lineX.size(), numLinesY = lineY.size(); // total number of detected lines
int numPoints = numLinesX * numLinesY; // total number of intersection of the lines
p.resize(numPoints); // define the size of "p" vector as "numPoints"
char txt[100]; // text to represent the index number of an intersection point
// calculate intersection points of vertical and horizontal lines
for( int i = 0; i < numLinesX; i++ )
{
for( int j = 0; j < numLinesY; j++ )
{
int indexP = numLinesY * i + j; // index number of intersection points in geometrical order
float Px = ( lineX[i].rho*lineY[j].b - lineY[j].rho*lineX[i].b ) / ( lineX[i].a*lineY[j].b - lineX[i].b*lineY[j].a ) ;
float Py = ( lineX[i].rho - lineX[i].a*Px ) / lineX[i].b ;
p[indexP].x = cvRound(Px);
p[indexP].y = cvRound(Py);
// display the points in an image
cvCircle( image, p[indexP], 3, CV_RGB(0,255,50) /* , <#int line_type#>, <#int shift#> */ );
sprintf(txt, "%d-%d", i, j); cvPutText(image, txt, p[indexP], &cvFont(0.7), CV_RGB(50,255,250));
}
}
return;
}
int main()
{
IplImage* iplInput = 0; // input image
IplImage* iplGray = 0; // grey image converted from input image
IplImage *iplTemp = 0; // converted image from input image with a change of bit depth
IplImage* iplDoGx = 0, *iplDoGxClone; // filtered image by DoG in x-direction
IplImage* iplDoGy = 0, *iplDoGyClone; // filtered image by DoG in y-direction
IplImage* iplEdgeX = 0, *iplEdgeY = 0; // edge-detected image by filtering in each direction, to be used as input in line-fitting
double minValx, maxValx; // minimum & maximum of pixel intensity values
double minValy, maxValy;
double minValt, maxValt;
int width, height; // window size of input frame
int kernel = 1; float edgethres; // parameters of NMS function
double rho = 0.8; // distance resolution in pixel-related units
double theta = 0.8; // angle resolution measured in radians
// "A line is returned by the function if the corresponding accumulator value is greater than threshold."
// int threshold = 24, rN = 5, tN = 5; // for grid pattern of 11x7 squares
int threshold = 20, rN = 5, tN = 5; // for grid pattern of lines with cross ratios
double h[] = { -1, -7, -15, 0, 15, 7, 1 }; // 1-D kernel of DoG filter
CvMat DoGx = cvMat( 1, 7, CV_64FC1, h ); // DoG filter in x-direction
CvMat* DoGy = cvCreateMat( 7, 1, CV_64FC1 ); // DoG filter in y-direction
cvTranspose( &DoGx, DoGy ); // transpose(&DoGx) -> DoGy
// output information of lines found by Hough transform with NMS
CvMemStorage* storageX = cvCreateMemStorage(0), *storageY = cvCreateMemStorage(0);
CvSeq* linesX = 0, *linesY = 0;
vector<CvPoint> p; // ordered intersection points on the "linesXorder" & "linesYorder"
// create windows
cvNamedWindow("input");
cvNamedWindow( "temp" );
char title_fx[200], title_fy[200];
sprintf(title_fx, "filtered image by DoGx");
sprintf(title_fy, "filtered image by DoGy");
cvNamedWindow(title_fx);
cvNamedWindow(title_fy);
char title_ex[200], title_ey[200];
sprintf(title_ex, "detected edges in x direction");
sprintf(title_ey, "detected edges in y direction");
cvNamedWindow(title_ex);
cvNamedWindow(title_ey);
// initialize capture from a camera
CvCapture* capture = cvCaptureFromCAM(0); // capture from video device #0
int frame = 0; // number of grabbed frames
while(1)
{
// get video frames from the camera
if ( !cvGrabFrame(capture) ) {
printf("Could not grab a frame\n\7");
exit(0);
}
else {
cvGrabFrame( capture ); // capture a frame
iplInput = cvRetrieveFrame(capture); // retrieve the caputred frame
cvSaveImage("original.bmp", iplInput);
if(iplInput) {
if(0 == frame) {
// create an image header and allocate the image data
iplGray = cvCreateImage(cvGetSize(iplInput), 8, 1);
iplTemp = cvCreateImage(cvGetSize(iplInput), IPL_DEPTH_32F, 1);
iplDoGx = cvCreateImage(cvGetSize(iplInput), IPL_DEPTH_32F, 1);
iplDoGy = cvCreateImage(cvGetSize(iplInput), IPL_DEPTH_32F, 1);
iplDoGyClone = cvCloneImage(iplDoGy), iplDoGxClone = cvCloneImage(iplDoGx);
iplEdgeX = cvCreateImage(cvGetSize(iplInput), 8, 1);
iplEdgeY = cvCreateImage(cvGetSize(iplInput), 8, 1);
width = iplInput->width, height = iplInput->height;
cvMoveWindow( "temp", 100+width+10, 100 );
cvMoveWindow( title_fx, 100, 100+height+30 );
cvMoveWindow( title_fy, 100+width+10, 100+height+30 );
cvMoveWindow( title_ey, 100, 100+(height+30)*2 );
cvMoveWindow( title_ex, 100+width+10, 100+(height+30)*2 );
}
// convert the input color image to gray one
cvCvtColor(iplInput, iplGray, CV_BGR2GRAY); // convert an image from one color space to another
cvSaveImage("gray.bmp", iplGray);
// convert one array to another with optional linear transformation
cvConvert(iplGray, iplTemp);
// increase the frame number
frame++;
}
// #1. DoG filtering
// convolve an image with the DoG kernel
// void cvFilter2D(const CvArr* src, CvArr* dst, const CvMat* kernel, CvPoint anchor=cvPoint(-1, -1)
cvFilter2D( iplTemp, iplDoGx, &DoGx ); // convolve an image with the DoG kernel in x-direction
cvFilter2D( iplTemp, iplDoGy, DoGy ); // convolve an image with the DoG kernel in y-direction
// convert negative values to positive to filter the image in reverse direction
cvAbs(iplDoGx, iplDoGx); cvAbs(iplDoGy, iplDoGy);
// normalize the pixel values
cvMinMaxLoc( iplDoGx, &minValx, &maxValx ); // find global minimum and maximum in image array
cvMinMaxLoc( iplDoGy, &minValy, &maxValy );
cvMinMaxLoc( iplTemp, &minValt, &maxValt );
cvScale( iplDoGx, iplDoGx, 1.0 / maxValx );
cvScale( iplDoGy, iplDoGy, 1.0 / maxValy );
cvScale( iplTemp, iplTemp, 1.0 / maxValt );
// display windows
cvShowImage( "temp", iplTemp );
cvShowImage( title_fx, iplDoGx ); cvShowImage( title_fy, iplDoGy );
// save images to files
cvSaveImage("temp.bmp", iplTemp);
cvSaveImage("DoGx.bmp", iplDoGx); cvSaveImage("DoGy.bmp", iplDoGy);
// #2. separate selected edges into vertical and horizontal
// arrange vertical lines from left to right
cout << "vertical" << endl;
indexLinesY(linesYorder, linesY, iplInput );
// arrange horizontal lines from up to bottom
cout << "horizontal" << endl;
indexLinesX(linesXorder, linesX, iplInput );
// calculate and index intersection points
indexIntersections(p, linesXorder, linesYorder, iplInput);
swPark_2000rti 440쪽: "The cross-ratio is not preserved for the (image) frame coordinate, positions of the feature points in an image, or for the distorted image coordinate. Cross-ratio is invariant only for the undistorted coordinate." (swPark_20
박승우_1999전자공학회지 96쪽: "이렇게 곡선으로 나타난 가로선과 세로선을 직선으로 피팅할 경우 cross-ratio는 왜곡 현상 때문에 이 선들에 대해서는 보존되지
않게 된다. 따라서 정확한 피팅을 위해서는 아래와 같이 렌즈의 왜곡변수(k1)를 고려한 이차곡선으로의 피팅이 필요하다.
Y = a*X + b/(1+k1*R^2) = a*X + b/(1+k1*(X^2+Y^2)) <--- 이 식은 영어 논문 (19)식과 한글 논문 (15)식을 조합, 수정한 식임. 확인 필요
이 식을 피팅해서 계수 a, b, k1를 구하고,
여기서 k1=0을 두면 왜곡이 보상된 점에 대한 직선식을 구할 수 있다. 이렇게 구해진 직선들을 패턴의 가로선들과 세로선들의 cross-ratio와 비교함으로써
영상에서 찾아진 선들을 인식할 수 있다. 또한 영상에서의
특징점은 이 식에 의해 피팅된 가로선들과 세로선들의 교점으로 정확하게 구할 수 있다."
그런데,
현재 시험용 패턴과 코드로부터 촬영, 검출된 이미지 상의 점들은 거의 직선에 가깝다. 우선 OpenCV 라이브러리의 cvHoughLines2() 함수에 의한 직선 찾기를 해 보자.
1) 교점 구하기 테스트
line fitting을 통해 찾은 직선들로부터 패턴 격자의 corner points를 구하는 것을 시험해 본다.
/* Test: line fitting in implementing a virtual studio
using cvHoughLines2() function in OpenCV library
ref.
1) swPark_2000rti.pdf
2) 박승우_1999대한전자공학회지 제36권 S편 제7호
3) http://opencv.willowgarage.com/documentation/feature_detection.html?highlight=cvhoughlines#cvHoughLines2
camera: Logitech QuickCam Pro 4000
2010, lym
*/
#include <OpenCV/OpenCV.h>
#include <iostream>
using namespace std;
// non-maximum suppression (NMS)
void nonMaximumSuppression ( IplImage* image, int kernel, int threshold )
{
for ( int y = 0; y < image->height; y++ )
{
// cout << "y = " << y << endl;
for ( int x = 0; x < image->width; x++ )
{
float intensity = CV_IMAGE_ELEM( image, float, y, x );
if ( intensity > threshold ) {
float neighbor;
int flag = 0;
for ( int ky = -kernel; ky <= kernel; ky++ ) // in y-direction
{
if ( y+ky < 0 || y+ky >= image->height ) { // border check
continue;
}
for ( int kx = -kernel; kx <= kernel; kx++ ) // in x-direction
{
if ( x+kx < 0 || x+kx >= image->width ) { // border check
continue;
}
neighbor = CV_IMAGE_ELEM( image, float, y+ky, x+kx );
if ( intensity < neighbor ) {
CV_IMAGE_ELEM( image, float, y, x ) = 0.0;
flag = 1;
break;
}
}
if ( 1 == flag ) {
break;
}
}
}
else {
CV_IMAGE_ELEM( image, float, y, x ) = 0.0;
}
}
}
}
// non-maximum suppression (NMS)
void nonMaximumSuppression2 ( IplImage* image, IplImage* image2, int kernel)
{
float neighbor, neighbor2;
for ( int y = 0; y < image->height; y++ )
{
// cout << "y = " << y << endl;
for ( int x = 0; x < image->width; x++ )
{
float intensity = CV_IMAGE_ELEM( image, float, y, x );
// if ( intensity > threshold ) {
if (intensity > 0) {
int flag = 0;
for ( int ky = -kernel; ky <= kernel; ky++ ) // in y-direction
{
if ( y+ky < 0 || y+ky >= image->height ) { // border check
continue;
}
for ( int kx = -kernel; kx <= kernel; kx++ ) // in x-direction
{
if ( x+kx < 0 || x+kx >= image->width ) { // border check
continue;
}
neighbor = CV_IMAGE_ELEM( image, float, y+ky, x+kx );
neighbor2 = CV_IMAGE_ELEM( image2, float, y+ky, x+kx );
// if ( intensity < neighbor ) {
if ( intensity < neighbor || intensity < neighbor2) {
CV_IMAGE_ELEM( image, float, y, x ) = 0.0;
flag = 1;
break;
}
}
if ( 1 == flag ) {
break;
}
}
}
else {
CV_IMAGE_ELEM( image, float, y, x ) = 0.0;
}
}
}
}
// in the gradient direction
void selectEdges( IplImage* image1, IplImage* image2 )
{
for ( int y = 0; y < image1->height; y++ )
{
// cout << "y = " << y << endl;
for ( int x = 0; x < image1->width; x++ )
{
if( x == image1->width - 1 ) {
x = x;
}
float intensity1 = CV_IMAGE_ELEM( image1, float, y, x );
if ( intensity1 > 0.0 ) { // if the pixel is a edge point surviving NMS
float intensity2 = CV_IMAGE_ELEM( image2, float, y, x );
// compare it with the gradient value in the other direction
if ( intensity1 < intensity2 ) {
CV_IMAGE_ELEM( image1, float, y, x ) = 0.0;
}
}
}
}
}
// display the points in an image
cvCircle( image, c, 3, CV_RGB(0,255,100), 2 /* , <#int line_type#>, <#int shift#> */ );
}
}
}
int width = 320;
int height = 240;
int main()
{
IplImage* iplInput = 0; // input image
IplImage* iplGray = 0; // grey image converted from input image
IplImage *iplTemp = 0; // converted image from input image with a change of bit depth
IplImage* iplDoGx = 0, *iplDoGxClone; // filtered image by DoG in x-direction
IplImage* iplDoGy = 0, *iplDoGyClone; // filtered image by DoG in y-direction
IplImage* iplEdgeX = 0, *iplEdgeY = 0; // edge-detected image in each direction, to be used as input in line-fitting
// output information of lines found by Hough transform
CvMemStorage* storageX = cvCreateMemStorage(0), *storageY = cvCreateMemStorage(0);
CvSeq* linesX = 0, *linesY = 0;
// initialize capture from a camera
CvCapture* capture = cvCaptureFromCAM(0); // capture from video device #0
int count = 0; // number of grabbed frames
while(1) {
// get video frames from the camera
if ( !cvGrabFrame(capture) ) {
printf("Could not grab a frame\n\7");
exit(0);
}
else {
cvGrabFrame( capture ); // capture a frame
iplInput = cvRetrieveFrame(capture); // retrieve the caputred frame
// iplInput = cvLoadImage("P:/input.bmp"); // retrieve the caputred frame
if(iplInput) {
if(0 == count) {
// create an image header and allocate the image data
iplGray = cvCreateImage(cvGetSize(iplInput), 8, 1);
iplTemp = cvCreateImage(cvGetSize(iplInput), IPL_DEPTH_32F, 1);
iplDoGx = cvCreateImage(cvGetSize(iplInput), IPL_DEPTH_32F, 1);
iplDoGy = cvCreateImage(cvGetSize(iplInput), IPL_DEPTH_32F, 1);
iplDoGyClone = cvCloneImage(iplDoGy), iplDoGxClone = cvCloneImage(iplDoGx);
iplEdgeX = cvCreateImage(cvGetSize(iplInput), 8, 1);
iplEdgeY = cvCreateImage(cvGetSize(iplInput), 8, 1);
}
// convert the input color image to gray one
cvCvtColor(iplInput, iplGray, CV_BGR2GRAY); // convert an image from one color space to another
// convert one array to another with optional linear transformation
cvConvert(iplGray, iplTemp);
// increase the frame number
count++;
}
// cvShowImage( "input", iplInput );
// convolve an image with the kernel
// void cvFilter2D(const CvArr* src, CvArr* dst, const CvMat* kernel, CvPoint anchor=cvPoint(-1, -1)
cvFilter2D( iplTemp, iplDoGx, &DoGx ); // convolve an image with the DoG kernel in x-direction
cvFilter2D( iplTemp, iplDoGy, DoGy ); // convolve an image with the DoG kernel in y-direction
// ref. http://opencv.willowgarage.com/documentation/operations_on_arrays.html?highlight=cvabs#cvAbsDiffS
cvAbs(iplDoGx, iplDoGx); cvAbs(iplDoGy, iplDoGy);
// save images to files
cvSaveImage("edgeX.bmp", iplDoGx);
cvSaveImage("edgeY.bmp", iplDoGy);
// line fitting
cvConvert(iplDoGx, iplEdgeY);
cvConvert(iplDoGy, iplEdgeX);
// ref. http://opencv.willowgarage.com/documentation/feature_detection.html?highlight=cvhoughlines#cvHoughLines2
// CvSeq* cvHoughLines2(CvArr* image, void* storage, int method, double rho, double theta, int threshold, double param1=0, double param2=0)
double rho = 1.0; // distance resolution in pixel-related units
double theta = 1.0; // angle resolution measured in radians
int threshold = 20; // ("A line is returned by the function if the corresponding accumulator value is greater than threshold.")
linesX = cvHoughLines2(iplEdgeX, storageX, CV_HOUGH_STANDARD, 1.0*rho, CV_PI/180*theta, threshold, 0, 0);
linesY = cvHoughLines2(iplEdgeY, storageY, CV_HOUGH_STANDARD, 1.0*rho, CV_PI/180*theta, threshold, 0, 0);
2)
그러나, line fitting의 결과가 깔끔하지 않은 문제를 우선 해결해야 한다. (rho, theta, threshold 등의 함수 매개변수 값을 조정하는 것을 포함하여 사용 중인 웹캠에 적합한 데이터 처리가 필요하다.)
현재의 코드로부터 나오는 결과를 정리해 두면 아래와 같다.
NMS와 동시에 수평선 또는 수직선 위의 점들을 따로 추출한 결과 이미지 ("iplEdgeX"와 "iplEdgeY")를 cvHoughLines2() 함수의 입력으로 하고,
double rho = 1.0; // distance resolution in pixel-related units
double theta = 1.0; // angle resolution measured in radians
int threshold = 20; // (A line is returned by the function if the corresponding accumulator value is greater than threshold)
위와 같이 매개변수 값을 주면 검출된 직선들과 그로부터 계산한 교점들은 다음과 같이 나타난다.
수직선 상의 edges만 검출한 영상
수평선 상의 edges만 검출한 영상
Hough transform에 의한 line fitting 한 결과
(Non Maximal suppression (NMS)을 하기 전에) 1차 DoG 필터를 이미지 프레임의 x 방향, y 방향으로 적용한 결과 이미지 ("iplDoGx"와 "iplDoGy")를 cvHoughLines2() 함수의 입력으로 하고,
double rho = 1.0; // distance resolution in pixel-related units
double theta = 1.0; // angle resolution measured in radians
int threshold = 20; // (A line is returned by the function if the corresponding accumulator value is greater than threshold)
위와 같이 매개변수 값들을 주면 검출된 직선들과 그로부터 계산한 교점들은 다음과 같이 나타난다.
x방향으로 DoG 필터를 적용한 이미지
y방향으로 DoG 필터를 적용한 이미지
Hough transform에 의한 line fitting한 결과
그러니까... 실제로 한 직선 상의 점들로부터 여러 개의 직선을 찾게 되는 것은 edge points로 detection된 (흰색으로 보이는) 픽셀 부분의 세기값이 약하거나 일정하지 않기 때문인 것 같다. 입력 이미지를 binary로 바꾸고 cvHoughLines2()의 입력으로 accumulator value에 기준값을 주는 파라미터 threshold를 증가시키면 될 것 같다.
Try #1. 입력 이미지 이진화
NMS와 동시에 수평선 또는 수직선 위의 점들을 따로 추출한 결과 이미지 ("iplEdgeX"와 "iplEdgeY")를 이진화하고,
double rho = 1.0; // distance resolution in pixel-related units
double theta = 1.0; // angle resolution measured in radians
int threshold = 40; // ("A line is returned by the function if the corresponding accumulator value is greater than threshold.")
위와 같이 매개변수 값들을 주면 검출된 직선들과 그로부터 계산한 교점들은 다음과 같이 나타난다.
수직선 상의 edges만 검출하여 이진화한 영상
수평선 상의 edges만 검출하여 이진화한 영상
Hough transform에 의한 line fitting한 결과
실제로 한 직선에 여러 개의 직선이 검출되는 빈도는 현저히 줄지만 대신 실제 직선 중에 검출되지 않는 것이 생긴다.
Try #2. line fitting의 입력 이미지 처리 & 매개변수 조정
Try #3. 실제로 하나인데 여러 개로 겹쳐서 나오는 직선들의 평균을 취해 하나로 합침
다음과 같은 입력 영상에 대해 탐지된 직선들의 방정식을 정의하는 매개변수 (rho와 theta) 값을 출력해 보면 아래와 같이 나온다.
1) 1차 DoG filter 만들기: x방향과 y방향의 local maxima를 찾는다.
swPark_2000rti.pdf 440쪽:
"To find the edge of the grid, a first-order Derivative of Gaussian
(DoG) filter with a kernel h = [-1, -7, -15, 0, 15, 7, 1] is used."
/* Test: feature point extraction in implementing virtual studio
: using Gaussin gradient filter, first-order Derivative of Gaussian (DoG) filter
with a kernel h = [-1, -7, -15, 0, 15, 7, 1]
ref. swPark_2000rti.pdf:440p
2010, lym
camera: Logitech QuickCam Pro 4000
*/
// + non maximum suppression
#include <OpenCV/OpenCV.h>
#include <iostream>s
using namespace std;
// non-maximum suppression (NMS)
void NonMaximumSuppression ( IplImage* image, int kernel, int threshold )
{
for ( int y = 0; y < image->height; y++ )
{
cout << "y = " << y << endl;
for ( int x = 0; x < image->width; x++ )
{
if( x == image->width - 1 ) {
x = x;
}
float intensity = CV_IMAGE_ELEM( image, float, y, x );
if ( intensity > threshold ) {
float neighbor;
int flag = 0;
for ( int ky = -kernel; ky <= kernel; ky++ ) // in y-direction
{
if ( y+ky < 0 || y+ky >= image->height ) { // border check
continue;
}
for ( int kx = -kernel; kx <= kernel; kx++ ) // in x-direction
{
if ( x+kx < 0 || x+kx >= image->width ) { // border check
continue;
}
neighbor = CV_IMAGE_ELEM( image, float, y+ky, x+kx );
if ( intensity < neighbor ) {
CV_IMAGE_ELEM( image, float, y, x ) = 0.0;
flag = 1;
break;
}
}
if ( 1 == flag ) {
break;
}
}
}
else {
CV_IMAGE_ELEM( image, float, y, x ) = 0.0;
}
}
}
}
int main()
{
IplImage* iplInput = 0; // input image
IplImage* iplGray = 0; // grey image converted from input image
IplImage *iplTemp = 0; // converted image from input image with a change of bit depth
// IplImage* iplDoG = 0; // filtered image by DoG
IplImage* iplDoGx = 0; // filtered image by DoG in x-direction
IplImage* iplDoGy = 0; // filtered image by DoG in y-direction
// initialize capture from a camera
CvCapture* capture = cvCaptureFromCAM(0); // capture from video device #0
int count = 0; // number of grabbed frames
while(1) {
// get video frames from the camera
if ( !cvGrabFrame(capture) ) {
printf("Could not grab a frame\n\7");
exit(0);
}
else {
cvGrabFrame( capture ); // capture a frame
iplInput = cvRetrieveFrame(capture); // retrieve the caputred frame
if(iplInput) {
if(0 == count) {
// create an image header and allocate the image data
/* iplGray = cvCreateImage(cvGetSize(iplInput), 8, 1);
iplDoGx = cvCreateImage(cvGetSize(iplInput), 8, 1);
iplDoGy = cvCreateImage(cvGetSize(iplInput), 8, 1);
*/
iplGray = cvCreateImage(cvGetSize(iplInput), 8, 1);
iplTemp = cvCreateImage(cvGetSize(iplInput), IPL_DEPTH_32F, 1);
iplDoGx = cvCreateImage(cvGetSize(iplInput), IPL_DEPTH_32F, 1);
iplDoGy = cvCreateImage(cvGetSize(iplInput), IPL_DEPTH_32F, 1);
}
// convert the input color image to gray one
cvCvtColor(iplInput, iplGray, CV_BGR2GRAY); // convert an image from one color space to another
// convert one array to another with optional linear transformation
cvConvert(iplGray, iplTemp);
// increase the frame number
count++;
}
cvShowImage( "input", iplInput );
// convolve an image with the kernel
// void cvFilter2D(const CvArr* src, CvArr* dst, const CvMat* kernel, CvPoint anchor=cvPoint(-1, -1)
cvFilter2D( iplTemp, iplDoGx, &DoGx ); // convolve an image with the DoG kernel in x-direction
cvFilter2D( iplTemp, iplDoGy, DoGy ); // convolve an image with the DoG kernel in y-direction
// ref. http://opencv.willowgarage.com/documentation/operations_on_arrays.html?highlight=cvabs#cvAbsDiffS
cvAbs(iplDoGx, iplDoGx);
cvAbs(iplDoGy, iplDoGy);
Try #1.
-1) 필터링의 결과 이미지의 bit depth를 "8"이 아니라 "IPL_DEPTH_32F"로 바꾼 다음, 음수로 나온 gradient 값을 양수로 바꾸어 준다.
그런데, 입력 영상을 담을 메모리를 별도로 생성하지 않고, 다음과 같이 비디오 프레임 캡처 시 만들어 주므로 인위적으로 설정해 줄 수 없다.
shift – Value added to the scaled source array elements
-2) Non Maximum Suppression (NMS)
이웃 화소들의 세기값을 비교하여 해당 픽셀이 최대값이 아니면 "0"으로 하여 지워 준다
/* Test: feature point extraction in implementing virtual studio
: using Gaussin gradient filter, first-order Derivative of Gaussian (DoG) filter
with a kernel h = [-1, -7, -15, 0, 15, 7, 1]
ref. swPark_2000rti.pdf:440p
2010, lym
camera: Logitech QuickCam Pro 4000
*/
#include <OpenCV/OpenCV.h>
#include <iostream>s
using namespace std;
// non-maximum suppression (NMS)
void NonMaximumSuppression ( IplImage* image, int kernel, int threshold )
{
for ( int y = 0; y < image->height; y++ )
{
cout << "y = " << y << endl;
for ( int x = 0; x < image->width; x++ )
{
if( x == image->width - 1 ) {
x = x;
}
float intensity = CV_IMAGE_ELEM( image, float, y, x );
if ( intensity > threshold ) {
float neighbor;
int flag = 0;
for ( int ky = -kernel; ky <= kernel; ky++ ) // in y-direction
{
if ( y+ky < 0 || y+ky >= image->height ) { // border check
continue;
}
for ( int kx = -kernel; kx <= kernel; kx++ ) // in x-direction
{
if ( x+kx < 0 || x+kx >= image->width ) { // border check
continue;
}
neighbor = CV_IMAGE_ELEM( image, float, y+ky, x+kx );
if ( intensity < neighbor ) {
CV_IMAGE_ELEM( image, float, y, x ) = 0.0;
flag = 1;
break;
}
}
if ( 1 == flag ) {
break;
}
}
}
else {
CV_IMAGE_ELEM( image, float, y, x ) = 0.0;
}
}
}
}
int main()
{
IplImage* iplInput = 0; // input image
IplImage* iplGray = 0; // grey image converted from input image
IplImage *iplTemp = 0; // converted image from input image with a change of bit depth
// IplImage* iplDoG = 0; // filtered image by DoG
IplImage* iplDoGx = 0; // filtered image by DoG in x-direction
IplImage* iplDoGy = 0; // filtered image by DoG in y-direction
// initialize capture from a camera
CvCapture* capture = cvCaptureFromCAM(0); // capture from video device #0
int count = 0; // number of grabbed frames
while(1) {
// get video frames from the camera
if ( !cvGrabFrame(capture) ) {
printf("Could not grab a frame\n\7");
exit(0);
}
else {
cvGrabFrame( capture ); // capture a frame
iplInput = cvRetrieveFrame(capture); // retrieve the caputred frame
if(iplInput) {
if(0 == count) {
// create an image header and allocate the image data
/* iplGray = cvCreateImage(cvGetSize(iplInput), 8, 1);
iplDoGx = cvCreateImage(cvGetSize(iplInput), 8, 1);
iplDoGy = cvCreateImage(cvGetSize(iplInput), 8, 1);
*/
iplGray = cvCreateImage(cvGetSize(iplInput), 8, 1);
iplTemp = cvCreateImage(cvGetSize(iplInput), IPL_DEPTH_32F, 1);
iplDoGx = cvCreateImage(cvGetSize(iplInput), IPL_DEPTH_32F, 1);
iplDoGy = cvCreateImage(cvGetSize(iplInput), IPL_DEPTH_32F, 1);
}
// convert the input color image to gray one
cvCvtColor(iplInput, iplGray, CV_BGR2GRAY); // convert an image from one color space to another
// convert one array to another with optional linear transformation
cvConvert(iplGray, iplTemp);
// increase the frame number
count++;
}
cvShowImage( "input", iplInput );
// convolve an image with the kernel
// void cvFilter2D(const CvArr* src, CvArr* dst, const CvMat* kernel, CvPoint anchor=cvPoint(-1, -1)
cvFilter2D( iplTemp, iplDoGx, &DoGx ); // convolve an image with the DoG kernel in x-direction
cvFilter2D( iplTemp, iplDoGy, DoGy ); // convolve an image with the DoG kernel in y-direction
// ref. http://opencv.willowgarage.com/documentation/operations_on_arrays.html?highlight=cvabs#cvAbsDiffS
cvAbs(iplDoGx, iplDoGx);
cvAbs(iplDoGy, iplDoGy);
/* Test: feature point extraction in implementing virtual studio
: using Gaussin gradient filter, first-order Derivative of Gaussian (DoG) filter
with a kernel h = [-1, -7, -15, 0, 15, 7, 1]
ref. swPark_2000rti.pdf:440p
camera: Logitech QuickCam Pro 4000
2010, lym & kyu
*/
// + non maximum suppression
#include <OpenCV/OpenCV.h>
#include <iostream>
using namespace std;
// non-maximum suppression (NMS)
void nonMaximumSuppression ( IplImage* image, int kernel, int threshold )
{
for ( int y = 0; y < image->height; y++ )
{
// cout << "y = " << y << endl;
for ( int x = 0; x < image->width; x++ )
{
float intensity = CV_IMAGE_ELEM( image, float, y, x );
if ( intensity > threshold ) {
float neighbor;
int flag = 0;
for ( int ky = -kernel; ky <= kernel; ky++ ) // in y-direction
{
if ( y+ky < 0 || y+ky >= image->height ) { // border check
continue;
}
for ( int kx = -kernel; kx <= kernel; kx++ ) // in x-direction
{
if ( x+kx < 0 || x+kx >= image->width ) { // border check
continue;
}
neighbor = CV_IMAGE_ELEM( image, float, y+ky, x+kx );
if ( intensity < neighbor ) {
CV_IMAGE_ELEM( image, float, y, x ) = 0.0;
flag = 1;
break;
}
}
if ( 1 == flag ) {
break;
}
}
}
else {
CV_IMAGE_ELEM( image, float, y, x ) = 0.0;
}
}
}
}
// non-maximum suppression (NMS)
void nonMaximumSuppression2 ( IplImage* image, IplImage* image2, int kernel)
{
float neighbor, neighbor2;
for ( int y = 0; y < image->height; y++ )
{
// cout << "y = " << y << endl;
for ( int x = 0; x < image->width; x++ )
{
float intensity = CV_IMAGE_ELEM( image, float, y, x );
// if ( intensity > threshold ) {
if (intensity > 0) {
int flag = 0;
for ( int ky = -kernel; ky <= kernel; ky++ ) // in y-direction
{
if ( y+ky < 0 || y+ky >= image->height ) { // border check
continue;
}
for ( int kx = -kernel; kx <= kernel; kx++ ) // in x-direction
{
if ( x+kx < 0 || x+kx >= image->width ) { // border check
continue;
}
neighbor = CV_IMAGE_ELEM( image, float, y+ky, x+kx );
neighbor2 = CV_IMAGE_ELEM( image2, float, y+ky, x+kx );
// if ( intensity < neighbor ) {
if ( intensity < neighbor || intensity < neighbor2) {
CV_IMAGE_ELEM( image, float, y, x ) = 0.0;
flag = 1;
break;
}
}
if ( 1 == flag ) {
break;
}
}
}
else {
CV_IMAGE_ELEM( image, float, y, x ) = 0.0;
}
}
}
}
// in the gradient direction
void selectEdges( IplImage* image1, IplImage* image2 )
{
for ( int y = 0; y < image1->height; y++ )
{
// cout << "y = " << y << endl;
for ( int x = 0; x < image1->width; x++ )
{
if( x == image1->width - 1 ) {
x = x;
}
float intensity1 = CV_IMAGE_ELEM( image1, float, y, x );
if ( intensity1 > 0.0 ) { // if the pixel is a edge point surviving NMS
float intensity2 = CV_IMAGE_ELEM( image2, float, y, x );
// compare it with the gradient value in the other direction
if ( intensity1 < intensity2 ) {
CV_IMAGE_ELEM( image1, float, y, x ) = 0.0;
}
}
}
}
}
int main()
{
IplImage* iplInput = 0; // input image
IplImage* iplGray = 0; // grey image converted from input image
IplImage *iplTemp = 0; // converted image from input image with a change of bit depth
// IplImage* iplDoG = 0; // filtered image by DoG
IplImage* iplDoGx = 0, *iplDoGxClone; // filtered image by DoG in x-direction
IplImage* iplDoGy = 0, *iplDoGyClone; // filtered image by DoG in y-direction
// initialize capture from a camera
CvCapture* capture = cvCaptureFromCAM(0); // capture from video device #0
int count = 0; // number of grabbed frames
while(1) {
// get video frames from the camera
// if (0) {
if ( !cvGrabFrame(capture) ) {
printf("Could not grab a frame\n\7");
exit(0);
}
else {
cvGrabFrame( capture ); // capture a frame
iplInput = cvRetrieveFrame(capture); // retrieve the caputred frame
// iplInput = cvLoadImage("P:/input.bmp"); // retrieve the caputred frame
}
// convert the input color image to gray one
cvCvtColor(iplInput, iplGray, CV_BGR2GRAY); // convert an image from one color space to another
// convert one array to another with optional linear transformation
cvConvert(iplGray, iplTemp);
// increase the frame number
count++;
}
cvShowImage( "input", iplInput );
// convolve an image with the kernel
// void cvFilter2D(const CvArr* src, CvArr* dst, const CvMat* kernel, CvPoint anchor=cvPoint(-1, -1)
cvFilter2D( iplTemp, iplDoGx, &DoGx ); // convolve an image with the DoG kernel in x-direction
cvFilter2D( iplTemp, iplDoGy, DoGy ); // convolve an image with the DoG kernel in y-direction
// ref. http://opencv.willowgarage.com/documentation/operations_on_arrays.html?highlight=cvabs#cvAbsDiffS
cvAbs(iplDoGx, iplDoGx); cvAbs(iplDoGy, iplDoGy);
// cout << "cross ratio = " << cross_ratio << endl;
printf("cross ratio = %f\n", cross_ratio);
}
return 0;
}
cross ratio = 1.088889
cross ratio = 2.153846
cross ratio = 1.185185
cross ratio = 1.094737
cross ratio = 2.166667
cross ratio = 1.160714
cross ratio = 1.274510
cross ratio = 1.562500
cross ratio = 1.315789
cross ratio = 1.266667
cross ratio = 1.266667
cross ratio = 1.446429
cross ratio = 1.145455
cross ratio = 1.441176
cross ratio = 1.484848
cross ratio = 1.421875
cross ratio = 1.123457
cross ratio = 1.600000
cross ratio = 1.142857
cross ratio = 1.960784
cross ratio = 1.142857
cross ratio = 1.350000
cross ratio = 1.384615
cross ratio = 1.529412
cross ratio = 1.104575
cross ratio = 1.421875
cross ratio = 1.711111
cross ratio = 1.178571
cross ratio = 1.200000
cross ratio = 1.098039
cross ratio = 2.800000
cross ratio = 1.230769
cross ratio = 1.142857
다른 식 적용
cross ratio = 0.040000
cross ratio = 0.666667
cross ratio = 0.107143
cross ratio = 0.064935
cross ratio = 0.613636
cross ratio = 0.113636
cross ratio = 0.204545
cross ratio = 0.390625
cross ratio = 0.230769
cross ratio = 0.203620
cross ratio = 0.205882
cross ratio = 0.316406
cross ratio = 0.109375
cross ratio = 0.300000
cross ratio = 0.360000
cross ratio = 0.290909
cross ratio = 0.090909
cross ratio = 0.400000
cross ratio = 0.100000
cross ratio = 0.562500
cross ratio = 0.100000
cross ratio = 0.257143
cross ratio = 0.285714
cross ratio = 0.363636
cross ratio = 0.074380
cross ratio = 0.290909
cross ratio = 0.466667
cross ratio = 0.125000
cross ratio = 0.156250
/* Test: pattern design in implementing a virtual studio
to design grid lines using cross-ratio as represented in "swPark_2000rti": (16)
C(x1,x2,x3,x4) = (x2-x1)(x4-x3) / (x4-x2)(x3-x1)
based on two plots in figure 7, 439p
2010, lym
*/
cout << "# of cross-ratios in vertical lines = " << sizeof(crossratio_vertical) / sizeof(double) << endl;
cout << "# of cross-ratios in horizontal lines = " << sizeof(crossratio_horizontal) / sizeof(double) << endl;
// initialize grid lines of the pattern
double x[40] = { 0.0 }; // 40 vertical lines
double y[20] = { 0.0 }; // 20 horizontal lines
double r = 0.0; // cross ratio of 4 consecutive lines
double a,b,c,d; // temporary variables to be used in calculating a cross ratio
// set the positions of the first three vertical lines
x[0] = 1.0;
x[1] = x[0] + 1.0;
x[2] = x[1] + 2.0;
// set the positions of the first three horizontal lines
y[0] = 1.0;
y[1] = y[0] + 2.0;
y[2] = y[1] + 1.0;
// for vertical lines
int number = 40;
cout << endl << "x[0]=" << x[0] << " x[1]=" << x[1] << " x[2]=" << x[2] << endl;
for( int n = 0; n < number-3; n++ )
{
// cout << "n = " << n << endl;
r = crossratio_vertical[n];
/* a = x[n];
b = x[n+1];
c = x[n+2];
// cout << "a=" << a << " b=" << b << " c=" << c << " ratio =" << r;
Publication IEICE TRANSACTIONS on Fundamentals of Electronics, Communications and Computer SciencesVol.E83-ANo.10pp.1921-1928 Publication Date: 2000/10/20 Online ISSN: Print ISSN: 0916-8508 Type of Manuscript: Special Section PAPER (Special Section on Information Theory and Its Applications) Category: Image Processing Keyword: cross ratio,
Markov process,
error analysis,
reliability evaluation,
virtual studio,
Full Text:
MVA2000 IAPR Workshop on Machine Vision Applications, Nov. 28-30,2000,
The University of Tokyo, Japan
13-28
Optimal Grid Pattern for
Automated Matching Using Cross Ratio
Chikara Matsunaga (Broadcast
Division, FOR-A Co. Ltd.)
Kenichi Kanatanit (Department of Computer
Science, Gunma University)
Summary: With
a view to virtual studio applications, we design an optimal grid
pattern such that the observed image of a small portion of it can be
matched to its corresponding position in the pattern easily. The grid
shape is so determined that the cross ratio of adjacent
intervals is different everywhere. The cross ratios are generated by an
optimal Markov process that maximizes the accuracy of matching. We test
our camera calibration system using the resulting grid pattern in a
realistic setting and show that the performance is greatly improved by
applying techniques derived from the designed properties of the pattern.
Camera calibration is a first step in all vision and media applications.
> pre-calibration (Tsai) vs. self-calibration (Pollefeys)
=> "simultaneous calibration" by placing an easily distinguishable planar pattern in the scene
Introducing a statistic model of image noise, we generate the grid intervals by an optimal Markov process that maximizes the accuracy of matching.
: The pattern is theoretically designed by statistical analysis
If the cross rations are given, the sequence is determined as follows.
To find a sequence of cross ratios such that the sequence of numbers is a homogeneous increasing with the average interval being 1 and the minimum width as specified.
=> To generate the sequence of cross ratios stochastically, according to a probability distribution defined in such a way that the resulting sequence of numbers has the desired properties
=> able to optimize the probability distribution so that the matching performance is maximized by analyzing the statistical properties of image noise
출처: C. Matsunaga, Y. Kanazawa, and K. Kanatani, Optimal grid pattern for automated camera calibration using cross ratio , IEICE Transactions on Fundamentals of Electronics, Communications and Computer Sciences, Vol. E83-A, No. 10, pp. 1921--1928, 2000. 중 1926쪽 Fig.8 4배 확대 캡처
1) 무늬의 deep/light 색의 경계점들 찾기 edge detection
2) 찾은 점들을 직선으로 연결
3) 검출된 가로선과 세로선의 cross ratio와 실제 무늬의 cross ratio를 비교하여, 몇 번째 선인지 인식
detailed preview
1. initial identification process 초기 인식 과정 (특징점 인식)
1) chroma keying: RGB -> YUV 변환
2) gradient filtering: first-order derivative Gaussian filter (length = 7)
-1) 세로축에 대해 영상 축소 (1/4)하여 필터링
-2) Gx, Gy 절대값 비교하여 vertical / horizontal direction 판별
-3) 가로축에 대해
3) line fitting: lens distortion coefficient을 고려하여 이차곡선으로 피팅
4) identification
-1) 영상에서 찾아진 선들이 실제 무늬에서 몇 번째 선인지 인식
-2) feature points는 직선 식에 의해 피팅된 선들의 교점으로 정확하게 구할 수 있음
2. feature point tracking 실제 동작 과정 (특징점 위치 추적)
: feature points corresponding 검출된 특징점을 무늬의 교점과 매칭
1) intersection filter H (교점 필터)로 local maximum & minimum를 가지는 교점 검출
2) 검출된 교점의 부호를 판별하여 두 부류로 나눔
3) 이전 프레임에서의 교점의 위치를 기준으로 현재 프레임에서 검출된 교점에 대해 가장 가까운 이전 점을 찾음
* 다음 프레임에서 새로 나타난 특징점에 대해서도 이전 프레임에서의 카메라 변수를 이용해 실제 패턴 상의 교점을 영상으로 투영시켜 기준점으로 삼을 수 있음
2> real-time camera parameter extraction 실시간 카메라 변수 추출: Tsai's algorithm
1. determining image center 영상 중심 구하기: zooming
: using the center of expansion as a constant image-center
1) (lens distortion을 구하기 위한 초기화 과정에서) 정지된 카메라의 maximum zoom-out과 maximum zoom-in 상태에서 찾아서 인식한 특징점들을 저장
2) 두 개의 프레임에서 같은 점으로 나타난 특징점들을 연결한 line segments의 common intersection 교점을 계산
* 실제로 zooming은 여러 개의 lens들의 조합으로 작동하기 때문에 카메라의 zoom에 따라서 image center가 변하게 되지만, 이에 대한 표준 편차가 작으므로 무시하기로 함
2. lens distortion coefficient 계산
zooming이 없다면 고정된 값이 되므로 이하와 같이 매번 계산해 줄 필요가 없어짐
(1) f-k1 look-up table을 참조하는 방법
: zooming하는 과정에서 초점 거리 f와 렌즈 왜곡 변수 k1이 계속 변하게 되므로, 이에 대한 참조표를 미리 만들어 두고 나서 실제 동작 과정에서 참조
* 특징점들이 모두 하나의 평면에 존재하는 경우에는 초점거리 f와 카메라의 z 방향으로의 이동 Tz가 서로 coupled되기 때문에 카메라 변수가 제대로 계산되기 어렵다는 점을 고려하여 평면 상의 특징점들에 대해서 Tz/f를 인덱스로 사용하는 편법을 쓴다면, 카메라가 z 방향으로는 이동하지 않고 고정되어 있어야 한다는 (T1z = 0)조건이 붙게 됨
(2) collinearity를 이용하는 방법
: searching for k1 which maximally preserves collinearity 인식된 교점들에 대해 원래 하나의 직선에 속하는 점들이 왜곡 보상 되었을 때 가장 직선이 되게 하는 왜곡변수를 구함
1) 영상에서 같은 가로선에 속하는 교점들 (Xf, Yf) 가운데 세 개를 고름
2) 식7로부터 왜곡된 영상면 좌표 (Xd, Yd)를 구함
3) 식5로부터 왜곡 보상된 영상면 좌표 (Xu, Yu)를 구함
4) 식21과 같은 에러 함수 E(k1)를 정의
5) 영상에 나타난 N개의 가로선들에 대해서 E(k1) 값을 최소화하는 k1을 구함 (식 23) -> 비선형 최적화이나 iteration은 한 번
3. Tsai's algorithm
렌즈 왜곡 변수를 알면 카메라 캘리브레이션은 선형적 방법으로 구할 수 있게 됨
3> filtering
잡음으로 인해 검출된 교점에 오차가 생기므로 카메라변수가 틀려지게 됨
(->카메라가 정지해 있어도 카메라변수에 변화가 생겨 결과적으로 그래픽으로 생성된 가상의 무대에 떨림이 나타나게 됨)
Seong-Woo Park, Yongduek Seo, Ki-Sang Hong: Real-Time Camera Calibration for Virtual Studio. Real-Time Imaging 6(6): 433-448 (2000) doi:10.1006/rtim.1999.0199
Dept. of E.E. POSTECH, San 31, Hyojadong, Namku, Pohang, Kyungbuk, 790-784, Korea
Abstract
In this paper, we present an overall algorithm for real-time camera parameter extraction, which is one of the key elements in implementing virtual studio, and we also present a new method for calculating the lens distortion parameter in real time. In a virtual studio, the motion of a virtual camera generating a graphic studio must follow the motion of the real camera in order to generate a realistic video product. This requires the calculation of camera parameters in real-time by analyzing the positions of feature points in the input video. Towards this goal, we first design a special calibration pattern utilizing the concept of cross-ratio, which makes it easy to extract and identify feature points, so that we can calculate the camera parameters from the visible portion of the pattern in real-time. It is important to consider the lens distortion when zoom lenses are used because it causes nonnegligible errors in the computation of the camera parameters. However, the Tsai algorithm, adopted for camera calibration, calculates the lens distortion through nonlinear optimization in triple parameter space, which is inappropriate for our real-time system. Thus, we propose a new linear method by calculating the lens distortion parameter independently, which can be computed fast enough for our real-time application. We implement the whole algorithm using a Pentium PC and Matrox Genesis boards with five processing nodes in order to obtain the processing rate of 30 frames per second, which is the minimum requirement for TV broadcasting. Experimental results show this system can be used practically for realizing a virtual studio.
전자공학회논문지 제36권 S편 제7호, 1999. 7
가상스튜디오 구현을 위한 실시간 카메라 추적 ( Real-Time Camera Tracking for Virtual Studio )
박성우 · 서용덕 · 홍기상 저 pp. 90~103 (14 pages) http://uci.or.kr/G300-j12265837.v36n07p90
서지링크 한국과학기술정보연구원
가상스튜디오의 구현을 위해서 카메라의 움직임을 실시간으로 알아내는 것이 필수적이다. 기존의 가상스튜디어 구현에 사용되는 기계적인 방법을 이용한 카메라의 움직임 추적하는 방법에서 나타나는 단점들을 해결하기 위해 본 논문에서는 카메라로부터 얻어진 영상을 이용해 컴퓨터비전 기술을 응용하여 실시간으로 카메라변수들을 알아내기 위한 전체적인 알고리듬을 제안하고 실제 구현을 위한 시스템의 구성 방법에 대해 다룬다. 본 연구에서는 실시간 카메라변수 추출을 위해 영상에서 특징점을 자동으로 추출하고 인식하기 위한 방법과, 카메라 캘리브레이션 과정에서 렌즈의 왜곡특성 계산에 따른 계산량 문제를 해결하기 위한 방법을 제안한다.
camera tracking system : electromechanical / optical
pattern recognition
2D-3D pattern matches
planar pattern
feature extraction -> image-model matching & identification -> camera calibration
: to design the pattern by applying the concept of cross-ratio and to identify the pattern automatically
영상에서 찾아진 특징점을 자동으로 인식하기 위해서는 공간 상의 점들과 영상에 나타난 그것들의 대응점에 대해서 같은 값을 갖는 성질이 필요한데 이것을 기하적 불변량 (Geometric Invariant)이라고 한다. 본 연구에서는 여러 불변량 가운데 cross-ratio를 이용하여 패턴을 제작하고, 영상에서 불변량의 성질을 이용하여 패턴을 자동으로 찾고 인식할 수 있게 하는 방법을 제안한다.
radial alignment constraint
"If we presume that the lens has only radial distortion, the direction of a distorted point is the same as the direction of an undistorted point."
카메라의 움직임을 알아내기 위해서는 공간상에 인식이 가능한 물체가 있어야 한다. 즉, 어느 위치에서 보더라도 영상에 나타난 특징점을 찾을 수 있고, 공간상의 어느 점에 대응되는 점인지를 알 수 있어야 한다.
패턴이 인식 가능하기 위해서는 카메라가 어느 위치, 어느 자세로 보던지 항상 같은 값을 갖는 기하적 불변량 (Geometric Invariant)이 필요하다.
Coelho, C., Heller, A., Mundy, J. L., Forsyth, D. A., and Zisserman, A.1992. An experimental evaluation of projective invariants. In Geometric invariance in Computer Vision, J. L. Mundy and A. Zisserman, Eds. Mit Press Series Of Artificial Intelligence Series. MIT Press, Cambridge, MA, 87-104.
> initial identification process
extracting the pattern in an image: chromakeying -> gradient filtering: a first-order derivative of Gaussian (DoG) -> line fitting: deriving a distorted line (that is actually a curve) equation -> feature point tracking (using intersection filter)
이상적인 렌즈의 optical axis가 영상면에 수직이고 변하지 않는다고 할 때, 영상 중심은 카메라의 줌 동작 동안 고정된 값으로 계산된다. (그러나 실제 렌즈의 불완전한 특성 때문에 카메라의 줌 동작 동안 영상 중심 역시 변하게 되는데, 이 변화량은 적용 범위 이내에서 2픽셀 이하이다. 따라서 본 연구에서는 이러한 변화를 무시하고 이상적인 렌즈를 가정하여 줌동작에 의한 영상 중심을 구하게 된다.)
For zoom lenses, the image centers vary as the camera zooms because the
zooming operation is executed by a composite combination of several
lenses. However, when we examined the location of the image centers,
its standard deviation was about 2 pixels; thus we ignored the effect
of the image center change.
calculating lens distortion coefficient
Zoom lenses are zoomed by a complicated combination of several lenses so that the effective focal length and distortion coefficient vary during zooming operations.
When using the coplanar pattern with small depth variation, it turns out that focal length and z-translation cannot be separated exactly and reliably even with small noise.
카메라 변수 추출에 있어서 공간상의 특징점들이 모두 하나의 평면상에 존재할 때는 초점거리와 z 방향으로의 이동이 상호 연관 (coupling)되어 계산값의 안정성이 결여되기 쉽다.
collinearity
Collinearity represents a property when the line in the world coordinate is also shown as a line in the image. This property is not preserved when the lens has a distortion.
Once the lens distortion is calculated, we can execute camera calibration using linear methods.
filtering
가상 스튜디오 구현에 있어서는 시간 지연이 항상 같은 값을 가지게 하는 것이 필수적이므로, 실제 적용에서는 예측 (prediction)이 들어가는 필터링 방법(예를 들면, Kalman filter)은 사용할 수가 없었다.