개발자 블로그
영상처리 강좌 - 7. 이미지 내 마음대로 움직여보자(2) : 크롭(Crop), 축소, 확대 본문
안녕하세요.
오늘 배워볼 내용은 이미지를 내 마음대로 조작하는 처리에 대해서 입니다. 이전시간에 배운 내용은 픽셀의 이동 및 반전에 관한 내용이었고, 오늘부터 강의할 내용은 이미지 내 마음대로 움직이기 그 두번째! 바로 크롭(Crop), 축소, 확대에 대해서 알아보겠습니다.
☞ 2015/08/10 - [영상처리강좌] - 영상처리 강좌를 시작합니다~!!
먼저 크롭(Crop)이란 무엇인가에 대해서 먼저 알아보겠습니다.
① 사진을 편집할 때 원하는 크기에 맞도록 그 사이즈를 트리밍(trimming)하는 것. 흔히 크로핑(cropping)한다고 말한다. 이러한 크롭을 할 때 자른 부분에 금(線)을 긋거나 표시를 한 것을 크롭 마크(crop mark)라고 한다. 그리고 잘려나가는 부분은 크롭트(cropped)라고 한다.
출처 : 네이버 지식사전
뭐.. 사전에 나온 내용이 더 어렵군요..-_-;; 그냥 간단히 얘기하면 특정 부위를 뜯어내는 것을 말합니다. 포토샵에서도 쓰이고 음악재생프로그램의 리스트 메뉴에서도 찾아볼 수 있습니다.
영상에서 예를 들면, 인물사진에서 얼굴부분만 따로 뜯어낸다거나 하는 처리를 크롭이라고 합니다. 아,, 크롭을 설명하기 전에 복사를 먼저 설명하고 진행을 해야되겠군요. 복사(1:1로 복사하는 것을 말합니다.)는 정말 간단합니다. 아래 5 x 5 크기의 이미지가 있다고 했을 때, 동일한 크기의 5 x 5 크기의 배열을 준비해서 값을 그대로 copy 해주면 됩니다.
왼쪽의 배열을 A, 오른쪽의 배열을 B라고 한다면
for(int i=0; i<A.height; i++){
for(int j=0; j<A.width; j++){
B[i][j] = A[i][j];
}
}
이런식으로 표현될 것 입니다. 간단하죠. 여기서 포인트는 배열의 첨자로 사용된 인덱스 변수 입니다. copy에서는 동일한 변수를 사용할 수가 있죠.
그렇다면 크롭은? 일단 원본과 크롭처리 후 생기는 사본의 크기가 다를 것 입니다. 따라서 두 배열간의 인덱스 변수는 서로 다른 값을 갖기 때문에 따로 선언을 해줘야 합니다.
아래 이미지의 빨간 부분만을 크롭한다면, 사본의 크기는 3 x 3 크기가 될 것이고, 빨간색으로 표시된 영역의 값들만 copy 해주면 됩니다.
배열 A에서는 모든 픽셀에 접근할 필요없이, 빨간색 부분에만 접근합니다.
y_start = 1;
y_end = 3;
x_start = 1;
x_end = 3;
n = 0;
for(int i=y_start; i<=y_end; i++){
m = 0;
for(int j=x_start; j<=x_end; j++){
B[n][m] = A.[i][j];
m++;
}
n++;
}
원본 이미지의 빨간색 원들 중 제일 좌측상단에 있는 곳의 좌표는 (1, 1)입니다. 그런데 그 픽셀을 담기위한 배열 B의 좌표는 (0, 0)이 되죠. 이게 인덱스 변수가 추가적으로 필요한 이유입니다.
원본 이미지에서 빨간색 영역에만 접근하도록 하는 처리는 for문에서 제어하고 있습니다.
여기까지 기본적인 개념에 대한 소개였습니다. 중요 부분에 대한 소스도 유사코드로 표현해봤으니 여러분들께서 직접한번 구현해 보신 후 제가 작성한 소스와 비교해 보시는 것도 좋겠네요.
일단 먼저 결과를 확인해 보도록 하겠습니다.
( 클릭하시면 큰 화면으로 볼 수 있습니다. )
왼쪽사진이 원본, 오른쪽 사진이 결과영상으로, 원본의 노란색 영역을 크롭한 결과입니다. 이처럼 원본사진에서 원하는 부분만 추출하는 것이 가능합니다. 그럼 소스를 보죠.
// 인자로 넘어온 시작점과 끝점을 포함한 영역을 크롭 int fnCropImg(PGMImage *img_org, PGMImage *img_crp, int ys, int ye, int xs, int xe) { int new_height = ye - ys + 1; int new_width = xe - xs + 1; int y = 0; int x = 0; // 헤더값 복사 img_crp->M = img_org->M; img_crp->N = img_org->N; img_crp->width = new_width; img_crp->height = new_height; img_crp->max = img_org->max; // <-- 메모리 할당 - 크롭영역의 크기만큼 img_crp->pixels = (unsigned char**)calloc(new_height, sizeof(unsigned char*)); for(int i=0; i<new_height; i++){ img_crp->pixels[i] = (unsigned char*)calloc(new_width, sizeof(unsigned char)); } // --> // <-- 크롭 for(int i=ys; i<=ye; i++){ x = 0; for(int j=xs; j<=xe; j++){ img_crp->pixels[y][x] = img_org->pixels[i][j]; x++; } y++; } // --> return TRUE; }
크롭을 위해 추가된 함수입니다. 프로그램 전체 소스는 하단에 따로 첨부해 두었습니다. 참고하시고요~
크롭영역에 대한 좌표를 인자로 받습니다. 각각의 변수는
- ys : y좌표의 시작점, 크롭영역 상단의 y좌표 값
- ye : y좌표의 끝점, 크롭영역 하단의 y좌표 값
- xs : x좌표의 시작점, 크롭영역 좌측의 x좌표 값
- xe : x좌표의 끝점, 크롭영역 우측의 x좌표 값
입니다.
또, 크롭 후 새로 생성되는 이미지의 높이와 넓이를 위한 변수가 추가 되었습니다. .pgm 파일의 헤더값 셋팅과, 메모리 할당도 원본과 같은 사이즈가 아닌 새로 계산된 높이와 넓이만큼 할당합니다. 소스에서 굵게 강조한 부분을 눈여겨 보시면 될 것 입니다.
자~ 그럼 다음은 축소와 확대에 대해 알아보도록 하겠습니다. 사실 축소나 확대는 이동과 거의 유사합니다. 픽셀의 입장이 되서 이동, 축소, 확대를 구분해보면
- 이동 : 픽셀좌표 + n (n은 0 ~ 이미지넓이 사이 값)
- 축소 : 픽셀좌표 * n (n은 0 ~ 1 사이 값)
- 확대 : 픽셀좌표 * n (n은 1 이상의 값)
으로 나눠 볼 수 있습니다.
이동에 대해 공부할 때 같이 설명하지 않고 뒤로 따로 뺀 이유는 '보간법'에 대한 설명이 필요하기 때문입니다. 보간법에 대한 설명은 다음 강좌에서 '회전'에 대해서 설명할 때 까지 미뤄두도록 하겠습니다.
축소와 확대는 위의 공식에서 알 수 있듯이 기본적인 코드는 같습니다. n값에 따라서 축소 또는 확대가 되는 것이죠.
그럼 위에서 크롭한 결과 영상을 가지고 테스트 해보도록 하겠습니다. (우리가 결과로 저장한 이미지는 아스키타입의 PGM이미지이므로, XnView를 이용해서 다시 PGM 파일(헥사)로 저장해주셔야 합니다.)
먼저 소스를 보겠습니다.
int fnResizeImg(PGMImage *img_org, PGMImage *img_new, double n) { int new_height = img_org->height * n; int new_width = img_org->width * n; int new_x; int new_y; // 헤더값 복사 img_new->M = img_org->M; img_new->N = img_org->N; img_new->width = new_width; img_new->height = new_height; img_new->max = img_org->max; // <-- 메모리 할당 - 새로 생성될 이미지의 크기에 따라 img_new->pixels = (unsigned char**)calloc(new_height, sizeof(unsigned char*)); for(int i=0; i<new_height; i++){ img_new->pixels[i] = (unsigned char*)calloc(new_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++){ new_y = ceil(i * n); if(new_y >= img_new->height){ new_y = img_new->height - 1; } for(int j=0; j<img_org->width; j++){ new_x = ceil(j * n); if(new_x >= img_new->width){ new_x = img_new->width - 1; } img_new->pixels[new_y][new_x] = img_org->pixels[i][j]; } } // --> return TRUE; }
함수 전체를 붙여넣다보니 소스가 좀 길어졌네요. 굵게 강조한 소스가 핵심부분 입니다. 확대를 한다고 가정하고 설명하겠습니다.
- 확대 비율로 계산 된 결과영상의 width, height를 계산합니다.
- 해당 크기만큼 새로운 이미지에 대한 메모리를 할당합니다.
- 원본이미지의 좌료를 비율만큼 계산(곱셈)해서 새로운 좌료를 구합니다.
- 해당위치에 원본 픽셀의 값을 copy
이렇게 나눠볼 수 있겠네요. 3번에 대해서 좀 더 설명을 하자면, 이미지를 2배로 확대한다고 했을 때, 좌표 (2, 10)에 위치한 픽셀은 확대 후 (4, 20)의 좌표값을 갖게 될 것 입니다. 축소도 마찬가지 입니다. 좌표 (2, 10)에 위치한 픽셀을 1/2로 축소한다고 하면(이 경우 n값은 0.5) 축소 후 해당 픽셀의 좌표는 (1, 5)가 됩니다.
배경색을 설정하는 코드는 축소의 경우에는 필요없지만 확대의 경우에는 필요합니다. 그 이유는 이미지를 확대하게 되면 아래 이미지처럼 빈 공간이 생기기 때문입니다.
( 이미지를 2배 확대한 경우에 생기는 현상 )
우리가 포토샵등의 이미지 편집프로그램을 이용해서 확대를 하게 되면 저런 빈 공간들이 없는데 그건 왜 일까요? 그건 저러한 빈 공간을 메꾸는 알고리즘에 따라 임의의 픽셀값으로 저 공간들을 메꾸기 때문입니다. 이 '빈 공간을 메꾸는 것'을 '보간법'이라고 합니다. 일단은 보간법이라는게 있고, 그것이 왜 필요한지에 대해서만 알아두시면 되겠습니다.
그럼 실행 결과를 확인해 보겠습니다. 먼저 축소입니다. 원본이미지를 1/2로 축소한 영상입니다.
( 클릭하시면 큰 화면으로 볼 수 있습니다. )
다음, 확대 결과 영상입니다. 원본이미지를 1.5배로 확대한 영상입니다.
( 클릭하시면 큰 화면으로 볼 수 있습니다. )
위에서 얘기한 것 처럼 확대영상에는 중간에 빈 공간이 보이실 것 입니다. 이 문제가 개선 된 확대는 다음 강의에서 보간법에 대해 배우고 난 뒤에 다시 보여드리도록 하겠습니다.
자~ 오늘은 크롭, 축소, 확대에 대해서 알아봤습니다. 지금까지 강좌를 모두 잘 따라오신 분이라면 오늘 내용은 수월하게 느껴지셨을 것 같네요. 아무래도 내용이나 흐름이 계속 이어지는 부분이 많아서 처음에 개념에 대한 이해만 된다면 그 후로는 크게 어렵지는 않습니다.
다음 강의에서는 영상의 회전과 보간법에 대해서 알아보도록 하겠습니다. 다소 어려운 부분일 수도 있는데 최대한 쉽게 한번 설명해보도록 노력하겠습니다~
혹시 궁금한 부분이나 잘못된 부분은 댓글로 남겨주세요~ 응원의 메시지도 환영입니다~ㅋ
부족한 강의 봐주셔서 대단히 감사합니다.
'영상처리강좌' 카테고리의 다른 글
영상처리 강좌 - 9. PPM파일을 읽어보자 (21) | 2016.01.04 |
---|---|
영상처리 강좌 - 8. 이미지 내 마음대로 움직여보자(3) : 회전(rotation), 보간법(interpolation) (5) | 2016.01.04 |
영상처리 강좌 - 6. 이미지 내 마음대로 움직여보자(1) : 이동(Move), 미러(Mirror), 플립(Flip) (6) | 2016.01.03 |
영상처리 강좌 - 5. 이미지 밝기를 조절해보자! (0) | 2016.01.03 |
영상처리 강좌 - 쉬어가기 #1. 당신은 레나(Lenna)에 대해서 아시나요? (1) | 2016.01.03 |