RaymondHuang
RaymondHuang
发布于 2023-10-13 / 547 阅读
0
3

BMP图像文件读取与显示

常见图像文件格式

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位整数。

· 各个参数的定义如下

参数

说明

bfType(Bitmap File Type)

指定文件类型,必须是0x424D,即字符串"BM",说明所有.bmp文件的最开始两个字节都是BM

bfSize(Bitmap File Size)

指定文件大小包括文件头本身的14个字节

bfReserved1,bfReserved2(Bitmap File Reserved 1/2)

保留字,不用考虑。

bfOffBits(Bitmap File Off Bits)

头文件到实际位图数据的偏移字节数,即文件头+信息头+调色板的长度之和。

· 第二部分——信息头,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个字节

· 各个参数的定义如下

参数

说明

biSize(Bitmap Info Size)

指定这个结构的长度,固定为40。

biWidth(Bitmap Info Width)

指定图像的宽度,单位为像素

biHeight(Bitmap Info Height)

指定图像的高度,单位为像素

biPlanes

必须是1,不用考虑。

biBitCount(Bitmap Info Bit Count)

指定表示颜色时要用到的位数,常用值为1(黑白两色),4(16色),8(256色),24(真彩)

biCompression

指定位图是否压缩。Windows的位图是可以压缩的,但不常用,指定为BI_RGB即可。

biSizeImage

位图数据占用的字节数。若biCompression为BI_RGB,则该项可以为0。

biXPelsPerMeter

指定目标设备的水平分辨率,单位是每米的像素个数。

biYPelsPerMeter

指定目标设备的垂直分辨率,单位同上。

biClrUsed(Bitmap Color Used)

指定本图像实际用到的颜色数,会决定调色板数组元素的个数,如果该值为0,则用到的颜色数为2的biBitCount次方

biClrImportant

指定本图像中重要的颜色数,设置为0即可,即所有颜色都重要。

· 第三部分——调色板,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值都是相同的,如:

索引

存储的RGB值

0

RGB(0,0,0)

1

RGB(1,1,1)

2

RGB(2,2,2)

......

......

255

RGB(255,255,255)

② 对于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
);

· 各个参数的定义如下:

参数

说明

hdc

句柄,是Windows中的概念,此处不需要关心

XDest

目标区域的X坐标

YDest

目标区域的Y坐标

nDestWidth

目标区域的宽度(像素数)

nDestHeight

目标区域的高度(像素数)

XSrc

源区域的X坐标

YSrc

源区域的Y坐标

nSrcWidth

源区域的宽度(像素数)

nSrcHeight

源区域的高度(像素数)

lpBits

指向实际位图数据指针

lpBitsInfo

指向信息头指针

iUsage

颜色使用选项,直接填DIB_RGB_COLORS即可

dwRop

直接填SRCCOPY即可

· 为什么会有目标区域和源区域的概念呢?

· 因为在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:


评论