개발자 블로그

영상처리 강좌 - 3. PBM 파일을 읽어보자! 본문

영상처리강좌

영상처리 강좌 - 3. PBM 파일을 읽어보자!

로이드.Roid 2015. 11. 1. 17:31

  안녕하세요~
  영상처리 강좌 세 번째 시간입니다~!

  오늘은 PBM 파일을 실제 프로그램에서 읽어들이는 방법에 대해서 설명드리겠습니다. PBM 파일과 PGM 파일을 읽는 법은 큰 차이가 없으므로, PBM 파일 읽는 법을 알고나면 PGM 파일 역시 마음껏 프로그램을 통해서 읽어들일 수 있을 것입니다. 강의는 PBM 파일을 중심으로 설명하고, 강의 마지막 부분에서 PGM 파일을 읽을 때 어떤 점이 달라지는지에 대해서만 설명 드리겠습니다.


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



  본 실습에 사용 된 전체 소스코드는 본 강의의 하단에 첨부해 두었습니다. 바로 다운받아서 사용하기 보다는 본 강의를 먼저 보신 후 스스로 코드를 작성하시고, 제가 작성한 코드는 그냥 참고용으로만 보시기를 권해드립니다.

  또 소스 상단의 pragma문은 warning을 제거하기 위해 추가한 코드입니다. warning을 무시하는 것은 잘못 된 것이지만 여러분들에게 좀 더 친숙한 형태의 코드(함수)를 제공하기 위한 선택입니다. 역시 같은 이유로 문자셋도 유니코드가 아닌 멀티바이트스트링을 이용하고 있습니다.




  PBM 파일의 구조에 대해서는 지난번 강의에서 설명드렸습니다. 오늘은 실제 PBM 파일을 읽어드리는 프로그램을 작성 해 보겠습니다.
  우선 PBM 파일의 데이타를 담기위한 구조체를 정의합니다. 
typedef struct {	
    char	           M, N;		// 매직넘버	
    int		width;	
    int		height;	
    unsigned char** pixels;
} PBMImage;


  구조체에는 매직넘버 저장을 위한 변수, width, height, 픽셀값을 담기 위한 '포인터의 포인터' 변수가 있습니다. 포인터 개념이 아직 확실하게 잡히지 않았다면 다소 어렵게 느껴질 수 있는 부분인데요. 인터넷에 이와 관련된 내용이 많이 있으니 참조하시기 바랍니다.


    이 강의에서 다루고 있는 PBM 파일은 아스키(ASCII) 코드로 작성되어있기 때문에 보통의 텍스트 파일을 읽는 것과 다를 것이 없습니다. 다른 점 이라면 파일의 내용을 담는 그릇(변수)이 조금 다를 뿐이죠. 그럼 주요 소스코드를 살펴 보겠습니다. 
int fnReadPBM(char* fileNm, PBMImage* img);
void fnClosePBM(PBMImage* img);


  먼저 사용자 정의 함수 두 개를 정의 했습니다. fnReadPBM() 함수는 PBM 이미지 파일의 파일명과 위에서 설명한 구조체 변수의 포인터를 인자로 전달 받습니다. 사실 이 함수가 이 강의의 전부라고 할 수 있습니다. 
  fnClosePBM() 함수는 fnReadPBM() 함수에서 동적으로 할당한 메모리를 해제해주는 함수입니다. free() 함수 호출이 코드의 전부 입니다.

  그럼 fnReadPBM() 함수를 먼저 자세히 들여다 보겠습니다. 코드의 길이를 짧게 하기 위해서 전달받은 인자나 파일 개방 성공여부 체크로직은 넣지 않았습니다. 첨부한 전체 소스코드 파일에는 체크로직이 포함되어 있으며, 전달받은 인자, 유효한 PBM 파일인지, 파일 개방은 정상적으로 되었는지 등을 체크 합니다. 
int fnReadPBM(char* fileNm, PBMImage* img)
{
	FILE* fp = fopen(fileNm, "r");
	fscanf(fp, "%c %c", &img->M    , &img->N     );	// 매직넘버 읽기
	fscanf(fp, "%d %d", &img->width, &img->height);	// 가로, 세로 읽기

	// <-- 메모리 할당
	img->pixels = (unsigned char**)calloc(img->height, sizeof(unsigned char*));

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

	// <-- pbm 파일로부터 픽셀값을 읽어서 할당한 메모리에 load
	int tmp;
	for(int i=0; i<img->height; i++){
		for(int j=0; j<img->width; j++){
			fscanf(fp, "%d", &tmp);
			img->pixels[i][j] = (unsigned char)tmp;
		}
	}
	// -->

	fclose(fp);	// 더 이상 사용하지 않는 파일을 닫아 줌

	return TRUE;
}


  큰 흐름은 아래와 같습니다.
  1. 파일 개방
  2. 매직넘버 읽기
  3. 가로(width), 세로(height) 읽기
  4. 가로 x 세로만큼 메모리 할당
  5. 파일로부터 픽셀값 읽어오기
  6. 파일 닫기

  우리에게 친숙한 C언어이고 생소한 함수도 없습니다. 파일관련한 부분은 C언어의 파일 입출력을 학습하면서 다 배운부분일 것 입니다. 핵심은 ① 이중 포인터에 대한 부분과 ② '이미지의 픽셀정보는 실제 프로그램에서 어떤 식으로 관리(변수타입, 접근방법 등) 되는가?' 입니다.

  이미지는 기본적으로 2차원 배열의 형태를 갖습니다. 한 픽셀은 unsigned char로 표현 되며(PGM, PPM 모두 마찬가지 입니다.), 한 행을 담기 위해서는 sizeof(unsigned char) * 가로 ] 만큼의 메모리가 필요합니다. 이를 확장해서 전체 픽셀정보를 표현하려면 세로(height)의 크기만큼 반복해서 메모리를 할당해야 겠지요. 
  이러한 이유로 인해서 이중 포인터(or 포인터의 포인터) 타입인 unsigned char** 변수가 필요합니다.







  위에서 말로 설명한 개념을 그림으로 표현한 것 입니다. 이중 포인터에 대한 개념만 알면 어렵지 않습니다.
  주황색으로 표시한 ⓐ 부분이 아래 소스코드에서 첫 번째 calloc 부분이고, 하늘색으로 표시한 ⓑ 부분이 아래 소스코드에서 루프를 돌면서 calloc을 호출하는 부분입니다.
img->pixels = (unsigned char**)calloc(img->height, sizeof(unsigned char*));

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

  첫 번째 메모리 할당에서 이미지의 높이(height)만큼 메모리 할당을 했고, 할당된 각 포인터 변수에는 이미지의 너비(width) 만큼 다시 메모리를 할당했습니다. 첫 번째 할당은 각 행들을 가르키는 포인터를 담기 위한 할당이고, 두 번째 할당은 한 행에 해당하는 픽셀들의 정보들을 담아두기 위한 할당입니다. 
  실제로 실습을 하면서 개념을 확실하게 이해하시기 바랍니다.


  이미지 파일의 모든 픽셀값이 2차원 배열 변수에 로드가 완료되면, 배열 첨자를 이용해서 특정 픽셀에 쉽게 접근 가능합니다. 예를 들어 2차원 배열 변수명이 array라고 했을 때, 좌표 (3, 4)에 접근하기 위해서는 array[3][4]로 접근하면 됩니다. 컬러영상인 PPM은 약간 다르지만 PBM, PGM 파일은 모두 이런식으로 쉽게 픽셀값에 접근 할 수 있습니다.



  이미지의 픽셀정보는 이차원 배열에 저장한다고 하였지만, 1차원 배열로도 표현이 가능합니다.
  이 경우 특정 픽셀에 대한 접근은 계산을 통해서 원하는 좌표에 해당되는 첨자를 얻어내야 합니다.
  예를 들어 설명하면, 좌표 (3, 4)에 접근하는 코드는 array[ ( 3 * width ) + 4 ]; 입니다. 우리가 테스트하는 영상의 width는 5 이므로, 좌표 (3, 4)에 해당되는 실제 배열 첨자는 19가 됩니다.
  특정 픽셀에 대한 접근은 2차원 배열을 이용한 방법보다 약간 복잡하게 느껴집니다. 하지만 메모리 공간 할당이나 전체 픽셀에 대한 접근 같은 처리는 이중 for문 대신 하나의 루프안에서 처리가 가능하기 때문에 2차원 배열에 비해 좀 더 간단하다는 장점이 있죠.
  개인적으로는 2차원 배열을 선호하지만, 라이브러리에 따라서 1차원 배열을 이용한 접근만 지원하는 경우도 있으므로 개념정도는 알고있어야 합니다.



   동적으로 할당된 메모리는 free를 해줘야 하며, 이 역할은 fnClosePBM() 함수에서 담당하고 있습니다. 메모리 할당이 이중으로 되었기 때문에 해제 또한 두 번의 과정을 거치게 되며, 순서는 할당한 순서의 역순 입니다. 
void fnClosePBM(PBMImage* img){
	for(int i=0; i<img->height; i++){
		free(img->pixels[i]);
	}

	free(img->pixels);
}

  PBM 이미지를 제대로 읽었는지는 콘솔에 문자를 찍어서 확인해 보도록 하겠습니다. 소스코드는 아래와 같습니다.
//  결과 확인을 위해서 읽은값을 콘솔에 출력함
for(int i=0; i<img.height; i++){
	for(int j=0; j<img.width; j++){
		if(img.pixels[i][j] == 1){
			printf("■");
		}
		else{
			printf("□");
		}
	}

	printf("\n");
}


  그럼 이전 강의에서 만들어본 PBM 이미지로 테스트를 해 보도록 하겠습니다. 아래는 이전 강의에서 만들었던 PBM 파일을 이미지 뷰어로 확인한 화면입니다.





  아래는 테스트 결과 화면입니다.




  이렇게 프로그램을 통해서 PBM 파일의 픽셀정보를 정상적으로 읽어들였다는게 확인이 되었습니다.



  PGM 이미지 역시 PBM 이미지와 유사하므로 전체픽셀에 대한 값을 콘솔에 출력해보고 마무리 하겠습니다. PGM 파일헤더명암도의 최대값만 들어간다는 점을 제외하고는 PBM과 동일합니다. 소스 코드는 구조체에 최대 명암도 값을 읽기위한 max 변수가 추가 되었고, 이 값을 읽어들이기 위한 코드가 추가 되었습니다. 
typedef struct {
	char	M, N;		// 매직넘버
	int		width;
	int		height;
	int	     max;
	unsigned char **pixels;
} PGMImage;
▲ PGM 파일을 읽기 위한 구조체

fscanf(fp, "%d", &img->max);	// 최대명암도 값
▲ width와 height 정보를 읽은 다음, 최대명암도 값을 읽는 코드를 추가합니다.


  아래는 지난 번 강의에서 제가 만들었던 PGM 이미지와 테스트 결과 화면입니다. PGM 파일을 텍스트 에디터로 열어서 값을 비교해 보시면 서로 일치한다는 것이 확인 가능합니다.








크게 어렵지 않으셨죠?? 
혹시 궁금한 부분이나 잘못된 부분은 댓글로 남겨주세요~ 
응원의 메시지도 환영입니다~ㅋ
그리고 부족한 강의 봐주셔서 대단히 감사합니다.


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


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


Comments