개발자 블로그

영상처리 강좌 - 8. 이미지 내 마음대로 움직여보자(3) : 회전(rotation), 보간법(interpolation) 본문

영상처리강좌

영상처리 강좌 - 8. 이미지 내 마음대로 움직여보자(3) : 회전(rotation), 보간법(interpolation)

로이드.Roid 2016. 1. 4. 09:13

  안녕하세요.

  영상처리 강좌 여덟 번째 시간입니다~!

  오늘 배워볼 내용은 회전(Rotation)과 보간법(Interpolation)에 대해서 입니다. 이전시간에 보간법에 대해서 살짝만 언급했는데 오늘 보간법의 종류와 알고리즘에 대해서 알아보고 지난시간에 약간(?) 부족했던 확대를 보간법을 적용해서 보완해보도록 하겠습니다. 그 전에 먼저 회전에 대해서 공부하고 보간법으로 넘어가겠습니다.


본 강좌를 처음부터 보시려면 아래 링크를 클릭하세요. (새창)
 ☞ 2015/08/10 - [영상처리강좌] - 영상처리 강좌를 시작합니다~!!


  회전은 말그대로 이미지를 시계방향 또는 반시계방향으로 회전시키는 것 입니다. 이동, 확대, 축소와 마찬가지로 픽셀의 좌표만 새롭게 계산해주면 됩니다. 다만 이 계산식이 좀 복잡합니다.. 수식을 어떻게 입력해야 될지 몰라서 학교다닐 때 노트필기 했던 것을 이미지로 첨부합니다..-_-;;



  '임의의 점'을 기준으로 한 회전입니다. 보통 일반적인 경우라면 이미지의 중점이 되겠지요. 위 수식을 소스코드로 옮기면 아래와 같습니다.


// x0 : x축의 중점
// y0 : y축의 중점
// x1, y1 : 현재 옮기고자 하는 픽셀의 좌표
// x2, y2 : 이동 후 픽셀의 좌표
// degree : 회전각도

pi = 3.141592;
seta = pi / (180.0 / degree );

x2
= ( y1 - y0 ) * sin(seta) + ( x1 - x0 ) * cos(seta) + x0; y2 = ( y1 - y0 ) * cos(seta) - ( x1 - x0 ) * sin(seta) + y0;



  위 코드는 역시 이중 for문 내부에 위치하겠죠. 이게 회전의 전부입니다. 수식이 좀 복잡해서 그렇지 기본적인 소스의 구조는 전에 했던 이동, 확대, 축소 등과 다를바 없습니다. 

  그럼 소스를 보도록 하죠. 회전 함수의 전체 소스는 아래와 같습니다.

int fnRotateImg(PGMImage *img_org, PGMImage *img_new, double degree)
{
	int center_x = img_org->width  / 2;
	int center_y = img_org->height / 2;
	int new_x;
	int new_y;
	double pi = 3.141592;
	double seta;
	

	// seta값 계산
	seta = pi / (180.0 / degree);


	// 헤더값 복사
	img_new->M		= img_org->M;
	img_new->N		= img_org->N;
	img_new->width	= img_org->width;
	img_new->height = img_org->height;
	img_new->max	= img_org->max;

	//	*img_new = *img_org;	위 코드는 이런식으로 간단하게 할 수도 있음



	// <-- 메모리 할당 - 새로 생성될 이미지의 크기에 따라
	img_new->pixels = (unsigned char**)calloc(img_org->height, sizeof(unsigned char*));

	for(int i=0; i<img_org->height; i++){
	  img_new->pixels[i] = (unsigned char*)calloc(img_org->width, sizeof(unsigned char));
	}
	// -->


	
	// <-- 배경색으로 초기화
	for(int i=0; i<img_new->height; i++){
		for(int j=0; j<img_new->width; j++){
			img_new->pixels[i][j] = 0;	// 0: 검정색
		}
	}
	// -->



	// <-- 회전
	for(int i=0; i<img_org->height; i++){
	  for(int j=0; j<img_org->width; j++){
            new_x = (i-center_y) * sin(seta) + (j-center_x) * cos(seta) + center_x;
	    new_y = (i-center_y) * cos(seta) - (j-center_x) * sin(seta) + center_y;
			
	    if(new_x < 0 )		continue;
	    if(new_x >= img_org->width)	continue;
	    if(new_y < 0)		continue;
	    if(new_y >= img_org->height)	continue;

img_new->pixels[new_y][new_x] = img_org->pixels[i][j]; } } // --> return TRUE; }



  역시 프로그램의 전체 소스는 본문 제일 하단에 따로 첨부해 두었습니다.

  소스를 보면 회전에 필요한 중점구하기, seta값 계산 등의 로직만 추가 된 것을 알 수 있습니다. 근본적으로는 확대, 축소와 다른점이 없죠. 수식이 약간 복잡해서 그렇지 확대와 축소를 공부하면서 다 설명한 부분이라서 따로 설명드릴 부분은 없을 것 같습니다. 혹시 설명이 필요하시면 이전 강좌를 참고해 주시기 바랍니다.

  바로 결과를 확인해보도록 하겠습니다. 





  원본 사진을 20도 각도로 회전시킨 결과영상 입니다. 근데 뭔가 좀 이상하죠? 이번에도 지난번 '확대'와 마찬가지로 중간중간 빈 공간이 발생했습니다. 그렇죠? 이 현상을 좀 전문적인 용어로 정리하면..

  • 확대 : hole이 발생
  • 축소 : overlap이 발생
  • 회전 : hole과 overlap이 모두 발생


  이렇습니다. 오버랩은 따로 설명을 드리지 않았었는데, 간단히 설명드리면 원본에서 복수개의 픽셀들이 축소결과 영상의 한 픽셀에 겹쳐서 써지는 것을 말합니다. 10px 짜리 이미지를 3px로 축소한다면 대략 평균적으로 따져서 원본의 3개의 픽셀이 결과영상의 1개의 픽셀로 기록이 될 것입니다. 즉, 3개의 픽셀을 결과영상의 한 위치에 덮어서 기록하게 되는데 이러한 현상을 오버랩이라고 합니다.

  
  자~ 그럼 이제 드디어 보간법에 대해서 알아볼 때가 된 것 같네요. 보간법의 개념에 대한 설명은 이전시간에 했고 구체적인 설명은 링크로 대체하겠습니다.





  이제 보간법에 대해서 확실히 아셨나요? 그럼 또 코딩을 해봐야겠군요. 소스는 항상 첨부하지만 되도록이면 제 소스를 보기 전에 스스로 코딩을 해보는게 실력향상에 큰 도움이 될 것 입니다. 저 같은 경우 다른사람의 소스를 보게 되면, 그 방식 말고 다른 방식으로 생각하기가 꽤 힘들더군요. 스스로 문제를 해결해나가다 보면 점점 실력이 늘게 되지 않을까 하는 생각을 감히 말씀드려 봅니다. (근데 사실 직장에 다니다보니 저 스스로도 기존 소스에서 해답을 찾게되네요.. 일단 저부터 반성해야 되겠습니다. ㅋㅋ)

  쓸데없는 말이 좀 길었네요~ 아래는 보간법 소스입니다. 보간법의 여러 알고리즘이 있지만 가장 간단한 방법으로 구현했습니다.


int fnInterpolate(PGMImage *img)
{
	int left_pixval = 0;
	int right_pixval = 0;


	for(int i=0; i<img->height; i++){
		for(int j=0; j<img->width; j++){
			if(j == 0){
				right_pixval = img->pixels[i][j+1];
				left_pixval  = right_pixval;
			}
			else if(j == img->width - 1){
				left_pixval  = img->pixels[i][j-1];
				right_pixval = left_pixval;
			}
			else{
				left_pixval  = img->pixels[i][j-1];
				right_pixval = img->pixels[i][j+1];
			}
			

			if(img->pixels[i][j] == 0 && left_pixval != 0 && right_pixval != 0){
				img->pixels[i][j] = ( left_pixval + right_pixval ) / 2;
			}
		}
	}

	return TRUE;
}



  경계를 벗어났는지 체크하는 if문이 좀 길어서 그렇지 매우 간단한 소스입니다. 현재 기준이 되는 픽셀의 값이 0이고, 좌우의 픽셀은 모두 값이 있는경우, 해당 픽셀을 hole이라고 판단하고 좌우픽셀의 평균값으로 메꿔줍니다. 매우 간단하죠.

  그럼 결과를 보도록 하겠습니다.



  전에 있었던 hole이 모두 메꿔진게 보이시나요? 좀 전에 했었던 회전보다 한결 보기 좋아졌네요. 


  자~ 그럼 전에 확대를 배울 때도 발생했었던 hole을 메꿔보도록 하겠습니다. 확대는 회전과는 달리 hole이 바둑판에 쳐진 줄 처럼 가로줄, 세로줄 형태로 발생합니다. 따라서 이 공간을 메꾸는 방법도 약간 다른데요. 소스를 먼저 보고 설명드리겠습니다.

int fnInterpolate(PGMImage *img)
{
	int pre_pixval = 0;

	// → 방향으로 진행
	for(int i=0; i<img->height; i++){
		for(int j=0; j<img->width; j++){
			if(img->pixels[i][j] == 0 && pre_pixval != 0){
				img->pixels[i][j] = pre_pixval;
			}

			pre_pixval = img->pixels[i][j];
		}

		pre_pixval = 0;
	}


	// ↓ 방향으로 진행
	for(int m=0; m<img->width; m++){
		for(int n=0; n<img->height; n++){
			if(img->pixels[n][m] == 0 && pre_pixval != 0){
				img->pixels[n][m] = pre_pixval;
			}

			pre_pixval = img->pixels[n][m];
		}

		pre_pixval = 0;
	}


	return TRUE;
}



  이중 for문이 두개나 있는데 각각 설명드리면

  • 첫 번째 이중 for문 : 가로방향으로 진행하면서 hole을 만나면 바로 왼쪽의 픽셀값으로 메꿔줍니다. 
  • 두 번째 이중 for문 : 세로방향으로 진행하면서 hole을 만나면 바로 위쪽의 픽셀값으로 메꿔줍니다.

  이런 식으로 hole을 메꾸게 됩니다. 이 방법말고도 다양한 방법이 있습니다. 제가 사용한 방법은 소스는 간단하지만 퀄리티는 좀 떨어집니다. 좀전에 설명한 보간법 링크에서 다양한 알고리즘을 소개하고 있으니 좀 더 나은방법으로 개선해 보시는 것도 좋을 것 같습니다.

  그럼 결과를 보죠.



( 클릭하면 원본 크기로 볼 수 있습니다. )


  사진이 리사이즈 되어있어서 그냥 보면 둘다 괜찮아 보이는데, 클릭해서 원본으로 보면 확실히 확대한 사진의 퀄리티가 떨어지는 것을 알 수 있습니다. hole을 메꾼다는게 없는 정보를 주변의 정보를 참조해서 임의로 메꾸는 것이기 때문에 확대를 하면 당연히 원본보다는 퀄리티는 떨어집니다. 하지만 하나의 픽셀을 참조해서 메꾸기 보다는 아까 회전을 배울 때 처럼 좌우의 픽셀을 참조해서 평균값을 구한다거나 하면, 즉 참조하는 픽셀이 좀 더 많다면 조금 더 나은 결과사진을 얻을 수 있을 것입니다.


  오늘은 이미지의 회전과 보간법에 대해서 알아봤습니다. 보간법에 대한 설명을 링크로 대체했더니 뭔가 좀 허전한게 중요한걸 빼먹은 기분이 들고 그러네요..-_-;; 그 글들이 워낙에 설명이 잘 되어있어서 제가 설명하는 것 보다 훨씬 나을 듯 하여 링크로 대체했습니다. 양해 부탁드려요~

  이렇게 해서 영상의 위치에 기반한 처리 - 이동, 미러, 플립, 크롭, 축소, 확대, 회전 - 에 대해서 모두 알아보았습니다. 분량이 좀 많긴 했지만 기본적인 원리는 다 거기서 거기라 부담되지는 않았을거라 생각합니다.

  
그 동안 흑백이미지 보기 좀 지루하시지 않았나요?  다음 강의부터는 드디어 컬러영상을 가지고 놀아보도록 하겠습니다~ 다음강의의 주세는 'PPM 파일을 읽어보자' 입니다. 이것 역시 처음에 개념잡는게 좀 어려울지 몰라도 기본적인 구조는 뭐 다 거기서 거기입니다~ 

  혹시 궁금한 부분이나 잘못된 부분은 댓글로 남겨주세요~ 응원의 메시지도 환영입니다~ㅋ 

  그럼~ 부족한 강의 봐주셔서 대단히 감사합니다.



번 강의에 사용된 소스코드를 다운받으시려면 아래 파일을 클릭하세요.

그냥회전.cpp

회전(보간법).cpp

확대(보간법).cpp



※ 본 포스트에 대한 링크는 가능하지만, 퍼가는 것은 정중하게 사양합니다.


Comments