Home [데브코스] 6주차 - OpenCV Image Geometric Transformation
Post
Cancel
Preview Image

[데브코스] 6주차 - OpenCV Image Geometric Transformation


컬러 영상 처리의 기초

컬러 영상의 픽셀 값 참조

  • OpenCV에서 컬러 영상 표현 방법
    • 빨강,초록, 파랑 색 성분을 256단계로 표현
    • opencv에서는 RGB순서가 아니라 BGR순서임
  • OpenCV에서 컬러 영상 다루기
1
2
3
4
5
6
7
8
9
Mat img1 = imread("lenna.bmp",IMREAD_COLOR);
Mat img2(rows,cols, CV_8UC3); // 3 channels

Mat img3 = imread("lenna.bmp",IMREAD_GRAYSCALE);
Mat img4;
cvtColor(img3,img4,COLOR_GRAY2BGR); // 눈으로 보기에는 동일하게 회색으로 나오게 된다. 이 때는 B=G=R 값이 같기 때문이다. 그러므로 1픽셀당 3byte를 차지하고 있다.

circle(src,Point(200,200), 100, Scalar(255,0,0),3); // grayscale이므로 밝기가 된다. 그래서 색이 아닌 밝기가 255
circle(dst,Point(200,200), 100, Scalar(255,0,0),3); // truecolor이므로 bgr순서의 스칼라값이 된다. 그래서 파란색


1
2
3
Mat src = imread("lenna.bmp");

Mat dst = 255 - src; // 이렇게 하면 파란색으로 된다.

이 결과는 올바르지 않다. 이 코드는 Mat dst = Scalar(255,0,0,0) -src;로 인식되므로 blue성분만 처리된다.



따라서 아래와 같이 3채널로 지정해줘야 반전이 된다.

1
Mat dst = Scalar(255,255,255) - src; 


반전을 직접 구현하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
Mat dst(src.rows, src.cols, CV_8UC3);

for (int y = 0; dst.rows; y++) {
    for (int x = 0; x < dst.cols; x++) {
        Vec3b& p1 = src.at<Vec3b>(y, x);
        Vec3b& p2 = dst.at<Vec3b>(y, x);
        p2[0] = 255 - p1[0]; //Blue
        p2[1] = 255 - p1[1]; //Green
        p2[2] = 255 - p1[2]; //Red
    }
}

여기서 중요한 것은 원래는 uchar이었던 자료형대신 Vec3b 자료형을 사용해야 한다. 전체 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 1번째 방법 */
Vec3b& p1 = src.at<Vec3b>(y, x);
Vec3b& p2 = dst.at<Vec3b>(y, x);
p2[0] = 255 - p1[0]; //Blue
p2[1] = 255 - p1[1]; //Green
p2[2] = 255 - p1[2]; //Red

/* 2번째 방법 */
Vec3b& p1 = src.at<Vec3b>(y, x);
Vec3b& p2 = dst.at<Vec3b>(y, x);
p2 = Vec3b(255, 255, 255) - p1;

/* 3번째 방법 */
dst.at<Vec3b>(y, x) = Vec3b(255, 255, 255) - src.at<Vec3b>(y, x);

이 3가지 방법은 다 동일하게 동작한다.



이동 변환과 전단 전환

  • 강체변환(Rigid-body) : 크기 및 각도가 보존되는 변환 (translation,rotation)
  • 유사변환(similarity) : 크기는 변하고 각도는 보존되는 변환 (scaling)
  • 선형변환(linear) : vector공간에서의 이동
  • affine : 선형변환과 이동변환까지 포함, 선의 수평선은 유지
  • perspective : affine 변환에 수평성도 유지되지 않음, 원근 변환

영상의 기하학적 변환(geometric transformation)

영상을 구성하는 픽셀의 배치 구조를 변경함으로써 전체 영상의 모양을 바꾸는 작업이다. 전처리 작업, 영상 정합(image registration), 왜곡 제거 등이 이에 해당한다.

영상의 이동 변환

이동 변환(translation transform)

  • 가로 또는 세로 방향으로 영상을 특정 크기만큼 이동시키는 변환
  • x축과 y축 방향으로의 이동 변위를 지정할 수 있다.

출력 영상을 (x’, y’)이라 할 때

  • x’ = x + a
  • y’ = y + b

이를 행렬로 표기하면 다음과 같다.

아래 행렬에서 1은 행렬의 수식을 위해 존재하는 추가항이다. 이 때, 2x3 행렬을 2x3 어파인 변환 행렬이라 한다.


  • 코드 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);

Mat dst = Mat::zeros(src.size(), CV_8UC1);

for (int y = 0; y < src.rows; y++) {
		for (int x = 0; x < src.cols; x++) {
			int x_ = x + 100;
			int y_ = y + 100;
			if (x_ < 0 || x_ >= dst.cols) continue;
			if (y_ < 0 || y_ >= dst.rows) continue;
			dst.at<uchar>(y_, x_) = src.at<uchar>(y, x);
		}
	}

그레이 스케일 영상을 (100,100) 이동하는 변환 프로그램을 나타낸 것이다. 이중 for 루프를 이용하여 직접 구현했다. 이를 구현한 이유는 OpenCV에서는 이동 변환이나 전단 변환에 대한 함수가 따로 지원되지 않아서 알고리즘을 직접 구현하거나 행렬을 직접 생성해서 warpAffine을 시킬 수 있다.

1
2
3
4
Mat trans = (Mat_<float>(2, 3) << 1, 0, 100, 0, 1, 100); // (1 0 100; 0 1 100) ==> 가로 100, 세로 100 이동하는 이동 변환 행렬

Mat dst; 
warpAffine(src, dst, trans, Size()); 


행렬(trans)에서 1대신 0.5씩 주면 1/2배 축소되어 출력되는 것을 볼 수 있다.

1
2
3
4
Mat trans = (Mat_<float>(2, 3) << 0.5, 0, 100, 0, 0.5, 100); 

Mat dst; 
warpAffine(src, dst, trans, Size(700,700)); // 사이즈를 키워서 하게 되면 이동을 하면서 사라진 부분까지 볼 수 있다. Size(700,700)

이를 통해 크기 변환과 이동 변환을 함께 줄 수 있다.



영상의 전단 변환(shear transformation)

직사각형 형태의 영상을 한쪽 방향으로 밀어서 평행사변형 모양으로 변형되는 변환, 층밀림 변환이라고도 한다. 가로 방향 또는 세로 방향으로 정의된다.

첫번째는 x좌표가 이동하는 형태를 통해 변형되는 것이고, 두번째는 y좌표가 변형되는 것이다.


  • 코드 구현
1
2
3
4
5
6
7
8
9
10
Mat dst(src.rows * 3 / 2, src.cols, src.type(), Scalar(0));

double m = 0.5;
for (int y = 0; y < src.rows; y++) {
  for (int x = 0; x < src.cols; x++) {
    int nx = x;
    int ny = int(y + m*x);
    dst.at<uchar>(ny, nx) = src.at<uchar>(y, x);
  }
}

코드를 자세히 살펴보게 되면, 먼저 dst의 크기를 원래 크기의 1.5배 해줌으로써 나중에 예외처리를 하지 않아도 되도록 또는 이미즹 변형된 형태를 보기 위해서 1.5배 해주었다. x, 즉 rows방향만 변형되기 때문에, cols에 대해서는 크기를 늘려주지 않은 것을 볼 수 있다.

그 후 for 루프를 도는데, (y+m*x)에서 m이 0.5이므로 최대 원본의 1.5배까지 커지도록 되어 있음을 알 수 있다.

마지막으로 결과 영상의 픽셀 값을 입력 영상의 픽셀 값으로 집어 넣어주면 된다.



영상의 크기 변환(scale transform)

영상의 크기를 원보 영상보다 크게 또는 작게 만드는 변환을 말한다. x축과 y축 방향으로의 스케일 비율(scale factor)를 지정할 수 있다.

이 때, scale factor을 보면 Sx = w'/w,Sy = h'/h 로 표현된다.


  • 코드 구현
1
2
3
4
5
6
7
8
9
10
Mat dst = Mat::zeros(src.rows * 2, src.cols * 2, CV_8UC1);

for (int y = 0; y < src.rows; y++) {
  for (int x = 0; x < src.cols; x++) {
    int x_ = x * 2;
    int y_ = y * 2;

    dst.at<uchar>(y_, x_) = src.at<uchar>(y, x);
  }
}

단순히 2배로 키우게 되면 중간중간에 검정색이 존재하게 된다. 위의 코드를 순방향 매핑(foreward mapping)이라고 하는데, 입력 영상의 좌표만큼을 for문을 돌면서 입력 영상의 좌표값을 출력 영상의 좌표값으로 설정하는 코드를 맗한다. 이 경우 채워지지 않은 픽셀이 존재하게 된다. 이 문제를 해결하기 위해 역방향 매핑을 진행한다.

역방향 매핑(backward mapping)

순방향 매핑의 경우

  • x’ = Sx * x
  • y’ = Sy * y

그러나 역방향 매핑의 경우

  • x = x’ / Sx
  • y = y’ / Sy

를 통해 결과 영상의 좌표(x’,y’)에서 원본 영상의 좌표(x,y)를 참조하도록 만든다. 이를 코드로 구현하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);
Mat dst = Mat::zeros(src.rows * 2, src.cols * 2, CV_8UC1);

for (int y_ = 0; y_ < dst.rows; y_++) {
  for (int x_ = 0; x_ < dst.cols; x_++) {
    int x = x_ / 2;
    int y = y_ / 2;
    dst.at<uchar>(y_, x_) = src.at<uchar>(y, x);
  }
}

픽셀값들이 계단형태로 나오고 있다. 그 이유는 scale factor이 4라고 가정할 때, x_ = 0,1,2,3 일 때는 x=0 인 픽셀값을 참조하고, x_ = 4,5,6,7일 때는 x=1인 픽셀값을 참조하기 때문이다. 이 문제를 해결하기 위해 보간법을 사용한다.


보간법

역방향 매핑에 의한 크기 변환 시, 참조해야 할 입력 영상의 (x,y) 좌표가 실수 좌표일 경우

  • (x,y)와 가장 가까운 정수 좌표의 픽셀 값을 참조하거나
  • (x,y) 근방의 정수 좌표 픽셀 값을 이용하여 실수 좌표 위치의 픽셀 값을 추정해야 한다.

보간법은 실수 좌표 상에서의 픽셀 값을 결정하기 위해 주변 픽셀 값을 이용하여 값을 추정하는 방법을 말한다.


주요 보간법

  • 최근방 이웃 보간법(nearest neighbor interpolation)
  • 양선형 보간법(bilinear interpolation)
  • 3차 보간법(cubic interpolation)
  • 스플라인 보간법(spline interpolation)
  • 란쵸스 보간법(lanczos interpolation)


가장 단순한 방법은 최근방 이웃 보간법이다. 즉 가장 가까운 위치에 있는 픽셀의 값을 참조하는 방법을 말한다. 장점은 빠르고 구현하기 쉽다. 단점으로는 계단 현상이 발생한다. 방금 우리가 해봤던 방법이 이에 해당한다.


이 계단 현상을 해결하기 위해 양선형 보간법을 많이 사용한다. 실수 좌표를 둘러싸고 있는 네 개의 픽셀 값에 가중치를 곱한 값들의 선형 합으로 결과 영상의 픽셀 값을 구하는 방법이다. 최근방 이웃 보간법에 비해서는 느리지만 비교적 빠르며 계단 현상이 크게 감소한다.


  • 양선형 보간법 구현 방법

실수 좌표를 둘러싸고 있는 네 개의 픽셀 값을 이용해야 한다. 예를 들어 점(double p,double q)가 있다고 하고, 이를 (10.3,20.5) 에 해당한다고 하면

이를 둘러싼 4개의 픽셀 값은 (10,20),(11,20),(10,21),(11,21) 이 될 것이다.

a,b,c,d는 픽셀 값의 크기를 의미한다. 이를 통해 x는 (1-p) * a 와 p * b 를 더하면 된다.

  • x = (1-p)a + pb
  • y = (1-p)c + pd
  • z = (1-q)x + qy

z는 보간법을 통해 얻은 해당 픽셀의 픽셀 값이다. 따라서 우리는 z만 쓰면 된다.

  • 코드 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void resizeBilinear(const Mat& src, Mat& dst, Size size)
{
	dst.create(size.height, size.width, CV_8U);

	int x1, y1, x2, y2;	double rx, ry, p, q, value;
	double sx = static_cast<double>(src.cols - 1) / (dst.cols - 1); // 가로 방향 scale factor
	double sy = static_cast<double>(src.rows - 1) / (dst.rows - 1); // 세로 방향 scale factor

	for (int y = 0; y < dst.rows; y++) {
		for (int x = 0; x < dst.cols; x++) {
			rx = sx * x; ry = sy * y;
			x1 = cvFloor(rx); y1 = cvFloor(ry);
			x2 = x1 + 1; if (x2 == src.cols) x2 = src.cols - 1;
			y2 = y1 + 1; if (y2 == src.rows) y2 = src.rows - 1;
			p = rx - x1;q = ry - y1;

			value = (1. - p) * (1. - q) * src.at<uchar>(y1, x1)  // src.at<uchar>(y1, x1) == x1,y1에서의 픽셀 값, a
				+ p * (1. - q) * src.at<uchar>(y1, x2)  // b
				+ (1. - p) * q * src.at<uchar>(y2, x1)  // c
				+ p * q * src.at<uchar>(y2, x2);  // b

			dst.at<uchar>(y, x) = static_cast<uchar>(value + .5);
		}
	}
}

코드에서 (x1,y1),(x2,y2)는 아래 그림을 참고하면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(void)
{
	Mat src = imread("camera.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return;
	}

	Mat dst;
	resizeBilinear(src, dst, Size(1024, 1024));

	imshow("src", src);
	imshow("dst", dst);
	waitKey();
}

이 코드를 실행하게 되면 아래와 같다. 아래는 1024,1024 크기로 키운 결과이다.



  • 3차 보간법(bicubic interpolation)

방금까지는 2차원 보간법을 진행했다. 그러나 3차 즉 3차원의 픽셀값을 계산하는 방법이다. 그래서 총 16개의 픽셀 값에 3차 함수를 이용한 가중치를 부여하여 결과 영상 픽셀의 값을 계산한다.

이는 그냥 직접 구현하기보다 OpenCV에서 제공하는 함수를 사용해볼 것이다.



위의 코드들은 알고리즘을 직접 구현했다. 그러나 OpenCV의 resize()라는 함수를 통해 쉽게 사용할 수 있다.

1
void resize(InputArray src, OutputArray dst,Size dsize, double fx = 0, double fy = 0, int interpolation = INTER_LINEAR);
  • src,dst : 입력, 출력 영상
  • dsize : 결과 영상의 크기, Size()로 지정하면 fx,fy에 의해 자동 결정된다.
  • fx,fy : x,y방향 스케일 비율, dsize값이 0일 때 유효하다.
  • interpolation : 보간법 지정 상수
    • INTER_NEAREST : 최근방 이웃 보간법
    • INTER_LINEAR : 양선형 보간법 (2x2 이웃 픽셀 참조)
    • INTER_CUBIC : 3차회선 보간법 (4x4 이웃 픽셀 참조)
    • INTER_LANCZOS4 : Lanczos 보간법 (8x8 이웃 픽셀 참조)
    • INTER_AREA : 영상 축소 시 효과적인 방법


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Mat src = imread("rose.bmp");

Mat dst1, dst2, dst3, dst4;
resize(src, dst1, Size(), 4, 4, INTER_NEAREST); 
// 이를 1번더 한 이유는 이미지를 불러올 때 시간이 걸려서 정확한 판단이 잘 안될 수 있기 때문에 미리 1번 불러와서 정확한 시간을 측정하기 위함

tm.start();
resize(src, dst1, Size(), 4, 4, INTER_NEAREST);
tm.stop();
cout << "INTER_NEAREST: " << tm.getTimeMilli() << "ms. " << endl;
tm.reset(); // reset을 하지 않을 경우 뒤에 측정한 것이 앞에 것과 합쳐져 출력된다.
tm.start();
resize(src, dst2, Size(1920, 1280));
tm.stop();
cout << "INTER_LINEAR: " << tm.getTimeMilli() << "ms. " << endl;
tm.reset();
tm.start();
resize(src, dst3, Size(1920, 1280), 0, 0, INTER_CUBIC);
tm.stop();
cout << "INTER_CUBIC: " << tm.getTimeMilli() << "ms. " << endl;
tm.reset();
tm.start();
resize(src, dst4, Size(1920, 1280), 0, 0, INTER_LANCZOS4);
tm.stop();
cout << "INTER_LANCZOS4: " << tm.getTimeMilli() << "ms. " << endl;

imshow("src", src);
imshow("dst1", dst1(Rect(400, 500, 400, 400)));
imshow("dst2", dst2(Rect(400, 500, 400, 400)));
imshow("dst3", dst3(Rect(400, 500, 400, 400)));
imshow("dst4", dst4(Rect(400, 500, 400, 400)));
waitKey();

총 4가지의 방법으로 진행해서 비교해보았다. dst1,2,3,4 다 동일한 크기로 resize된다.

  • dst1 : fx,fy를 지정해줌으로써 size가 알아서 결정된다.
  • dst2 : size를 지정해주어서 fx,fy를 지정해주지 않았다. 그리고 interpolation은 default가 INTER_LINEAR이므로 지정하지 않을 경우 INTER_LINEAR 방식을 사용한다.
  • dst3 : INTER_CUBIC
  • dst4 : INTER_LANCZOS4

그리고 출력 영상이 너무 크기 때문에 특정 크기만큼 잘라서 보고자 했다. (400,500)위치에서 가로 400, 세로 400 크기만큼만 화면에 출력하도록 했다.

이와 같이 비교해보면 3,4가 화질이 좋다. 그러나 연산량을 비교해보면 3번이 2번보다 1.5배 정도 더 걸리고, 4번의 경우 3번의 2배 정도의 시간이 더 걸린다.


  • 영상의 축소 시 고려할 사항
    • 한 픽셀로 구성된 성분들은 영상을 축소할 때 사라지는 경우가 발생할 수 있다.
    • 입력 영상을 부드럽게 필터링(블러링)한 후 축소하거나 다단계 축소를 권장한다.
    • OpenCV의 resize() 함수에서는 INTER_AREA 플래그를 사용하면 축소할 때 발생하는 화질의 열화를 방지할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main(void)
{
	Mat src(1280, 1280, CV_8UC3, Scalar(255, 255, 255));

	rectangle(src,Rect(600,600,100,100),Scalar(0,0,0));
	rectangle(src, Rect(500, 630, 50, 50), Scalar(0, 0, 0));

	Mat kernel = Mat::ones(3, 3, CV_32FC1) / 9.f;

	Mat dst1, dst2, dst3, dst4;

	GaussianBlur(src, dst3, Size(), 1.0);
	GaussianBlur(src, dst4, Size(), 1.0);

	resize(src, dst1, Size(320, 320),INTER_AREA);
	resize(src, dst2, Size(320, 320),INTER_LINEAR);
	resize(src, dst3, Size(320, 320), INTER_LINEAR);
	resize(src, dst4, Size(320, 320), INTER_AREA);

	imshow("src", src);
	imshow("dst1", dst1);
	imshow("dst2", dst2);
	imshow("dst3", dst3);
	imshow("dst4", dst4);
	waitKey();
}

흰색 배경에 검은색 두께 1인 사각형 2개를 그리고, 축소를 시켰더니 선들이 사라지는 것을 볼 수 있다. 나의 경우에는 INTER_AREA를 사용하거나, 블러링하고 축소를 해도 사각형이 사라지는 현상이 발생했다.



영상의 회전 변환(rotation transform)

영상을 특정 각도만큼 회전시키는 변환이다. OpenCV에서는 반시계 방향을 기본으로 사용한다.

노란색이 입력 영상, 초록색이 회전된 영상을 표현한다.


이 때, 회전했을 때 y가 0보다 작으면 오류가 나므로 회전한 좌표가 유효한 좌표인지 검사를 하는 것이 중요하다. 또한, 정방향 매핑이 아닌 역방향 매핑을 해야 빈 픽셀이 발생하지 않는다.

입력 영상은 출력 영상과 역행렬을 곱한 것으로 표현할 수 있다.

추가적으로 보간법도 선택하여 진행한다.

  • 함수 - 영상의 회전 변환 행렬
1
Mat getRotationMatrix2D(Point2f center, double angle, double scale);
  • center : 회전 중심 좌표
  • angle : (반시계 방향) 회전 각도(degree), 음수는 시계 방향
  • scale : 회전 후 확대 비율
  • 반환값 : 2x3 double(CV_64F) 행렬 (어파인 변환 행렬)

이 때, center 좌표를 단순히 0,0으로 두려면 Point2f pt;로 할 수 있지만, 대체로 영상 중심의 좌표를 center로 두기 때문에

1
Point2f pt(src.cols/2.f, src.rows/2.f)

주의해야 할 것은 cols==y가 먼저 나온다는 것이다.

반환하는 행렬은 다음과 같다.


  • 함수 - 어파인 변환
1
void warpAffine(InputArray src, OutputArray dst, InputArray M, Size dsize, int flags= INTER_LINEAR, int borderMode = BORDER_CONSTANT, const Scalar& borderValue = Scalar());
  • src,dst : 입력, 출력 영상, 두 개는 같은 타입이어야 함
  • M : 2x3 어파인 변환 행렬, CV_32F or CV_64F, 위의 회전 변환 행렬의 반환값을 여기에 입력
  • dsize : 결과 영상의 크기, 입력 영상과 동일하게 하려고 하면 Size()로 지정
  • flags : 보간법 선택
  • borderMode : 가장자리 픽셀 처리 방식
  • borderValue : BORDER_CONSTANT 모드 사용 시 사용할 픽셀 값

실제로 회전을 시키기 위해서는 이 함수를 선언해야 한다.

  • 코드 구현
1
2
3
4
5
6
7
8
9
10
11
Mat src = imread("lenna.bmp");

float degree = 50;
Point2f pt(src.cols / 2.f, src.rows / 2.f);
Mat rot = getRotationMatrix2D(pt, degree, 1.0);

Mat dst;
warpAffine(src, dst, rot, Size(700,700)); // 512,512 를 700,700으로 키워서 잘리는 부분까지 추출

imshow("dst", dst);
waitKey();



이동, 크기, 회전 변환 조합

  • 크기 -> 회전 변환할 경우

Sx,Sy 는 크기에 대한 scale factor, 그 앞에 있는 2x2 행렬이 회전에 대한 변환이다. 뒤에서부터 계산하여 출력 영상을 만들어낸다.


  • 크기 -> 회전 -> 크기 변환할 경우

크기 변환에 대한 변환을 1번 더 진행한다. 이 3개를 하나의 2x2 행렬로 표현이 가능하다.


  • 이동 -> 크기 변환할 경우

이동 변환을 먼저 수행하기 때문에 x방향으로 a, y방향으로 b만큼을 이동한 결과를 크기 변환한다.


  • 이동 -> 크기 -> 회전 변환할 경우

이 경우에도 2x2행렬 * [x,y] + 2x1행렬 로 나타낼 수 있는데, 덧셈이 들어가게 되면 연산이 번거로워진다. 연산의 번거로움을 해결하기 위해 동차 좌표계를 사용한다.


동차 좌표계(homogenous coordinates)

차원의 좌표를 1차원 증가시켜서 표현하는 방법을 말한다. 예를 들어 2차원(x,y)좌표를 (x,y,1)로 표현할 수 있다. 이를 통해 각각의 변환들을 표현할 수 있다.

이처럼 오른쪽 식으로 변환할 수 있다. 이 때 마지막 항은 항상 1이다.


동차 좌표계를 활용하여 이동 변환 -> 크기 변환 -> 회전 변환을 수행한다면 아래 식처럼 쓸 수 있다.


getRotationMatrix2D() 함수를 이용하여 영상의 중앙 기준 회전한다고 할 때, 한 이미지를 (-cx,cy)만큼 이동 변환하여 영상의 중심이 (0,0)에 오도록 한다. 그 후 이미지를 회전 변환하고, 다시 (cx,cy)만큼 이동 변환하여 제자리로 이동시킨다.

이를 행렬로 나타내면 다음과 같다.

이를 전개하면 위에서 봤던 getRotationMatrix2D() 함수의 반환값과 똑같은 형태가 된다.


영상의 대칭 변환(flip,reflection)

영상의 상하/좌우/원점 대칭이 있다. 상하/좌우 대칭의 경우 축을 기준으로 반전시킨 후 이동 변환을 통해 원래 자리로 되돌리는 것이다.

  • 함수
1
void flip(InputArray src, OutputArray dst, int flipCode);
  • src, dst : 입력, 출력 영상
  • flipCode : 대칭 방향 지정
    • 양수(+1) : 좌우 대칭
    • 0 : 상하 대칭
    • 음수(-1) : 원점 대칭



어파인 변환과 투시 변환

어파인 변환(affine transform) vs. 투시 변환(perspective transform == projective transfrom)

어파인 변환

  • translation
  • shear
  • scaling
  • rotation
  • parallelograms

이 어파인 변환은 3x3행렬로 표현되지만, 마지막 열은 [0,0,1] 이므로 2x3 행렬(6 DOF == 6개의 미지수)이라고도 말할 수 있다.


투시 변환

  • trapazoids

이 투시 변환은 3x3행렬(8 DOF)로 표현된다.


affine transform의 경우 직사각형이 평행사변형이 되므로 마지막 1점은 추론이 가능하다. 각 점마다 수식이 2개씩 나오게 되므로 총 6개의 수식만 알면 된다.

perspective transform의 경우 점이 각각 변하기 때문에 4개 다 계산이 필요하다. 그래서 총 8개의 수식이 필요하다.


  • 어파인 변환 점 행렬 구하기
1
2
Mat getAffineTransform(const Point2f src[], const Point2f dst[]);
Mat getAffineTransform(InputArray src, InputArray dst);
  • src : 3개의 원본 좌표점(point2f src[3]; 또는 vector<Point2f> src;)
  • dst : 3개의 결과 좌표점(point2f dst[3]; 또는 vector<Point2f> dst;)
  • 반환값 : 2x3 크기의 변환 행렬 (CV_64F)

원래의 좌표점src와 변환된 결과 좌표점dst를 지정하면 변환 행렬을 출력해준다.

getRotationMatrix2D() 함수와 반환값이 같지만 이 함수는 회전 각도를 통해 어떻게 회전을 할지에 대한 함수이다.


  • 영상의 어파인 변환
1
void warpAffine(InputArray src, OutputArray dst, InputArray M, Size dsize, int flags= INTER_LINEAR, int borderMode = BORDER_CONSTANT, const Scalar& borderValue = Scalar());
  • src,dst : 입력, 출력 영상, 두 개는 같은 타입이어야 함
  • M : 2x3 어파인 변환 행렬, CV_32F or CV_64F, 위의 회전 변환 행렬의 반환값을 여기에 입력
  • dsize : 결과 영상의 크기, 입력 영상과 동일하게 하려고 하면 Size()로 지정
  • flags : 보간법 선택
  • borderMode : 가장자리 픽셀 처리 방식
  • borderValue : BORDER_CONSTANT 모드 사용 시 사용할 픽셀 값


  • 투시 변환 행렬 구하기
1
2
Mat getPerspectiveTransform(const Point2f src[], const Point2f dst[], int solveMethod = DECOMP_LU);
Mat getPerspectiveTransform(InputArray src,InputArray dst, int solveMethod = DECOMP_LU);
  • src : 4개의 원본 좌표점(Point2f src[4]; 또는 vector<Point2f> src;)
  • dst : 4개의 원본 좌표점(Point2f dst[4]; 또는 vector<Point2f> dst;)
  • solveMethod : 어떻게 계산할지에 대한 수학적 방법
  • 반환값 : 3x3 크기의 변환 행렬 (CV_64F)


  • 영상의 투시 변환
1
void warpPerspective(InputArray src, OutputArray dst, InputArray M, Size dsize, int flags = INTER_LINEAR, int borderMode = BORDER_CONSTANT, const Scalar& borderValue = Scalar());
  • src,dst : 입,출력 영상, 둘은 같은 타입
  • M : 3x3 투시 변환 행렬, CV_32F 또는 CV_64F
  • dsize : 결과 영상의 크기, 입력 영상과 동일하게 하려고 하면 Size()로 지정
  • flags : 보간법 선택
  • borderMode : 가장자리 픽셀 처리 방식
  • borderValue : BORDER_CONSTANT 모드 사용 시 사용할 픽셀 값


추가적으로 4개의 대응점으로부터 투시 변환 행렬을 구하는 식에 대해 살펴보고자 한다.



차선 영상의 조감도(bird’s eye view) 만들기

bird’s eye view : 새가 하늘에서 내려다보듯이 매우 높은 곳에 위치한 카메라가 아래의 피사체를 찍은 화면을 말한다. 투시 변환을 이용하여 전면에서 촬영된 영상을 버드아이뷰처럼 변환할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	VideoCapture cap("../../../data/test_video.mp4");

	if (!cap.isOpened()) {
		cerr << "Video open failed!" << endl;
		return -1;
	}

	Mat src;
	while (true) {
		cap >> src;

		if (src.empty())
			break;

		int w = 500, h = 260;

		vector<Point2f> src_pts(4); // 입력 영상에서의 점들의 좌표 4개
		vector<Point2f> dst_pts(4); // 출력 영상에서의 점들의 좌표 4개

		src_pts[0] = Point2f(474, 400);	src_pts[1] = Point2f(710, 400); // 특정 위치에서 차선을 기준으로 한 사다리꼴의 점 좌표
		src_pts[2] = Point2f(866, 530); src_pts[3] = Point2f(366, 530);

		dst_pts[0] = Point2f(0, 0);	dst_pts[1] = Point2f(w - 1, 0); // 출력 영상에서의 좌측 상단, 우측 상단
		dst_pts[2] = Point2f(w - 1, h - 1);	dst_pts[3] = Point2f(0, h - 1); // 우측 하단, 좌측 하단

		Mat per_mat = getPerspectiveTransform(src_pts, dst_pts); // 입력과 출력 좌표를 통해 matrix 획득

		Mat dst;
		warpPerspective(src, dst, per_mat, Size(w, h)); // 투시 변환 실행, 크기는 임의의 크기 500,260

#if 1
		vector<Point> pts; // 자료형을 변환시키기 위한 변수
		for (auto pt : src_pts) {
			pts.push_back(Point(pt.x, pt.y)); // polylines가 int로 받는데, 위의 point2f는 float타입이르모 point를 통해 int로 변환
		}
		polylines(src, pts, true, Scalar(0, 0, 255), 2, LINE_AA); // 위의 입력 영상에서의 좌표 4개에 대한 사각형 표시
#endif

		imshow("src", src);
		imshow("dst", dst);

		if (waitKey(10) == 27)
			break;
	}
}
This post is licensed under CC BY 4.0 by the author.