해당 포스트는 "열혈강의 영상처리 프로그래밍" 책의 내용을 요약한 것이다.



비트맵 파일 읽기와 쓰기

 

 

비트맵 파일의 읽기와 쓰기에서 주의할 점은 비트맵 파일 안에서 영상은 상하가 반전되어 저장된다. 위 그림에서와 같이 프로그램 내부에서는 왼쪽 상단부터 시작해서 행 우선 배열에 따라 픽셀의 배열이 이루어지나 BMP 파일 안에서는 영상의 왼쪽 하단부터 시작해서 행 우선 배열에 따라 저장되 상하 좌표가 바뀐다.

 

ex) 영상 데이터 클래스 구현하기 포스트 코드에서 나왔던 LoadImage() 와 SaveImage()를 구현할 것이다.

 

 

bool LoadImage(const char* filename) 
{ 
assert(sizeof(T) == 1); // 일반적인 비트맵 파일은 1byte이기 때문에 BYTE형의 영상인 경우에만 동작할 수 있도록 한다. 

if (strcmp(".BMP", &filename[strlen(filename) - 4])) //확장자가 BMP인 비트맵 파일이 맞는 지 확인
{ 
FILE* pFile = NULL; 
fopen_s(&pFile, filename, "rb"); // 바이너리 읽기 모드 
if (!pFile) 
return false; 

BITMAPFILEHEADER fileHeader; 

if (!fread(&fileHeader, sizeof(BITMAPFILEHEADER), 1, pFile)) 
{ 
fclose(pFile); 
return false; 
} 

if (fileHeader.bfType != 0x4D42) // 'BM' 문자 검사 
{ 
fclose(pFile); 
return false; 
} 

BITMAPINFOHEADER infoHeader; 
if (!fread(&infoHeader, sizeof(BITMAPINFOHEADER), 1, pFile)) 
{ 
fclose(pFile); 
return false; 
} 

if (infoHeader.biBitCount != 8 && infoHeader.biBitCount != 24) //회색조 영상 또는 트루 컬러영상만을 읽을 수 있도록 한다.
{ 
fclose(pFile); 
return false; 
} 

if (m_nWidth != infoHeader.biWidth && m_nHeight != infoHeader.biHeight 
&& m_nChannels != infoHeader.biBitCount / 8) 
{ 
if (m_pImageData) 
delete[] m_pImageData; 

m_nChannels = infoHeader.biBitCount / 8; 
m_nHeight = infoHeader.biHeight; 
m_nWidth = infoHeader.biWidth; 
m_nWStep = (m_nWidth*m_nChannels * sizeof(T) + 3)&~3; 

m_pImageData = new T[m_nHeight*m_nWStep]; 
} 

fseek(pFile, fileHeader.bfOffBits, SEEK_SET); //실제 픽셀 데이터 위치를 가리킨다.

int r; 
for (r = m_nHeight - 1; r >= 0; r--) //비트맵 파일은 상하가 바뀌므로 m_nHeight-1부터 접근
{ 
if (!fread(&m_pImageData[r*m_nWStep], sizeof(BYTE), m_nWStep, pFile)) 
{ 
fclose(pFile); 
return false; 
} 
} 

fclose(pFile); 

return true; 
} 
else 
{ 
return false; 
} 
}

비트맵 파일의 헤더정보를 읽는 과정과 저장할 픽셀 데이터에 관한 영상 정보를 읽는 과정, 픽셀 데이터를 읽는 과정으로 구성된다.

 

 

bool SaveImage(const char* filename) 
{ 
assert(sizeof(T) == 1); // 일반적인 비트맵 파일은 1byte이기 때문에 BYTE형의 영상인 경우에만 동작할 수 있도록 한다.  

if (strcmp(".BMP", &filename[strlen(filename) - 4])) 
{ 
FILE* pFile = NULL; 
fopen_s(&pFile, filename, "wb"); 
if (!pFile) 
return false; 

BITMAPFILEHEADER fileHeader; 
fileHeader.bfType = 0x4D42; // 'BM' 
//(1)
fileHeader.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + m_nWStep*m_nHeight + (m_nChannels == 1) * 1024; 
fileHeader.bfReserved1 = 0; 
fileHeader.bfReserved2 = 0;
//(2) 
fileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + (m_nChannels == 1) * 256 * sizeof(RGBQUAD); 

fwrite(&fileHeader, sizeof(BITMAPFILEHEADER), 1, pFile); 

BITMAPINFOHEADER infoHeader; 
infoHeader.biSize = sizeof(BITMAPINFOHEADER); 
infoHeader.biWidth = m_nWidth; 
infoHeader.biHeight = m_nHeight; 
infoHeader.biPlanes = 1; 
infoHeader.biBitCount = m_nChannels * 8; 
infoHeader.biCompression = BI_RGB; 
infoHeader.biSizeImage = m_nWStep*m_nHeight; 
infoHeader.biClrImportant = 0; 
infoHeader.biClrUsed = 0; 
infoHeader.biXPelsPerMeter = 0; 
infoHeader.biYPelsPerMeter = 0; 

fwrite(&infoHeader, sizeof(BITMAPINFOHEADER), 1, pFile); 

if (m_nChannels == 1)  //(3)
{ 
for (int l = 0; l<256; l++) 
{ 
RGBQUAD GrayPalette = { l, l, l, 0 }; 
fwrite(&GrayPalette, sizeof(RGBQUAD), 1, pFile); 
} 
} 

int r; 
for (r = m_nHeight - 1; r >= 0; r--) 
{ 
fwrite(&m_pImageData[r*m_nWStep], sizeof(BYTE), m_nWStep, pFile); 
} 

fclose(pFile); 
return true; 
} 
else 
{ 
return false; 
} 
} 

SaveImag(0 함수와 반대 과정으로 비트맵 파일 헤더와 영상 정보 헤더를 생성하고 파일에 쓰는 과정, 픽셀 데이터를 파일에 쓰는 과정으로 구성된다. 회색조 영상은 팔레트 정보를 기록하는 과정도 있다. 또한, BMP파일에 픽셀 데이터를 쓸 때 상하 반대로 해서 써야한다.

(1)번 코드 fileHeader.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + m_nWStep*m_nHeight + (m_nChannels == 1) * 1024; 는 비트맵 파일의 크기를 구하는 것으로 m_nChannels가 1이면 즉, 회색조 영상일 경우 팔레트 정보의 크기를 추가해 주는 코드이다. 1024 대신 (2)번 코드와 같이 256*sizeof(RGBQUAD)를 곱해줘도 된다.

(3)번 코드는 회색조 영상의 경우 팔레트 정보를 추가해 줘야 하기 때문에 구현한 코드이다. 

+ Recent posts