개발자 블로그

영상처리 강좌 - 9. PPM파일을 읽어보자 본문

영상처리강좌

영상처리 강좌 - 9. PPM파일을 읽어보자

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


  안녕하세요~

  영상처리 강좌 아홉 번째 시간입니다~!

  오늘 배워볼 내용은 PPM파일을 읽는 방법에 대한 것 입니다. 그 동안은 흑백사진만 가지고 해서 좀 아쉬운 부분도 있었는데, 오늘부터는 드디어 컬러영상을 가지고 배워보도록 하겠습니다. 자~ 그럼 PPM 파일은 어떻게 생겼는지 먼저 살펴보도록 하겠습니다.


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


  PPM이란 Portable PixMap의 약자로 PGM, PBM과는 달리 컬러영상을 표현할 수 있습니다. 이 파일포멧에 대한 설명은 간단하게 나마 첫 번째 강의에서 언급을 했었고, 또 위키피디아에 굉장히 설명이 잘 되어있어서 그 링크로 대체하도록 하겠습니다.

위키피디아 NetPbm format : http://en.wikipedia.org/wiki/Netpbm_format


  영문으로 되어있지만 이해하시는데 큰 어려움은 없을 것 입니다. 전체 내용을 다 읽어볼 필요는 없고, 그냥 각각 파일포멧의 매직넘버나 파일이 실제 어떻게 생겼는지 정도만 살펴보시면 됩니다. 

  PGM 파일포멧과 비교해서 살펴보자면

3 x 2px 크기의 PGM 이미지의 경우에는

P2
3 2
255
 0150255 
 25550 
0
 



< 결과영상 >


이러한 식으로 파일이 구성되어있을 것 입니다. 하나의 값이 하나의 픽셀에 매칭되는 형태이죠.


  하지만 PPM 이미지의 경우에는 

P3
3 2
255
 0      0      0
255   0   0 
0       255   0 
 255  255  0
255   0   255
255   255   255 
 



< 결과영상 >


이렇게 3개의 값이 한개의 픽셀을 표현하도록 구성이 됩니다. 즉, 하나의 픽셀을 표현하기 위해서는 R(Red), G(Green), B(Blue) 이렇게 세 개의 값을 필요로 한다는 것 입니다.



  PGM 파일보다는 좀 까다롭지만 그렇다고 뭐 크게 달라질 부분은 없습니다. 먼저 테스트 해볼 PPM 파일이 필요하겠죠? PGM 파일 만드는 것과 똑같지만 시간이 오래 지났으니 다시한번 알려드리겠습니다.

  PPM 파일을 인턴넷에서 그냥 구하기는 힘들고 JPG나 GIF, BMP, PNG 같은 파일을 구해서 PPM 파일로 변환해주시면 됩니다. PPM 파일로 변환은 우리가 이미지 뷰어로 사용하는 XnView를 이용하면 쉽게 변환할 수 있습니다.


XnView를 다운 받으시려면, 여기를 클릭하세요. 다운로드 페이지로 이동합니다. (새창)


  먼저 이미지 파일을 XnView로 열어서 [파일] -> [다른 이름으로 저장] 을 클릭하신 후 [그림 저장] 대화상자가 뜨면 파일 형식에서 PPM - Portable Pixmap 을 선택하신 후 저장하면 됩니다. 매우 간단하므로 이미지는 따로 첨부하지 않도록 하겠습니다.



< 오늘 실습을 위한 샘플영상입니다. 지금까지 흑백으로만 보다가 컬러로 보니 눈이 부시네요.. ㅋㅋ >


  우리가 만든 프로그램에서 PPM 파일을 제대로 읽었는지를 확인하기 위해서 바이너리 타입의 PPM 파일을 읽어서 아스키 타입의 PPM 파일로 저장을 해보도록 하겠습니다.

  전체 소스는 포스트 하단에 따로 첨부해 두었습니다. 여기서는 PPM 파일을 읽어들이는 부분, PPM 파일로 다시 쓰는 부분 이렇게 두 부분만 살펴보도록 하겠습니다. (나머지 부분은 변경된 부분이 거의 없습니다.)


  먼저 PPM 파일을 읽어들이는 함수입니다.

int fnReadPPM(char* fileNm, PPMImage* img)
{
	FILE* fp;

	if(fileNm == NULL){
		fprintf(stderr, "fnReadPPM 호출 에러\n");
		return FALSE;
	}
	
	fp = fopen(fileNm, "rb");	// binary mode
	if(fp == NULL){
		fprintf(stderr, "파일을 열 수 없습니다 : %s\n", fileNm);
		return FALSE;
	}

	fscanf(fp, "%c%c\n", &img->M, &img->N);	// 매직넘버 읽기

	if(img->M != 'P' || img->N != '6'){
		fprintf(stderr, "PPM 이미지 포멧이 아닙니다 : %c%c\n", img->M, img->N);
		return FALSE;
	}

	fscanf(fp, "%d %d\n", &img->width, &img->height);	// 가로, 세로 읽기
	fscanf(fp, "%d\n"   , &img->max                );	// 최대명암도 값

	if(img->max != 255){
		fprintf(stderr, "올바른 이미지 포멧이 아닙니다.\n");
		return FALSE;
	}


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

	for(int i=0; i<img->height; i++){
	   // 1개의 픽셀을 위해 R, G, B 3byte가 필요
	   img->pixels[i] = (unsigned char*)calloc(img->width * 3, sizeof(unsigned char));
	}
	// -->


	// <-- ppm 파일로부터 픽셀값을 읽어서 할당한 메모리에 load
	for(int i=0; i<img->height; i++){
		for(int j=0; j<img->width * 3; j++){
			fread(&img->pixels[i][j], sizeof(unsigned char), 1, fp);
		}
	}
	// -->


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

	return TRUE;
}

  기존 PGM 파일을 읽는 소스에서 변경된 부분은 매직넘버를 체크하는 부분에서 기존 'P5' 대신 'P6'으로 체크해주고 있습니다. 그 다음 메모리 할당할 때, 한 픽셀당 R G B 이렇게 3byte가 필요하기 때문에 이미지의 넓이(width)에 3을 곱해주고 있습니다. 파일에서 fread()로 픽셀값을 읽는 부분도 마찬가지로 3을 곱해주고 있고요.



  다음 읽어들인 PPM 파일을 다시 PPM 파일로 기록하는 함수입니다. 참고로 읽어들이는 대상 PPM 파일은 바이너리 타입이고, 읽어들인 이미지를 기록할 때의 PPM 파일은 아스키 타입입니다.

int fnWritePPM(char* fileNm, PPMImage* img)
{
	FILE* fp;

	fp = fopen(fileNm, "w");
	if(fp == NULL){
		fprintf(stderr, "파일 생성에 실패하였습니다.\n");
		return FALSE;
	}

	fprintf(fp, "%c%c\n", 'P', '3');
	fprintf(fp, "%d %d\n" , img->width, img->height);
	fprintf(fp, "%d\n", 255);


	for(int i=0; i<img->height; i++){
		for(int j=0; j<img->width * 3; j+=3){

			fprintf(fp, "%d ", img->pixels[i][j]);
			fprintf(fp, "%d ", img->pixels[i][j+1]);
			fprintf(fp, "%d ", img->pixels[i][j+2]);

		}

		fprintf(fp, "\n");	// 생략가능
	}

	fclose(fp);
	
	return TRUE;
}

  우선 매직넘버를 기록할 때 아스키타입 PPM 파일의 매직넘버 값인 'P3'를 써주고 있습니다. 그리고 혹시나해서 말씀드리는데 배열은 가로방향으로 x 3이 되긴했지만 이미지의 넓이가 바뀐 것은 아닙니다. 이미지의 넓이는 실제 넓이대로 헤더부분에 값을 기록해 주시고, 우리가 이미지를 참조할 때만 이미지넓이(width) x 3 으로 접근해서 처리해주시면 됩니다.

  두 번째 for문을 잘 살펴보셔야 합니다. 모든 픽셀들을 접근하려면 for 루프의 종료조건에는 width x 3을 해줘야 합니다. 또, 인덱스 변수 j는 +3 씩 증가를 시켜줘야 각각의 픽셀을 중복없이 접근할 수 있습니다. 루프의 내부에서는 순서대로 R(Red), G(Green), B(Blue) 값을 파일에 기록하고 있습니다.

  안쪽 for문의 밖에 있는 개행문자('\n') 기록하는 부분은 생략해도 관계없지만 나중에 결과파일을 텍스트 에디터로 열어보게 되는 경우에는 개행문자를 넣어주는 편이 보기에 좋습니다.


사실 안쪽의 for문에서 +3씩 증가시키지 않고

    for(int i=0; i<img->height; i++){
        for(int j=0; j<img->width*3; j++){
            fprintf(fp, "%d", img->pixels[i][j];
        }
    }

이런식으로 코딩해도 결과는 동일합니다. 다만 논리적으로 1픽셀씩 접근한다는 방식이 좀 더 이해하기 쉬울 것 같아서 +3씩 증가하는 방법을 사용하였습니다. 그냥 참고적으로 알아두세요~


  쓰고나니깐 사실 뭐 그다지 크게 바뀐부분도 없네요. 소스에서 변경된 부분은 폰트를 굵게 처리해놨으니 그 부분만 좀 더 관심있게 살펴보시면 되겠습니다.

  이 상태로는 원본과 결과 영상이 동일하므로, 제대로 된 결과 비교를 위해 이 전 강의에서 배웠던 '이미지 밝기조절'을 응용해서 빨간색의 값을 조금만 올려보도록 하겠습니다. 혹시 밝기조절 강좌를 다시 보고 싶으시면, 여기를 클릭하세요. 해당 강좌로 이동합니다. (새창)
  

  아래 영상은 RGB 값 중 R(Red) 값을 +30 해준 결과 영상입니다. 전체적으로 붉은 색의 기운이 증가되었음을 알 수 있습니다. 



< 클릭하시면 원본크기로 볼 수 있습니다. >



  이전에 배웠던 흑백영상 처리에 이어 드디어 컬러영상에 대해서도 배워봤습니다. 이제 뭐 더이상 배울게 없네요~ㅋㅋ


  다음 강좌는 쉬어가는 페이지를 마련했습니다. 배움에는 이유가 있어야하겠죠. 우리는 왜 영상처리를 공부하고 있을까요? 실제 영상처리가 실생활에서 어떻게 사용되고 또, 우리에게 어떤 도움을 주는지 살짝만 알아보는 순서를 마련했습니다. 이러한 것들을 알고나면 배우고자 하는 의욕이 더 커지지 않을까 하는 생각입니다~ㅋ


  쉬어가기 다음으로는 지금까지 그레이영상으로 배웠던 여러가지 처리들을 컬러영상으로도 해보고, 다양한 색상체계에 대해서도 살짝 배워본 후 이번 강좌는 끝이 날 것 같습니다. 몇몇 빠진 주제는 모아서 추후에 부록처럼 다시 강좌를 올릴생각이고요. 이 다음에는 무슨 강좌를 이어나갈지 벌써부터 고민이네요. 뭐 잘 할 줄 아는게 있어야 자신있게 강좌를 진행할텐데..ㅋㅋ 

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

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



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

PPM파일 읽기.cpp

붉은색 강조.cpp



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


Comments