개발자 블로그

영상처리 강좌 - 6. 이미지 내 마음대로 움직여보자(1) : 이동(Move), 미러(Mirror), 플립(Flip) 본문

영상처리강좌

영상처리 강좌 - 6. 이미지 내 마음대로 움직여보자(1) : 이동(Move), 미러(Mirror), 플립(Flip)

로이드.Roid 2016. 1. 3. 21:50

  안녕하세요.

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

  오늘 배워볼 내용은 이미지를 이동시키는 것 입니다. 이전시간에 배운 내용은 이미지의 픽셀 값에 기반한 처리였었고, 오늘부터 강의할 내용은 이미지내 픽셀의 좌표값(x, y)에 기반한 처리 입니다. 먼저 오늘은 영상의 이동, 미러(Mirror - 좌우반전), 플립(Flip - 상하반전) 등의 처리에 대해서 알아보겠습니다.


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



  이미지의 가로를 x, 세로를 y라고 편의상 지정하겠습니다. 기준점은 좌측상단이 되며, 이곳의 좌표값은 (0, 0) 입니다. 크기가 3 x 4인 영상(가로 3px, 세로 4px)을 예로들어 좌표값을 보자면 아래와 같습니다.

(0, 0)(0, 1)(0, 2)
(1, 0)(1, 1)(1, 2)
(2, 0)(2, 1)(2, 2)
(3, 0)(3, 1)(3, 2)



  좌측 상단 (0, 0)을 기준으로 y값은 아래로 갈수록 증가하고, x값은 우측으로 갈수록 증가합니다.

  이것을 소스코드로 표현하면 image[4][3] ( image[y][x] )이 될 것 입니다. (이 부분이 잘 이해가 안된다면 C언어의 2차원배열에 대해서 먼저 학습하신 후 본 강좌를 보시기를 추천합니다.) 

  좀 더 쉬운 이해를 위해서는 image[행][열], 또는 image[가로][세로] 라고 표현할 수도 있겠네요. 이미지의 이동은 이 좌표값에 대한 연산으로 이해하시면 됩니다. 이미지를 좌우로 이동시키는 것은 image[행][열]에서 '행'에 대해 (+) 또는 (-) 연산을 통해서 좌표를 이동시키는 것이고, 상하로 이동시키는 것은 '열'에 대해 (+) 또는 (-) 연산을 하는 것 입니다. 

  횡방향(오른쪽왼쪽)으로 이동을 시킨하고 했을 때, 좌표값에 (+)를 하게되면 영상은 오른쪽으로 이동하고 (-)를 하면 왼쪽으로 이동하는 것이고, 종방향(위아래)으로 이동을 시킨다면 (+)가 아래, (-)가 위로 가게 됩니다. 종방향이 약간 햇갈릴수도 있는데 좌상단이 기준점이 되고, 값이 커질수록 아래로 이동하게 됩니다.

  이게 이동의 전부 입니다. 예를 들어 설명을 드리면, 만약 이미지를 오른쪽으로 10픽셀만큼 옮기겠다라고 했을 때는 아래 박스에 보이는 것 처럼 y좌표는 변경 없이, x좌표에 대해서 일괄적으로 +10을 해주면 됩니다. 

image[0][0]   -> image[0][10];
image[0][1]   -> image[0][11];
image[0][2]   -> image[0][12];
   ...
image[30][0] -> image[30][10];
image[30][1] -> image[30][11];
image[30][2] -> image[30][12];




  종방향에 대한 이동도 이와 비슷합니다. 좌우로 이동시키는게 image[행][열]에서 '행'이 아닌 '열'에 대해서 연산을 해주면 됩니다. 이미지를 아래로 10픽셀 옮기는 것은 아래와 같습니다.

image[0][0]   -> image[10][0];
image[0][1]   -> image[10][1];
image[0][2]   -> image[10][2];
    ...
image[30][0] -> image[40][0];
image[30][1] -> image[40][1];
image[30][2] -> image[40][2];



  종, 횡 동시에 이동을 한다 그랬을 때는, 만약 오른쪽으로 10px, 아래로 20px이라고 하면 image[n+20][m+10] 으로 해주면 되겠죠. 크게 어려운건 없습니다. 그럼 실제 실습을 해보도록 하겠습니다.



  샘플이미지를 가로방향으로 +50, 세로방향으로 +30 픽셀만큼 옮겨보도록 하겠습니다. 이동 뒤에 보이게 되는 배경색은 검정색으로 하고, 기존에 해왔던 방식대로 결과 파일(.pgm)을 생성해서 결과를 확인해 보도록 하겠습니다. 

  먼저 새롭게 추가한 함수의 소스 코드 입니다.

int fnMoveImg(PGMImage *img_org, PGMImage *img_mov, int xd, int yd)
{
	int new_x;
	int new_y;


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

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



	// <-- 메모리 할당 - 원본이미지와 동일한 크기로
	img_mov->pixels = (unsigned char**)calloc(img_org->height, sizeof(unsigned char*));

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


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



	// <-- xd(가로), yd(세로) 만큼 좌표를 이동시킴
	for(int i=0; i<img_org->height; i++){
		new_y = i + yd;  

		// y - 유효한 좌표값인지 체크
		if(new_y < 0 || new_y >= img_org->height){
			continue;
		}

		for(int j=0; j<img_org->width; j++){
			new_x = j + xd;

			// x - 유효한 좌표값인지 체크
			if(new_x < 0 || new_x >= img_org->width){
				continue;
			}

			img_mov->pixels[new_y][new_x] = img_org->pixels[i][j];
		}
	}
	// -->


	return TRUE;
}


 이미지의 이동을 위한 함수로, 전체 소스는 크게 바뀐 부분이 없습니다. 참고로 전체 소스코드 파일은 포스트 하단에 첨부해 놓았습니다.


  함수에서 전달받는 인자는

  • img_org : 원본 이미지
  • img_mov : 이동 후 결과 이미지(출력변수)
  • xd : 가로방향으로 이동시킬 px 크기
  • yd : 세로방향으로 이동시킬 px 크기

  입니다.

  원본 이미지 파일을 읽어서 그 파일 내에서 이동처리를 하는것이 아니라, 원본을 읽어서 이동시킨 결과 이미지를 새롭게 만든다고 생각하시면 됩니다. 따라서 원본 이미지의 내용은 img_org 변수에 담겨있고, img_mov 라는 변수에는 이동 한 후의 결과 이미지가 저장됩니다.
  xd와 yd는 각각 가로방향, 세로방향으로 얼마만큼(픽셀단위)을 이동시킬지를 넘겨줍니다. 양의 값(+)을 넘겨주면 가로방향에서는 오른쪽으로, 세로방향에서는 아래쪽으로 이동되며, 음의 값(-)을 넘겨주게 되면 그 반대방향으로 이동하게 됩니다.

  지역변수로 선언된 new_x, new_y는 원본의 어떤 픽셀의 좌표를 xd, yd만큼 옮긴다고 했을 때 새롭게 계산되어진 좌표의 값(이동 후의 좌표 값)을 담아두기 위해서 사용됩니다.

  이동 후에 공백으로 남게 될 영역의 색상을 지정하기 위한 로직과 픽셀의 이동 후의 좌표가 전체 이미지 영역을 벗어나는지를 체크하는 로직이 들어가 있습니다. 이 로직이 없다면 당초 할당해놓은 메모리 영역을 넘어선 접근을 하게 되므로, 반드시 체크해줘야 합니다. 천천히 훑어보면 역시 그다지 복잡한 로직은 없습니다.


  다음은 main 함수입니다. 

int main(int argc, char** argv)
{
	PGMImage	img_org;
	PGMImage	img_mov;

	
	if(argc != 2){
		fprintf(stderr, "사용법 : %s <filename>\n", argv[0]);
		return -1;
	}


	if(fnReadPGM(argv[1], &img_org) != TRUE){
		return -1;
	}

	if(fnMoveImg(&img_org, &img_mov, 50, 30) != TRUE){
		return -1;
	}


	// <-- 결과 확인은 파일로 대체
	if(fnWritePGM("E:\\test\\result.pgm", &img_mov) == TRUE){
		printf("파일 저장완료!\n");
	}
	// -->

	fnClosePGM(&img_org);
	fnClosePGM(&img_mov);

	return 0;
}



  사실 main 함수는 크게 변한 부분은 없지만 조금이라도 이해를 돕기 위해 추가했습니다. 기존에는 원본 이미지 하나만을 참조해서 모든 처리를 했기 때문에 PGMImage 타입의 변수가 하나만 존재했지만, 지금부터는 원본과 이동시킨 결과 이미지를 변수에 담기 위해서 2개의 PGMImage 변수를 선언했습니다. 
  여기서는 그냥 큰 흐름만 보시면 됩니다.

  1. 원본 이미지 파일을 읽어들인다.    ☞  fnReadPGM();
  2. 원하는 픽셀만큼 이동시킨다.        ☞  fnMoveImg();
  3. 이동시킨 결과를 파일로 기록한다. ☞  fnWritePGM();

  이런 순서로 처리가 된다는 것만 알아두시면 되겠습니다. 앞으로 미러, 플립등의 처리도 큰 흐름은 모두 같은 방식으로 진행이 될 것입니다.


  그럼 결과를 확인해 보겠습니다. 아래 이미지는 이동(move) 처리의 실행 결과입니다.



  설명을 길게하기는 했는데 간단하게 생각하면 참 간단합니다. 그저 배열의 첨자값만 원하는 만큼(이동시키려는 만큼) 변경해주면 끝나는 것이지요. 미러와 플립도 비슷합니다.


  미러(Mirror)는 영상의 좌우를 반전시키는 것 입니다. 마치 거울처럼 좌우가 바뀐다고 해서 미러라고 합니다. 10 x 10 픽셀의 이미지라고 가정한다면,

imgae[0][0] -> image[0][9];
image[0][1] -> image[0][8];
image[0][2] -> image[0][7];
   ...
image[0][7] -> image[0][2];
image[0][8] -> image[0][1];
image[0][9] -> image[0][0];


  이런식으로 y축의 좌표는 변경없이 x축의 좌표만 변하게 됩니다. 소스코드는 아래와 같습니다.

  

int fnMirrorImg(PGMImage *img_org, PGMImage *img_mir)
{
	int new_x;


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

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



	// <-- 메모리 할당 - 원본이미지와 동일한 크기로
	img_mir->pixels = (unsigned char**)calloc(img_org->height, sizeof(unsigned char*));

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



	// <-- 미러(좌우반전) 효과
	for(int i=0; i<img_org->height; i++){
		for(int j=0; j<img_org->width; j++){
		    new_x = img_org->width - j - 1; // 배열은 0부터 시작이므로 1을 빼줌

		    img_mir->pixels[i][new_x] = img_org->pixels[i][j];
		}
	}
	// -->


	return TRUE;
}



  큰 흐름은 이동과 같습니다. 오히려 좌표를 이동시켰을 때, 이동 후의 결과가 전체 영역을 벗어나는지를 체크하는 로직이 필요없기 때문에 소스는 좀 더 간단합니다.

  저 중 눈여겨 볼 부분은 이중 for loop의 안에 있는 2줄 입니다. 이동(move)과는 달리 미러는 y축을 기준으로 x의 좌표만을 변경하기 때문에 지역변수는 new_x 하나만 필요합니다.

  -1을 추가로 빼주는 이유는 배열의 시작이 1이 아니라 0이기 때문입니다. 10px 크기 이미지의 제일 우측 픽셀의 좌표는 10이 아니라 9가 됩니다. 따라서 제일 오른쪽에 있는 픽셀은 넓이(10 px)에서 해당 픽셀의 x 좌표값 9를 뺀 후 -1을 추가로 빼줘야 원하는 값인 0이 됩니다.

  위 프로그램의 실행 결과는 아래와 같습니다.




  원하는대로 좌우가 반전되었습니다.


  바로 이어서 플립(flip)도 살펴보겠습니다. 미러가 y축을 기준으로 해서 좌우를 반전시킨 거라면, 플립은 반대로 x축을 기준으로 해서 상하를 반전시킨 것 입니다.

// <-- 플립(상하반전) 효과
for(int i=0; i<img_org->height; i++){
	new_y = img_org->height - i - 1;	// 배열은 0부터 시작이므로 1을 빼줌

	for(int j=0; j<img_org->width; j++){
		img_flp->pixels[new_y][j] = img_org->pixels[i][j];
	}
}
// -->


  나머지 부분은 모두 미러와 동일합니다. 미러와 크게 다른 부분이 없으므로 설명은 생략하고 결과만 확인해보도록 하겠습니다.




  상하로 반전된 이미지가 출력되었습니다. 한가지 주의할 점은 원본 사진을 180º 회전시켰을 때 얻어지는 이미지와는  다르다는 점 입니다. 플립은 말 그대로 x축을 기준으로 반바퀴 돌린 영상이 되는 것 입니다. 글로써 설명하려니 약간 제약이 있네요. 애니메이션 관련 툴을 다룰 줄 알면.. 그렇게라도 해서 설명드릴텐데.. 저도 답답하군요..ㅋㅋ



  오늘은 영상의 위치에 기반한 처리 중 이동, 미러, 플립에 대해서 알아봤습니다. 설명을 제대로 했나 모르겠네요.. 개인적으로 좀 복잡한 일이 있어서 집중해서 강좌글을 쓰기가 좀 힘드네요.. 그래도 뭐 기운내서 계속 이어나가야죠~ ^^/
  다음 강의에서는 '위치에 기반한 처리' 그 두 번째 시간으로 크롭(Crop, 특정 영역 잘라내기), 축소확대에 대해서 알아보도록 하겠습니다. 
  

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

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



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

move.cpp

mirror.cpp

flip.cpp



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


Comments