http://hi.baidu.com/pengjun/blog/item/4daf32faf36dea16a9d311ed.html
一、了解位图的文件结构
我这里要说的主要是如何读入8位或24位位图。因为这两种格式的位图更加具有代表性。如32位位图也只是多了一个透明度的分量罢了。其跟24位位图的格式相比变化不大。
我们常说进行数字图像处理,其实主要是处理位图的像素数据和采用何种算法去处理像素数据。可是,要想处理像素数据,首先就是要读入位图的数据。不然,处理从何谈起。
下面我将详细的说明BMP位图的文件结构,相信看完之后,对于读入位图就不是什么难事了。当然,我这里的前提是你熟悉如何读入文件。好了,废话不多说了。我们开始吧。
BMP位图的第一部分就是文件头。下面是它的结构:
typedef struct tagBITMAPFILEHEADER {
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER, *PBITMAPFILEHEADER;
上面的这个文件头的结构体是摘自MSDN。它的各项的含义如下:bfType代表的是文件格式,就是“BM”,用十六进制的数表示是0x4d42,用十进制的数表示就是19778。所以,从BMP文件的前两个字节我们就可以判断我们要打开的图片是不是位图,如果不是的话,我们就可以不用读了。bfSize代表的是该位图文件的大小(包含文件头、信息头、调色板(如果有)、像素数据)。它是一个DWORD类型,占4个字节。bfReserved1和bfReserved2都是保留量,因此它们应该都为0。它们合起来占4个字节。bfOffBits代表的是像素数据距离文件头的位置,也就是偏移地址。如果我们想直接读像素数据的话,可以把文件指针偏移bfOffBits个字节,然后读取就可以了,知道读到文件结尾。这样算起来,文件头一共占了2+4+2+2+4=14字节。所以,这个文件头的大小固定是14字节的。而且,还有重要的一点是,其实在保存BMP文件的时候,保存的顺序也是按照bfType、bfSize、bfReserved1、bfReserved2、bfOffBits的顺序的。因此,如果我们不想用微软的这个结构体的话,你可以按照上面的顺序依次的读入各项,用不用这个结构体,关系不是很大。不过只是说,用结构体的话,可能会带来一些方便罢了。这一点,在当你用C或Java来读取的时候,会感觉更深刻。因为在C或Java中没有微软提供的这个结构体,要么自己构造这个结构体,要么你就按找顺序和字节数依次读出各项的值。后面有我的C和Java读入位图的例子,你可以参看下。
BMP位图的第二部分就是信息头。下面是它的结构:
typedef struct tagBITMAPINFOHEADER{
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER, *PBITMAPINFOHEADER;
当然了,这个结构体还是来自MSDN的。biSize代表的是该结构体的大小,即40字节。biWidth代表的是位图的宽度,以像素为单位。biHeight代表的是位图的高度,以像素为单位。biPlanes代表的位图的平面数,平面数不同,RGB各分量所排列的顺序不一样。一般常见的是1,也就是RGB各分量是按照BGR的顺序在像素数据中排列的。biBitCount代表的是位图的位数,有1、16、8、24、32等。biCompression代表是位图的压缩方法,由于BMP位图是没有压缩的,所以通常情况下它的值都为0。biSizeImage代表的是位图像素数据的大小,就是高度乘上每行像素所占的字节数。biXPelsPerMeter代表的是水平方向的分辨率,以米为单位。这个参数没有什么太大的意义,至少现在我还没发现。所以,它通常情况下都为0,不是0也没有关系。biYPelsPerMeter代表的是垂直方向的分辨率,以米为单位,通常为0。biClrUsed代表的是位图用到的颜色数,可以设置为0,代表的是全部用到了。biClrImportant代表的是位图中重要的颜色数,可以设置为0,代表的是全部都很重要。LONG和DWORD都占4个字节,WORD占2个字节,因此,该结构体(也就是信息头)共占40字节。还要说明的一点是,在保存位图的时候,我们也可以按照上面的结构体中各项的顺序来保存位图。在读入位图的时候,也可以按字节和上面的顺序来依次读入各项。用不用结构体,关系不大。还是那句话,当你用C或Java读入信息头的时候,就会理解这个顺序的重要性了。
BMP位图的调色板。它的结构如下:
typedef struct tagRGBQUAD {
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD
同样,上面的这个结构体仍然来自MSDN。首先要说明的一点是,只有8位位图具有调色板。而调色板是有若干项(最多256项)构成的,而每一项都是一个RGBQUAD的结构体,也就是说每一项都有一个蓝色、绿色、红色和保留量。而在8位位图中,如果我们读到的biClrUsed不为0,代表没有全部用上256中颜色的话,那代表像素数据的偏移地址就不是1078(14+40+256*4=1078)。这样其实也完全没有影响,我们在读入和保存位图的时候,对于8位位图来说,完全没有必要去读入调色板。因为我们完全可以自己去构造一个调色板,如下面的程序:
RGBQUAD *pal=new RGBQUAD[256];
for(int i=0;i<256;i++)
{
pal[i].rgbBlue=i;
pal[i].rgbGreen=i;
pal[i].rgbRed=i;
pal[i].rgbReserved=0;
}
如上的程序,我们就完成了一个调色板的构造。而且,如果你自己按照用到的颜色数再去读取调色板,也比较麻烦。再说了,也没有那个必要。
BMP文件的像素数据。我在这里只说8位和24位的位图的像素数据。我们在读取像素的值时,首先要知道的就是位图的位数。因为位数不同,像素值的排列顺序是不同的。比如8位位图,它的一个字节就代表一个像素值,也就是灰度值。而24位位图,它的一个像素占三个字节,RGB三个分量每个占一个字节。且其排列顺序是BGR,而不是RGB。也就是说,在读入24位位图的像素时,要先读B,再读G,最后是R。如果位图的高度为正数,那代表图像的左上角的那个像素存在像素矩阵的第一个值。如果位图的高度为负数,那代表图像的左下角的那个像素存在像素矩阵的第一个值。这个不同太担心,一般的位图都是将左上角的第一个像素的值存储在像素矩阵的最前面的。
二、用VC6.0读入、显示和保存位图
新建一个基于对话框的应用程序,工程名为ReadImage,并在对话框上放上三个按钮,它们的ID属性分别为IDC_BTN_READ、IDC_BTN_SHOW、IDC_BTN_SAVE,它们的名字依次为Read、Show、Save。如下图:
为CReadImageDlg类添加三个成员变量:BITMAPFILEHEADER *m_pBmfh;BYTE *m_pBmInfo;BYTE *m_pPixel;添加完成之后,ReadImageDlg.h的整个程序如下:
// ReadImageDlg.h : header file
//
#if !defined(AFX_READIMAGEDLG_H__FD65CF60_4E3C_4111_B736_CD4B51F1CE63__INCLUDED_)
#define AFX_READIMAGEDLG_H__FD65CF60_4E3C_4111_B736_CD4B51F1CE63__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
/
// CReadImageDlg dialog
class CReadImageDlg : public CDialog
{
// Construction
public:
BYTE* m_pPixel;
BYTE* m_pBmInfo;
BITMAPFILEHEADER* m_pBmfh;
CReadImageDlg(CWnd* pParent = NULL); // standard constructor
// Dialog Data
//{{AFX_DATA(CReadImageDlg)
enum { IDD = IDD_READIMAGE_DIALOG };
// NOTE: the ClassWizard will add data members here
//}}AFX_DATA
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CReadImageDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
//}}AFX_VIRTUAL
// Implementation
protected:
HICON m_hIcon;
// Generated message map functions
//{{AFX_MSG(CReadImageDlg)
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.
#endif // !defined(AFX_READIMAGEDLG_H__FD65CF60_4E3C_4111_B736_CD4B51F1CE63__INCLUDED_)
作者:彭军
在CReadImageDlg的构造函数中添加一句程序:m_pBmfh=new BITMAPFILEHEADER;添加完成后,其构造函数为:
CReadImageDlg::CReadImageDlg(CWnd* pParent /*=NULL*/)
: CDialog(CReadImageDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CReadImageDlg)
// NOTE: the ClassWizard will add member initialization here
//}}AFX_DATA_INIT
// Note that LoadIcon does not require a subsequent DestroyIcon in Win32
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
//为位图文件头指针分配一个空间
m_pBmfh=new BITMAPFILEHEADER;
}
然后为Read按钮添加单击的响应函数,在下面写入如下程序:
void CReadImageDlg::OnBtnRead()
{
// TODO: Add your control notification handler code here
//用打开对话框获得要打开位图的路径
CFileDialog dlg(true,"*.bmp",NULL,OFN_OVERWRITEPROMPT| OFN_HIDEREADONLY,"BMP files(*.bmp)|*.bmp||");
if(dlg.DoModal()!=IDOK)
{
return;
}
//新建一个CFile类的对象dib,并以读的模式打开
CFile dib(dlg.GetPathName(),CFile::modeRead);
//读入位图文件头
dib.Read(m_pBmfh,sizeof(BITMAPFILEHEADER));
//为位图信息(信息头+调色板(如果有的话))分配空间
m_pBmInfo=new BYTE[m_pBmfh->bfOffBits-14];
//读入位图信息,其大小为像素数据的偏移地址-位图文件头大小
dib.Read(m_pBmInfo,m_pBmfh->bfOffBits-14);
//下面的程序的目的是计算像素矩阵的大小,就是为了得到height和LineBytes
//新建一个位图信息头变量
BITMAPINFOHEADER bmih;
//拷贝一个信息头大小的字节到该变量中
memcpy(&bmih,m_pBmInfo,sizeof(BITMAPINFOHEADER));
//从信息头中获取位图的信息
long width=bmih.biWidth;//获取宽度
int bitCount=bmih.biBitCount;//获取位数
long height=bmih.biHeight;//获取高度
long LineBytes=(width*bitCount+31)/32*4;//计算每行像素所占的字节数
//为像素矩阵分配空间
m_pPixel=new BYTE[height*LineBytes];
//读入位图的像素矩阵
dib.Read(m_pPixel,height*LineBytes);
//关闭文件
dib.Close();
}
为Show按钮添加一个响应函数,其代码如下:
void CReadImageDlg::OnBtnShow()
{
// TODO: Add your control notification handler code here
BITMAPINFOHEADER bmih;
memcpy(&bmih,m_pBmInfo,sizeof(BITMAPINFOHEADER));
long width=bmih.biWidth;
long height=bmih.biHeight;
StretchDIBits(GetDC()->m_hDC,0,0,width,height,0,0,width,height,m_pPixel,(BITMAPINFO*)m_pBmInfo,DIB_RGB_COLORS,SRCCOPY);
}
为Save按钮添加一个响应函数,其代码如下:
void CReadImageDlg::OnBtnSave()
{
// TODO: Add your control notification handler code here
//用保存对话框获取保存路径
CFileDialog dlg(false,"*.bmp",NULL,OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,"BMP files(*.bmp)|*.bmp||");
if(dlg.DoModal()!=IDOK)
{
return;
}
//以写二进制数据的方式,打开文件
CFile dib(dlg.GetPathName(),CFile::modeCreate | CFile::modeWrite | CFile::typeBinary);
//写入已经读入的文件头
dib.Write(m_pBmfh,sizeof(BITMAPFILEHEADER));
//写入已经读入的位图信息(信息头+调色板(如果有的话))
dib.Write(m_pBmInfo,m_pBmfh->bfOffBits-14);
//下面的程序的目的是计算像素矩阵的大小,就是为了得到height和LineBytes
BITMAPINFOHEADER bmih;
memcpy(&bmih,m_pBmInfo,sizeof(BITMAPINFOHEADER));
long width=bmih.biWidth;
int bitCount=bmih.biBitCount;
long height=bmih.biHeight;
long LineBytes=(width*bitCount+31)/32*4;
//写入已经读入的像素矩阵
dib.Write(m_pPixel,height*LineBytes);
//关闭文件
dib.Close();
}
为了尽量使整个程序简单、易懂,我没有对读入失败或写入失败的情况做判断。不仅如此,我们还应该添加一个析构函数来释放分配给信息头和像素矩阵的内存的。
把注释看明白,整个读入位图、显示位图和保存位图就都明白了。