1. 서문
BMP 파일은 이미지 파일중에 가장 간단한 파일중 하나입니다. 기본적으로 BMP 파일은 1, 4, 8, 16, 24 및 32비트를 지원합니다. 하지만 16과 32비트는 거의 사용하는 예가 없습니다. BMP 파일은 사용이 쉬운만큼 용량이 큰 파일입니다. 그렇기 때문에 이 파일 역시 기본적인 압축형식을 지원합니다. 그 방식은 간단한 압축방식인 Run-Length Compresstion 방식이며, 4비트와 8비트를 지원합니다.(?RLE4, ?RLE8). 어쨌든 이 압축방식의 특성은 간단한 색상의 그림 블록의 경우에는 비교적 효율이 좋으나, 복잡한 색상 혹은 완벽히 랜덤한 색상의 경우는 전혀 압축이 되지 않을수 있습니다. 그래서, 대부분의 BMP 파일은 압축을 사용하는 예가 드물게 됩니다.
기본적인 비트맵 파일의 구조는 다음과 같습니다.
파일 헤더 |
이미지 헤더 |
팔레트 |
영상 데이터(거꾸로 들어있음) |
보는것과 같이 BMP 파일은 상당히 단순한 구조를 갖고 있습니다. 또한, 만약 Windows 환경에서 프로그래밍을 하는 경우에는 windows.h 헤더화일에 정의가 되어 있습니다. 다른 환경에서 프로그래밍을 하는 경우에는 아래와 같이 설명하는 예를 직접 정의한 헤더 파일을 가지고 계셔야 합니다.
3. File Header
파일 헤더는 BITMAPFILEHEAER라는 구조체에 정의되어 있습니다. 이 헤더의 주된 임무(?)는 현재의 파일 포맷이 정말 BMP인지에 대한 정보를 주는것입니다. 대부분의 파일은 자신의 성격에 맞게 확장자 이름이 정해져 있습니다. BMP 파일 역시 확장자는 *.BMP로 정의 되어 있겠죠. 그러나 불행히도, 우연히 다른 파일이 XX.BMP라고 썼을수 있으므로, BMP 디코더가 이를 분석해 내야 합니다. 그 분석 방법에는 아래와 같이 세가지 방법이 있습니다. 그 방법을 알기에 앞서 BITMAPFILEHEADER의 레이어별 구조를 보면,
Field name | Size in Bytes | Description |
bfType | 2 | BM이라는 캐릭터형이 저장되어있습니다. |
bfSize | 4 | 파일의 전체 크기를 표시합니다. |
bfReserved1 | 2 | 사용하지 않습니다. |
bfReserved2 | 2 | 사용하지 않습니다. |
bfOffBits | 4 | 실질 데이터(pixel)의 시작좌표를 Offset 주소를 나타냅니다. |
위 구조를 이해하셨다면, 현재의 파일포맷이 정말 BMP 파일인지를 구분하는 방법을 아래와 같이 추려낼수 있습니다.
- 파일의 첫 2바이트가 BM으로 시작하는가.
- 바이트 단위의 정확한 파일크기와 헤더내의 bfSize가 있어야할 값과 일치하는가.
- bfReserved1과 bfReserved2가 있어야할 위치에 0값이 있는가.
세가지를 모두 체크하면, BMP 파일 여부를 판명할수 있습니다. 하지만, 디코더의 크기를 줄이기 위해, 첫 번째것만 체크하는 경우도 있습니다.
4. Image Header
이미지 헤더는 BITMAPFILEHEADER 구조체 다음에 바로 위치하도록 되어있습니다. 이미지 헤더는 2가지 종류가 있습니다. 앞에서 언급했듯이 BMP 파일은 두가지 종류가 있습니다. windows version 3와 OS/2에서 사용하는 BMP가 있다고 말씀드렸습니다. 이 경우, 이를 구분하여 저장해야 합니다. 먼저 전자의 경우에 저장해야할 이미지 헤더 구조체는 BITMAPINFOHEADER에 정의 되어있고, 후자의 경우는 BITAMPCOREHEADER에 정의 되어 있습니다. 이 두가지를 구분하는 방법은 어떠한 식별 필드가 존재하는 것이 아니라, 그 크기로 식별을 해야합니다. 이를 설명하기 위해서는 일단 위 두가지의 구조체를 알아보도록 하겠습니다.
4.1. BITAMPINFOHEADER 구조체
Field name | Size | Description |
biSize | 4 | 헤더 크기(최소 40bytes |
biWidth | 4 | 이미지 폭 |
biHeight | 4 | 이미지 높이 |
biPlanes | 2 | 현재 지원값은 1입니다. |
biBitCount | 2 | 비트수 1,4,8,16,24,32 |
biCompression | 4 | 압축타입 : BI_RGB(0),BI_?RLE8(1),BI_?RLE4(2),BI_BITFIELDS(3) |
biSizeImage | 4 | 이미지 크기 |
biX?PelsPerMeter | 4 | 미터당 픽셀수 x축 |
biY?PelsPerMeter | 4 | 미터당 픽셀수 y축 |
biClrUsed | 4 | 실질적으로 사용될 컬러맵의 엔트리수 |
biClrImportant | 4 | 주로 사용되는 컬러수 |
4.2. BITMAPCOREHEADER 구조체
Field name | Size | Description |
bcSize | 4 | 헤더 크기(12bytes) |
bcWidth | 2 | 이미지 폭 |
bcHeight | 2 | 이미지 높이 |
bcPlanes | 2 | 현재 지원값은 1입니다. |
bcBitCount | 2 | 비트수 1,4,8,24 |
위의 구조체를 보면, 두가지를 구분할 방법을 찾으셨을껍니다. 바로 헤더의 크기로 구분하면 되겠죠? 윈도우에서 지원하는 BMP의 경우 헤더의 크기가 최소 40바이트가 필요합니다. 그렇기 때문에 수치적으로 쉽게 구분을 할수 있습니다.
5. Color Palette
컬러팔레트는 세가지 포맷형식이 있습니다. 이는 BMP 파일의 비트수에 의해 구분이 되는데, 1, 4, 8비트를 갖는 이미지인 경우 RGB값을 나타내기 위해 컬러맵을 사용합니다. 비트수는 이미지 헤더에서 biBitCount나 bcBitCount에서 판단할수 있습니다. 여기서 윈도우 BMP 파일인 경우는 RGBQUAD 구조체를 사용하고, OS/2에서는 RGBTRIPLE 구조체를 이용합니다.
Field | Size | Description |
rgbBlue | 1 | |
rgbGreen | 1 | |
rfbRed | 1 | |
rgbReserved | 1 | 항상 0값 |
RGBTRIPLE 구조체
Field | Size | Description |
rgbtBue | 1 | |
rgbtGreen | 1 | |
rgbtRed | 1 |
세 번째 포맷형식은 컬러맵을 사용하지 않습니다. 일단 BI_BITFIELDS를 세팅하기 위한 biCompression이 없는 216, 24, 32비트 이미지인 경우는 컬러 팔레트를 사용하지 않습니다. 이는 팔레트 자체가 용량이 커버리기 때문입니다. 만약, 16, 32비트이고, BI_BITFIELDS인 biCompression값을 갖는다면, RGBQUAD 구조체에는 마스크를 사용합니다.(RGBTRIPLE은 압축을 지원하지 않으므로, 생각하지 않으셔도 됩니다.) 마스크에 대해 대강 설명하면, 미술시간에 종이에 무늬를 그리고, 칼로 짤라내고, 그 위에 스프레이 같은걸로 막 뿌리면, 종이 아래에 자른 무늬가 생기죠? 이때 짜른 종이가 바로 마스크입니다. 컴퓨터에서 마스크란 일련의 비트열입니다. 예를 들면 열 개의 비트열이 있다고 생각해보죠. 그중에 특정위치의 비트값을 알아내고 싶다면, 마스크와 AND연산 그리고 SHIFT연산을 이용하여 구해낼수 있습니다.
6. DIB 사용시 주의점
- 이미지는 거꾸로 저장됨
실제로 비트맵 영상이 저장될 때는 이미지가 거꾸로 저장되어 있다. 따라서, 비트맵에서 영상데이터를 나중에 영상처리를 위해 사용할 배열로 다시 저장할 때는 거꾸로 반전시켜 저장해주면 된다. - 영상 가로 길이는 4바이트의 배수
비트맵은 메모리 저장시, 가로줄의 크기는 항상 4바이트의 배수가 되어야 한다. 실제 사용하는 영상의 가로 길이는 4바이트의 배수가 아닐 수 있으므로 이럴 경우는 4의 배수바이트로 바꾸어 저장한다.
#define WIDTHBYTES(bits) (((bits) + 31) / 32 * 4) // 4바이트 배수로 변환
7. BMP 형식 영상파일 입력과 출력
#include <stdio.h> #include <windows.h> #define WIDTHBYTES(bits) (((bits)+31)/32*4) /* 영상 가로길이는 4바이트의 배수여야 함 */ #define BYTE unsigned char void main() { FILE *infile; infile=fopen("talent.bmp", "rb"); if(infile==NULL) { printf("There is no file!!!\n"); exit(1); } BITMAPFILEHEADER hf; fread(&hf,sizeof(BITMAPFILEHEADER),1,infile); /* 파일헤드를 읽음 */ if(hf.bfType!=0x4D42) exit(1); BITMAPINFOHEADER hInfo; fread(&hInfo,sizeof(BITMAPINFOHEADER),1,infile); /* 영상헤드를 읽음 */ printf("Image Size: (%3dx%3d)\n",hInfo.biWidth,hInfo.biHeight); printf("Pallete Type: %dbit Colors\n",hInfo.biBitCount); /* 256칼라 이하의 경우는 취급하지 않음 */ if(hInfo.biBitCount!=8 ) { printf("Bad File format!!"); exit(1); } RGBQUAD hRGB[256]; fread(hRGB,sizeof(RGBQUAD),256,infile); /* 팔레트를 파일에서 읽음 */ /* 영상데이타를 저장할 메모리 할당 */ BYTE *lpImg = new BYTE [hInfo.biSizeImage]; fread(lpImg,sizeof(char),hInfo.biSizeImage,infile); fclose(infile); /* 역상의 이미지 구하기 */ int rwsize = WIDTHBYTES(hInfo.biBitCount*hInfo.biWidth); for(int i=0; i<hInfo.biHeight; i++) { for(int j=0; j<hInfo.biWidth; j++) { lpImg[i*rwsize+j] = 255-lpImg[i*rwsize+j]; } } /* 영상 출력 */ FILE *outfile = fopen("OutImg.bmp","wb"); fwrite(&hf,sizeof(char),sizeof(BITMAPFILEHEADER),outfile); fwrite(&hInfo,sizeof(char),sizeof(BITMAPINFOHEADER),outfile); fwrite(hRGB,sizeof(RGBQUAD),256,outfile); fwrite(lpImg,sizeof(char),hInfo.biSizeImage,outfile); fclose(outfile); /* 메모리 해제 */ delete []lpImg; }
8. 팔레트를 사용하는 BMP 영상을 읽고 역상을 계산하여 트루컬러로 저장
#include <stdio.h> #include <windows.h> #define WIDTHBYTES(bits) (((bits)+31)/32*4) /* 영상 가로길이는 4바이트의 배수여야 함 */ #define BYTE unsigned char void main() { FILE *infile; infile=fopen("pshop256.bmp", "rb"); if(infile==NULL) { printf("There is no file!!!\n"); exit(1); } BITMAPFILEHEADER hf; fread(&hf,sizeof(BITMAPFILEHEADER),1,infile); /* 파일헤드를 읽음 */ if(hf.bfType!=0x4D42) exit(1); BITMAPINFOHEADER hInfo; fread(&hInfo,sizeof(BITMAPINFOHEADER),1,infile); /* 영상헤드를 읽음 */ printf("Image Size: (%3dx%3d)\n",hInfo.biWidth,hInfo.biHeight); printf("Pallete Type: %dbit Colors\n",hInfo.biBitCount); /* 256칼라 이하의 경우는 취급하지 않음 */ if(hInfo.biBitCount<8 ) { printf("Bad File format!!"); exit(1); } RGBQUAD *pRGB; if(hInfo.biClrUsed!=0) /* 팔레트가 있는 경우 */ { pRGB= new RGBQUAD [hInfo.biClrUsed]; /* 팔레트의 크기만큼 메모리를 할당함 */ fread(pRGB,sizeof(RGBQUAD),hInfo.biClrUsed,infile); /* 팔레트를 파일에서 읽음 */ } /* 영상데이타를 저장할 메모리 할당 */ BYTE *lpImg = new BYTE [hInfo.biSizeImage]; fread(lpImg,sizeof(char),hInfo.biSizeImage,infile); fclose(infile); /* 역상의 이미지 구하기 */ int rwsize = WIDTHBYTES(hInfo.biBitCount*hInfo.biWidth); int rwsize2= WIDTHBYTES(24*hInfo.biWidth); BYTE *lpOutImg = new BYTE [3*rwsize*hInfo.biHeight]; int index, R, G, B, i,j; if(hInfo.biBitCount==24) /* 만일 입력영상이 트루(24비트) 칼라인 경우 */ for(i=0; i<hInfo.biHeight; i++) { for(j=0; j<hInfo.biWidth; j++) { /* 팔레트가 없으므로 영상데이타가 바로 칼라값 */ lpOutImg[i*rwsize2+3*j+2] = 255-lpImg[i*rwsize+3*j+2]; lpOutImg[i*rwsize2+3*j+1] = 255-lpImg[i*rwsize+3*j+1]; lpOutImg[i*rwsize2+3*j ] = 255-lpImg[i*rwsize+3*j ]; } } else /* 트루칼라가 아닌 경우 */ for(i=0; i<hInfo.biHeight; i++) { for(j=0; j<hInfo.biWidth; j++) { index = lpImg[i*rwsize+j]; /* 영상데이타는 팔레트의 인덱스임 */ R = pRGB[index].rgbRed; /* 팔레트에서 실제 영상데이타를 가져옴(R) */ G = pRGB[index].rgbGreen; /* G */ B = pRGB[index].rgbBlue; /* B */ R = 255-R; G = 255-G; B = 255-B; /* 역상을 계산함 */ lpOutImg[i*rwsize2+3*j+2] = (BYTE)R; lpOutImg[i*rwsize2+3*j+1] = (BYTE)G; lpOutImg[i*rwsize2+3*j ] = (BYTE)B; } } /* 영상 출력 (24비트인 트루칼라로 출력) */ hInfo.biBitCount =24; hInfo.biSizeImage = 3*rwsize*hInfo.biHeight; hInfo.biClrUsed = hInfo.biClrImportant =0; hf.bfOffBits = 54; /* 팔레트가 없으므로 값이 변함 */ hf.bfSize = hf.bfOffBits+hInfo.biSizeImage; FILE *outfile = fopen("OutImg24.bmp","wb"); fwrite(&hf,sizeof(char),sizeof(BITMAPFILEHEADER),outfile); fwrite(&hInfo,sizeof(char),sizeof(BITMAPINFOHEADER),outfile); fwrite(lpOutImg,sizeof(char),3*rwsize*hInfo.biHeight,outfile); fclose(outfile); /* 메모리 해제 */ if(hInfo.biClrUsed!=0) delete []pRGB; delete []lpOutImg; delete []lpImg; }