常见图像文件格式
BMP文件
· BMP文件全称是Bitmap位图,后缀名是.bmp,它是一种设备无关(Device Independent Bitmap,DIB)的图像文件格式,常用于Windows。
· 特点:不进行压缩,包含丰富的图像信息。
· 缺点:占用磁盘空间过大。
· 采用RGB色彩系统。
GIF文件
· GIF文件全称是图像交换格式(Graphic Interchange Format),后缀名是.gif。
· 特点:压缩比高,磁盘占用空间较少。
· 缺点:不能存储超过256色的图像。
· 得到互联网广泛使用:①可存储单幅静止图像&同时存储若干静止图像进而形成连续动画。②可指定透明区域。③渐显方式。
· 采用RGB色彩系统。
JPEG文件
· JPEG文件全称Joint Photographic Experts Group,后缀名是.jpg或.jpeg。
· 特点:有损压缩,压缩比非常高。压缩算法复杂,存储和显示速度慢。可以处理24位真彩色,适合处理大幅图像。
· 采用的YCbCr色彩系统。
TIFF文件
· TIFF文件全称是Tag Image File Format,后缀名是.tif,它是一种独立于操作系统和文件系统的格式。
· 特点:支持压缩;支持单色、256色、24位真彩、32位、48位色。
· 可同时支持RGB、CMYK、YCbCr等多种色彩系统。
BMP文件结构
BMP文件的组成
· BMP文件可以分成四个部分:
· 第一部分——文件头,Bitmap File Header,它在Windows中是这样定义的:
typedef struct tagBITMAPFILEHEADER {
WORD bfType; //2个字节
DWORD bfSize; //4个字节
WORD bfReserved1; //2个字节
WORD bfReserved2; //2个字节
DWORD bfOffBits; //4个字节
} BITMAPFILEHEADER;
· 这个结构的长度是固定的,为14个字节,其中WORD为无符号16位整数,DWORD为无符号32位整数。
· 各个参数的定义如下
· 第二部分——信息头,Bitmap Info Header,它在Windows中是这样定义的:
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;
· 这个结构的长度也是固定的,为40个字节。
· 各个参数的定义如下
· 第三部分——调色板,Pallet,我们用一个例子来理解调色板。
· 假设有一副100*100的256色图像,它有1W个像素,如果每一个像素都用RGB三个分量表示,则一个像素需要3个字节,那么这张图就需要3W个字节,约为3.66MB,在那个年代非常大。
· 由于图像最多有256种颜色,如果建立一个256行的颜色表,每一行记录一种RGB值,这样当表示一个像素的颜色时,只需要指出颜色在第几行即可。如1表示RGB(255,0,0),那么某个像素为红色的时候,只需要存1即可。
· 只存索引的话,每个像素就只需要1个字节即可,那么就是1W字节,然后再加上颜色表3*256=768字节,一共才需要10768字节,少了2/3。
· 其实这张RGB表就是通常所说的调色板,它还有一个更确切的名字——颜色查找表(Look Up Table,LUT),引入调色板的目的是节约存储空间。
· Windows的Bitmap中就使用了调色板技术,“.gif”,“.tif”也都用了调色板。
· 调色板在Windows中是这样定义的:
typedef struct tagRGBQUAD {
BYTE rgbBlue; // 颜色的蓝色分量
BYTE rgbGreen; // 颜色的绿色分量
BYTE rgbRed; // 颜色的红色分量
BYTE rgbReserved; // 保留值,不用考虑
} RGBQUAD;
· 接下来介绍一些特别的图像的调色板。
· 24位真彩图像不需要调色板!
① 如果使用调色板,则每个像素都需要24位的存储空间来存储索引,这和直接存RGB需要的空间是一样的。
② 其次,如果要使用调色板,则需要维护一个2的24次方大小的数组,占用的空间为2^24*3B,这肯定比直接存RGB来的大。
③ 因为不需要使用调色板,因此它的文件结构就少了调色板这一项,即在信息头后面就直接是实际位图数据。
· 灰度图像使用的是256色(即8位)的调色板,因为BMP格式的文件中没有灰度图这个概念。
① 但是这个256色调色板比较特殊,它每一项的RGB值都是相同的,如:
② 对于R=G=B的色彩,我们把值带入HSI的色彩转换公式中可以看到只有亮度信息,饱和度分量都是0,即没有色彩信息。
· 第四部分——实际位图数据,Image Data,顾名思义,它存的就是图像真实的数据。
· 我们刚刚知道了有些图用了调色板,有些不用:
① 对于用到调色板的位图,图像数据就是该像素颜色在调色板中的索引值。
② 对于真彩色图,图像数据就是实际的RGB值。
· 举一些例子:
① 2色位图(黑白图像):用1位就可表示该像素的颜色,一般0表示黑,1表示白,这样一个字节就可以表示8个像素。
② 16色位图:2的4次方,故需要4位来表示一个像素的颜色,故一个字节可以表示2个像素。
③ 256色位图:2的8次方,故一个字节表示一个像素。
④ 真彩色图:2的24次方,故需要三个字节表示一个像素。
· 位图数据有一些需要注意的地方:
① 每一行的字节数必须是4的整数倍,如果不是则需要补齐。
② BMP文件的存储方式是从下到上,从左到右的,即:
BMP文件的读写与显示
· 在Windows API中,提供了一个显示位图的函数:
int StretchDIBits(
HDC hdc,
int XDest,
int YDest,
int nDestWidth,
int nDestHeight,
int XSrc,
int YSrc,
int nSrcWidth,
int nSrcHeight,
CONST VOID *lpBits,
CONST BITMAPINFO *lpBitsInfo,
UINT iUsage,
DWORD dwRop
);
· 各个参数的定义如下:
· 为什么会有目标区域和源区域的概念呢?
· 因为在BMP存储文件的时候,像素的坐标是从我们熟悉的左下角开始存的,也就是最左下角的像素的坐标为(0,0)。
· 然而在Windows中,一个窗口的坐标是从左上角开始的,也就是最左上角的像素为(0,0)。
· 因此,如果直接把BMP中读取到数据直接在屏幕上进行显示,那么显示出来的图像就是上下翻转的。
· 为了得到正确的图像,我们需要正确指定读取时候从哪里开始读,显示的时候从哪里开始显示。
· 因此一般我们会这样调用这个函数:
StretchDIBits(pDC->GetSafeHdc(),
0,0, lpBitsInfo->bmiHeader.biWidth, lpBitsInfo->bmiHeader.biHeight, // 目标区域的矩形框
0,0, lpBitsInfo->bmiHeader.biWidth, lpBitsInfo->bmiHeader.biHeight, // 源区域的矩形框
lpBits, lpBitsInfo,
DIB_RGB_COLORS,
SRCCOPY);
· 可以看到,我们指定源区域和目标区域的坐标原点都是(0,0),那就默认是从左下角读入,左上角开始显示。
· 接下来就要重点说说lpBitsInfo和lpBits这两个指针了。
· 我们知道BMP文件存储包含四个部分,文件头->信息头->调色板->实际位图数据。我们也知道lpBitsInfo是指向信息头的指针,lpBits是指向实际位图数据的指针。
· lpBitsInfo的数据类型是BITMAPINFO,它在Windows中是这样定义的:
typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader; // 信息头
RGBQUAD bmiColors[1]; // 调色板数组
} BITMAPINFO, FAR *LPBITMAPINFO, *PBITMAPINFO;
· 我们可以这样拿到lpBitsInfo指针:
// 先拿到BMP文件,path为文件的路径
FILE* fp;
if (NULL == (fp = fopen(path, "rb")))
return FALSE;
// 定义文件头和信息头
BITMAPFILEHEADER bf;
BITMAPINFOHEADER bi;
// 读入文件头和信息头,注意fread函数是先读入当前指针所在字节再往后移一位的
fread(&bf, 14, 1, fp); // 文件头长度为14前14个字节,一开始fp指针指着第0个字节,读完之后指针就指在第14个字节
fread(&bi, 40, 1, fp); // 信息头从第14个字节开始,长40个字节,此时fp指针指着第14个字节,因此直接往下读40个即可
// 计算图片的每一行有多少个像素
// 宽度*每个像素需要用到的位数 -> bi.biWidth * bi.biBitCount
// 因为需要保证最后每一行的字节数是4的倍数,因此我们对结果除32,就保证了一组里面有4个字节,里面的+31是为了四舍五入 -> (bi.biWidth * bi.biBitCount + 31) / 32
// 最后再把结果乘一个4,因为每组是4个字节,就能算出一行总共要多少字节
int LineBytes = (bi.biWidth * bi.biBitCount + 31) / 32 * 4;
// 计算整张图片需要多少字节
int ImgSize = LineBytes * bi.biHeight;
// 根据每个像素用到的颜色的位数计算出用了多少种颜色
DWORD NumColors;
if (bi.biClrUsed != 0)
NumColors = bi.biClrUsed;
else {
switch(bi.biBitCount) {
case 1:
NumColors = 2;
break;
case 4:
NumColors = 16;
break;
case 8:
NumColors = 256;
break;
case 24:
NumColors = 0; // 真彩图像没有调色板
break;
}
}
// 接下来我们计算要分配多少内存空间大小才能把图片显示出来
// 40是信息头的大小,NumColors*4是调色板占用的字节数,ImgSize是整张图片的图片信息的字节数
int size = 40 + NumColors * 4 + ImgSize;
// 分配内存空间
// 注意这里有个trick,把NULL写前面是为了方便编译器检查
if (NULL == (lpBitsInfo = (BITMAPINFO*)malloc(size)))
return FALSE;
// 最后把信息头+调色板+图像信息的数据全部读入lpBitsInfo指针内
// 首先让fp指针回到信息头的开始处
fseek(fp, 14, SEEK_SET);
// 把文件从信息头开始读入指针,读1次,大小为size
fread(lpBitsInfo, size, 1, fp);
// 设置一下用了多少种颜色
lpBitsInfo->bmiHeader.biClrUsed = NumColors;
· lpBitsInfo指针是最重要的指针,我们把它设为全局变量,这样在任何地方都可以访问。
· 在拿到了lpBitsInfo指针之后,就可以很简单的拿到lpBits指针了,因为lpBits指向实际位图数据,已经包含在lpBitsInfo里面了:
// 实际位图数据处于调色板后
// lpBitsInfo指针内的bmiColors成员存储的是调色板数组
// 而lpBitsInfo->bmiHeader.biClrUsed拿到的就是调色板数组的长度
// 直接访问调色板数组下表为长度的元素,其实就是刚好越界1格调色板数组
// 又因为调色板数组后的数据就是实际位图数据,所以刚好越界之后拿到的就是指向实际数据的指针!
BYTE* lpBits = (BYTE*)&lpBitsInfo->bmiColors[lpBitsInfo->bmiHeader.biClrUsed];
操作图像
· 我们刚刚已经拿到了处理图像最重要的lpBitsInfo指针和lpBits指针,现在我们要开始对图像进行处理。
· 首先,给定一个图像内像素的坐标(i,j),如何拿到它对应的指针?
// 双重遍历以遍历每一个像素点
for (int i = 0; i < lpBitsInfo->bmiHeader.biHeight; i++) {
for (int j = 0; j < lpBitsInfo->bmiHeader.biWidth; j++) {
// 我们知道lpBits指针指向的是实际位图数据的第一个像素点的位置
// 那么,我们要做的就是从第(0,0)个像素点开始算,到(i,j)个像素点共有几个像素点
BYTE* pixel = lpBits + LineBytes * (h - 1 - i) + j;
}
}
· 可以通过下面这个图来帮助理解,注意,位图文件的数据是从下到上排列的,因此是h-1-i: