지난 포스팅에서 비트맵 이미지를 읽어오고 다시 쓰는 함수 readImage
, writeImage
를 구현하여 비트맵 파일에 접근하기가 훨씬 용이해졌다. 다양한 프로세싱이 있겠지만, 색 필터, 그중에서도 흑백필터가 가장 쉬울것 같아 도전해보았다.


왼쪽의 비트맵 파일을 오른쪽처럼 만들어 줄 예정이다!
1. Gray Scale
근데 어떻게 바꿔주지? 저 두 사진의 각 픽셀의 값들은 어떤 차이가 있을까? 사실 글쓴이는 직관적으로 그냥 이거지 않을까 하고 때려 맞추어 아래의 방법을 시도했고, 잘 맞았다.
RGB에는 결국 Red의 밝기, Green의 밝기, Blue의 밝기가 저장되어 있다. 단순 1비트짜리 monochrome(단색)이 아닌 이상, 흑백 사진에는 흰색과 검은색, 그리고 그 중간의 수많은 회색들이 존재한다. 이 색들이 구별되는 것은 명도(밝기)이나 이들은 공통적으로 무채색으로 불린다. 채도가 없는? 색들이다. 흰색은 24bit색 깊이에서
0xFFFFFF
로, 검은색은0x000000
으로 표현하는데 보면 RGB 성분이 모두 같다. 채도와 각 RGB성분의 상관관계까지는 모르겠으나 RGB성분이 비슷할수록 채도가 낮아지는 것 같다. 그래서 세 성분의 평균을 내어 일괄적으로 적용을 하면 눈이 인식하는 밝기는 유지하되 채도를 낮출수 있지 않을까 생각하였다.
p->rgbRed = p->rgbGreen = p->rgbBlue
= ( (p->rgbRed) + (p->rgbGreen) + (p->rgbBlue) ) / 3;
와 같이 각 성분을 더해 3으로 나눠 평균을 구한 뒤, 각 성분에 일괄적으로 적용하였다. 위에서 추론한 내용이 맞는 내용인지는 몰라도 어쨌든 앞서 보인 결과물처럼 잘 되는 듯 하다.
근데 글을 쓰면서 구글링을 하니 바로 답이 나오더라. 예시까지 똑같다 글에 따르면 사람의 눈마다 각 색상에 대한 민감도가 다르기도하고, 다른 방법으로 프로세싱하면 다른 느낌의 결과물이 나오니 1:1:1이 아닌 다양한 비율로 시도하는 듯하다. 뭐 쨌든, 1:1:1 만으로도 꽤나 준수한 결과물이 나온 듯 하다.
어쨌든 핵심 원리는 찾아내었다. 문제는 파일마다 Pixel Array 데이터와 다른 부가 요소들의 구성형식이 달라 각각 읽는 방법이 다를 수밖에 없다. 이를 모두 분류하여 읽는 작업을 구현해야겠다.
2. Function Pointer로 분기하기
이런 글도 쓴 적이 있었는데, 드디어 function pointer를 이용하여 분기하는 방법을 활용하게 되었다. sizPxl
이 0일때, 1,2,3,4일때로 분류 가능하여 배열로 놓더라도 연속적이고 처리하기도 편하고, 가독성도 높아 좋은 듯하다.
void grayLess8(IMAGE* img);
void gray8(IMAGE* img);
void gray16(IMAGE* img);
void gray24(IMAGE* img);
void gray32(IMAGE* img);
void (*grayfunc[])(IMAGE*) = {grayLess8,gray8,gray16,gray24,gray32};
int gray(char* src, char* dst){
IMAGE img;
if(!readImage(&img,src)) return 0;
grayfunc[img.sizPxl](&img);
if(!writeImage(&img,dst)) return 0;
freeImage(&img);
return 1;
}
우선 지난 포스팅의 응용형태를 불러왔기 때문에 특별히 설명할 부분은 없다. main
대신 gray
함수를 정의하여 사용자가 불러 사용할 수 있도록 만들 예정이다. ‘processing’으로만 표시해놓고 미완성했던 부분은 색상깊이에 따라 읽는 방식이 다를 것이기 때문에 각각의 함수가 담긴 function pointer 배열로 대치하였다.
3. grayfunc 각 요소들 구현하기
이제 function pointer 배열을 이루는 각 함수들을 적절히 gray scale로 프로세싱하도록 구현해주면 완성되겠다.
3-0. void grayLess8(IMAGE* img);
어떻게 구현할까 하다 자괴감이 들었다. 1bit는 monochrome(단색)이라고도 하는데 진짜 흰색과 검은색으로만 이루어져 있다. 4bit부터는 16색이다보니 좀 더 낫긴한데 색이 16개 밖에 없는데 무슨 의미가 있겠나. 그림판으로 4bit변환 시켰을때는 처참했는데 컨버터로 바꾸니 꽤 볼만하긴하다. 아래는 각각 lena sample사진을 1bit와 4bit로 변환한 결과이다.


안그래도 한 픽셀이 1byte보다 작아 (채널이 아닌) 픽셀마저도 비트필드로 접근해야하는 처참한 포맷인데 잘 쓰이지도 않을 형식에 시간낭비는 적절치 않다고 판단했다. 여기에 쏳아부을 노력은 다른 곳에 쓰기로 하고 먼 훗날로 미루기로 하였다.
void grayLess8(IMAGE* img){
//TODO: processing
return;
}
그래서 이렇게 비워놓고 마무리 지었다.
3-1. void gray8(IMAGE* img);
어쨌든 한 픽셀의 각 채널값의 평균을 내야하는데, 8bit의 경우 색상데이터는 color table에 있다. 따라서 Pixel Array가 아닌 color table의 모든 색상에 적용해야한다.
void gray8(IMAGE* img){
for(RGBQUAD* p=((RGBQUAD*)(img->extra))+(img->bi.clrUsed-1);p>=(RGBQUAD*)(img->extra);--p){
p->rgbRed=p->rgbGreen=p->rgbBlue
= ( (p->rgbRed) + (p->rgbGreen) + (p->rgbBlue) ) / 3;
}
}
color table의 모든 색상을 역순이지만 차례대로 탐색하였다.
3-2. void gray16(IMAGE* img);
#define IDX(IMG,X,Y) (((IMG)->data)+((Y)*((((IMG)->bi.width)*((IMG)->sizPxl)+((IMG)->padding)))) + ((X)*((IMG)->sizPxl)))
void gray16(IMAGE* img){
for (int y=img->bi.height-1; y>=0; --y)
for (int x=img->bi.width-1; x>=0; --x){
RGB16* p = (RGB16*) IDX(img,x,y);
if(img->bi.compression){
unsigned short gray = (R565(*p) + (G565(*p)>>1) + B565(*p)) / 3;
*p=RGB565(gray,gray<<1,gray);
}else{
unsigned short gray = ( R555(*p) + G555(*p) + B555(*p) ) /3;
*p=RGB555(gray,gray,gray);
}
}
}
여기에서 언급한 바와 같이 탐색을 하면 출력순서대로 순차적 탐색이 가능하나, 그건 필요없고, 경계값 계산시 참조와 뺄샘이 계속 일어나는 것을 방지하기위해 탐색 순서를 본의아니게 저렇게 역순으로 해놨다.
padding
도 있고, 색상 깊이에 따라 pixel하나의 크기가 달라져 주소값 계산이 복잡해졌다.
index = ((img->bi.width)*(img->sizPxl)+(img->padding))*y + (img->sizPxl)*x;
로 index를 구하여 접근했었으나, 앞으로 계속 사용하게 될 것 같아 매크로 상수로 만들었고, bitmap.h
에 추가하게 되었다.
처음에는 아래와 같이 순차탐색을 고려하였다.
for(RGB16* p=((RGB16*)(img->data+img->sizData))-1;p>=(RGB16*)img->data;--p){
unsigned short gray = (R565(*p) + (G565(*p)>>1) + B565(*p)) / 3;
*p=RGB565(gray,gray<<1,gray);
}
그러나 padding
이 있어 탐색중에 2byte의 RGB16의 각 1byte가 서로 엇갈리게 되고, 대참사가 벌어진다.
이 사진을 대충보면 그냥 노이즈가 생긴 것 같지만, 확대하여 가장자리 부분에 주목해라. byte가 엇갈려 한 픽셀의 파란색과 빨간색이 분리된 것들이 쭉 보인다. 그래서 이 방법을 포기하고 위와 같이 이중반복문으로 좌표탐색하듯 접근하였다.
근데 반복문 안에 조건문을 넣으면 반복시마다 조건비교작업이 이루어진다. (물론 컴파일러는 최적화를 해주겠지만) 반복할때마다 바뀌는 조건이 아닌 항상 같은 조건이니 반복문 바깥으로 빼주었다.
void gray16(IMAGE* img){
if(img->bi.compression){
for (int y=img->bi.height-1; y>=0; --y)
for (int x=img->bi.width-1; x>=0; --x){
RGB16* p = (RGB16*) IDX(img,x,y);
unsigned short gray = (R565(*p) + (G565(*p)>>1) + B565(*p)) / 3;
*p=RGB565(gray,gray<<1,gray);
}
}
else{
for (int y=img->bi.height-1; y>=0; --y)
for (int x=img->bi.width-1; x>=0; --x){
RGB16* p = (RGB16*) IDX(img,x,y);
unsigned short gray = ( R555(*p) + G555(*p) + B555(*p) ) /3;
*p=RGB555(gray,gray,gray);
}
}
}
3-3. void gray24(IMAGE* img);
위에서 다 설명했기때문에 뭐 더 설명할 내용은 없다.
void gray24(IMAGE* img){
for (int y=img->bi.height-1; y>=0; --y)
for (int x=img->bi.width-1; x>=0; --x){
RGBTRIPLE* p = (RGBTRIPLE*) IDX(img,x,y);
p->rgbtRed=p->rgbtGreen=p->rgbtBlue
= ( (p->rgbtRed) + (p->rgbtGreen) + (p->rgbtBlue) ) / 3;
}
}
3-4. void gray32(IMAGE* img);
void gray32(IMAGE* img){
for (int y=img->bi.height-1; y>=0; --y)
for (int x=img->bi.width-1; x>=0; --x){
RGBQUAD* p = (RGBQUAD*) IDX(img,x,y);
p->rgbRed=p->rgbGreen=p->rgbBlue
= ( (p->rgbRed) + (p->rgbGreen) + (p->rgbBlue) ) / 3;
}
}
4. gray.c 완성
#include "bitmap.h"
void grayLess8(IMAGE* img){
//TODO: processing
return;
}
void gray8(IMAGE* img){
for(RGBQUAD* p=((RGBQUAD*)(img->extra))+(img->bi.clrUsed-1);p>=(RGBQUAD*)(img->extra);--p){
p->rgbRed=p->rgbGreen=p->rgbBlue
= ( (p->rgbRed) + (p->rgbGreen) + (p->rgbBlue) ) / 3;
}
}
void gray16(IMAGE* img){
if(img->bi.compression){
for (int y=img->bi.height-1; y>=0; --y)
for (int x=img->bi.width-1; x>=0; --x){
RGB16* p = (RGB16*) IDX(img,x,y);
unsigned short gray = (R565(*p) + (G565(*p)>>1) + B565(*p)) / 3;
*p=RGB565(gray,gray<<1,gray);
}
}
else{
for (int y=img->bi.height-1; y>=0; --y)
for (int x=img->bi.width-1; x>=0; --x){
RGB16* p = (RGB16*) IDX(img,x,y);
unsigned short gray = ( R555(*p) + G555(*p) + B555(*p) ) /3;
*p=RGB555(gray,gray,gray);
}
}
}
void gray24(IMAGE* img){
for (int y=img->bi.height-1; y>=0; --y)
for (int x=img->bi.width-1; x>=0; --x){
RGBTRIPLE* p = (RGBTRIPLE*) IDX(img,x,y);
p->rgbtRed=p->rgbtGreen=p->rgbtBlue
= ( (p->rgbtRed) + (p->rgbtGreen) + (p->rgbtBlue) ) / 3;
}
}
void gray32(IMAGE* img){
for (int y=img->bi.height-1; y>=0; --y)
for (int x=img->bi.width-1; x>=0; --x){
RGBQUAD* p = (RGBQUAD*) IDX(img,x,y);
p->rgbRed=p->rgbGreen=p->rgbBlue
= ( (p->rgbRed) + (p->rgbGreen) + (p->rgbBlue) ) / 3;
}
}
void (*grayfunc[])(IMAGE*) = {grayLess8,gray8,gray16,gray24,gray32};
int gray(char* src, char* dst){
IMAGE img;
if(!readImage(&img,src)) return 0;
grayfunc[img.sizPxl](&img);
if(!writeImage(&img,dst)) return 0;
freeImage(&img);
return 1;
}
더불어 bitmap.h
에
int gray(char* src, char* dst);
도 추가하였다.
5. 적용
#include "bitmap.h"
int main(void){
gray("source/lena/8.bmp","result/lena/8_gray.bmp");
gray("source/lena/16_555.bmp","result/lena/16_555_gray.bmp");
gray("source/lena/16_565.bmp","result/lena/16_565_gray.bmp");
gray("source/lena/24.bmp","result/lena/24_gray.bmp");
gray("source/lena/32.bmp","result/lena/32_gray.bmp");
return 0;
}
결과물은 github: BitmapProject의 result 폴더에서 확인할 수 있다.