BMP 和 JPEG
作者: 阙荣文
时间: 2016.5.29
时间: 2016.5.29
1. 什么是 BMP
BMP 格式是最简单,最直观的位图数据格式.它的思想非常朴素:
用若干个位来保存一个像素的信息,由若干个像素组成一个像素流来表达一张图片.
通常我们会用1位(可以保存2种颜色,0 表示一种颜色,1表示另一种,不一定是黑白,也可以是蓝绿,总之是2种),4位 - 16种颜色,8位 - 256种颜色,16位 - 65535种颜色,24位 - 2^24种颜色,32位 - 前24位和24位位图一样可以保存2^24种颜色,最后8位用来保存这个像素的灰度(也就是这个像素的明暗程度),可以表示256种灰度.注意,目前的显示器在硬件上通常只能支持24位色,而且 libjpeg 也只能处理最多24位色的像素流.
BMP 格式是最简单,最直观的位图数据格式.它的思想非常朴素:
用若干个位来保存一个像素的信息,由若干个像素组成一个像素流来表达一张图片.
通常我们会用1位(可以保存2种颜色,0 表示一种颜色,1表示另一种,不一定是黑白,也可以是蓝绿,总之是2种),4位 - 16种颜色,8位 - 256种颜色,16位 - 65535种颜色,24位 - 2^24种颜色,32位 - 前24位和24位位图一样可以保存2^24种颜色,最后8位用来保存这个像素的灰度(也就是这个像素的明暗程度),可以表示256种灰度.注意,目前的显示器在硬件上通常只能支持24位色,而且 libjpeg 也只能处理最多24位色的像素流.
对于24位以下的BMP,可以引入一个"调色板"来增强图片的表达能力,以8位,256色位图作为例子:
在8位位图中,每个像素的信息用一个字节存储,那么 DIB 数据就是一个 BYTE dibBuffer[] 数组, dibBuffer[0] 表示第一个像素的颜色,以此类推. 我们知道颜色是由RGB 3个分量组合而成的,编程中用 RGBQUAD 结构表示,那么8位除以3,每个分量只能用2位来存储(不使用编码压缩RLE的前提下),实际能够表现的颜色非常有限.现在我们引入一个长度为256的 RGBQUAD 类型的数组: RGBQUAD colorTable[256],我们在 DBI 数组 dibBuffer[] 中不再直接存放的每个像素的颜色值,而是存放该颜色值在 colorTable 中的索引,这样就可以充分利用 dibBuffer 中的每一位的存储空间.这个 "colorTable" 就是 Windows 中调色板的概念. 知道了这些,就可以理解为什么24位及以上色深的位图不需要调色板了.
在8位位图中,每个像素的信息用一个字节存储,那么 DIB 数据就是一个 BYTE dibBuffer[] 数组, dibBuffer[0] 表示第一个像素的颜色,以此类推. 我们知道颜色是由RGB 3个分量组合而成的,编程中用 RGBQUAD 结构表示,那么8位除以3,每个分量只能用2位来存储(不使用编码压缩RLE的前提下),实际能够表现的颜色非常有限.现在我们引入一个长度为256的 RGBQUAD 类型的数组: RGBQUAD colorTable[256],我们在 DBI 数组 dibBuffer[] 中不再直接存放的每个像素的颜色值,而是存放该颜色值在 colorTable 中的索引,这样就可以充分利用 dibBuffer 中的每一位的存储空间.这个 "colorTable" 就是 Windows 中调色板的概念. 知道了这些,就可以理解为什么24位及以上色深的位图不需要调色板了.
2.1. BMP 文件格式和DIB
DIB就是"设备无关位图"的意思,我们可以理解为一个像素数组,这是编程时我们需要处理的数据,非常简单,就是一个定长数组,如果是一个24位的DIB数据,那么在编程时就可以认为是一个 BYTE dibBuffer[], dibBuffer[0],dibBuffer[2],dibBuffer[2]表示第一个像素的 RGB 值(实际上是 BGR), dibBuffer[3],[4],[5] 表示第二个像素的 RGB 值,以此类推.当然我们不能直接把这个 dibBuffer 数组写到磁盘作为 BMP 文件,缺少图片的调色板,宽,高等信息,所以我们需要一个特定的格式来存储 DIB 像素流.
DIB就是"设备无关位图"的意思,我们可以理解为一个像素数组,这是编程时我们需要处理的数据,非常简单,就是一个定长数组,如果是一个24位的DIB数据,那么在编程时就可以认为是一个 BYTE dibBuffer[], dibBuffer[0],dibBuffer[2],dibBuffer[2]表示第一个像素的 RGB 值(实际上是 BGR), dibBuffer[3],[4],[5] 表示第二个像素的 RGB 值,以此类推.当然我们不能直接把这个 dibBuffer 数组写到磁盘作为 BMP 文件,缺少图片的调色板,宽,高等信息,所以我们需要一个特定的格式来存储 DIB 像素流.
BMP文件格式就是把DIB像素流存储到磁盘是需要遵循的相关约定. 关于BMP文件格式的详细说明在网上可以找到很多,比如这篇说的就很清楚: http://blog.csdn.net/lanbing510/article/details/8176231
从编程的角度来看,一个BMP文件是可以表述为以下结构:
typedef struct tagBITMAP_FILE
{
BITMAPFILEHEADER bitmapheader;
BITMAPINFOHEADER bitmapinfoheader;
PALETTEENTRY palette[n]; // 调色板数据(可选,由BITMAPFILEHEADER::bOffBits计算 n 的值)
UCHAR *dibBuffer; // DIB 数据数组
} BITMAP_FILE;
BITMAPFILEHEADER, BITMAPINFOHEADER, PALETTEENTRY 结构的详细信息可以在 MSDN 中查到.
从编程的角度来看,一个BMP文件是可以表述为以下结构:
typedef struct tagBITMAP_FILE
{
BITMAPFILEHEADER bitmapheader;
BITMAPINFOHEADER bitmapinfoheader;
PALETTEENTRY palette[n]; // 调色板数据(可选,由BITMAPFILEHEADER::bOffBits计算 n 的值)
UCHAR *dibBuffer; // DIB 数据数组
} BITMAP_FILE;
BITMAPFILEHEADER, BITMAPINFOHEADER, PALETTEENTRY 结构的详细信息可以在 MSDN 中查到.
用自然语言简单描述一下:
文件头 - 固定长度,表示这个文件是一个 BMP 文件,版本号,文件长度等,最重要的时文件头结构中的 bfOffBits 字段,它表示 DIB 像素流数据在文件中的偏移位置,编程时,我们打开一个 BMP 文件,先读取固定长度的文件头,在根据这个字段就可以构造前面说的调色板数组 RGBQUAD colorTable[] 和 DIB 数组 BYTE dibBuffer[] 了.
BMP信息头 - 固定长度,存储位图的宽高等信息,需要注意的字段 biHeight, 如果它是正数则表示像素流的信息是倒序存储的,即位图的底下一行的像素存储在前;如果它是负数则表示像素流的信息是正序存储的,位图的第一行像素存储在 dibBuffer 开头.
调色板数组 - 可选,用文件头中的偏移地址减去文件头和信息头的长度就是调色板数组的长度.
DIB像素流 - 就是 dibBuffer[] 数组.
特别要注意的一点是,在实际编程中, 24位 DIB 数据的存放顺序是 BGR 即 dibBuffer[0] 存放的是最后一行的第一个像素的 B 分量, dibBuffer[1] 是 G 分量, dibBuffer[2] 是 R 分量, 而 JPG 压缩时要求输入顺序是 RGB, 所以把 dibBuffer 提供给 JPEG 压缩器前需要处理一下, dibBuffer[i] 和 dibBuffer[i + 2] 交换,否则得到的 JPG 图像颜色是不对的.
文件头 - 固定长度,表示这个文件是一个 BMP 文件,版本号,文件长度等,最重要的时文件头结构中的 bfOffBits 字段,它表示 DIB 像素流数据在文件中的偏移位置,编程时,我们打开一个 BMP 文件,先读取固定长度的文件头,在根据这个字段就可以构造前面说的调色板数组 RGBQUAD colorTable[] 和 DIB 数组 BYTE dibBuffer[] 了.
BMP信息头 - 固定长度,存储位图的宽高等信息,需要注意的字段 biHeight, 如果它是正数则表示像素流的信息是倒序存储的,即位图的底下一行的像素存储在前;如果它是负数则表示像素流的信息是正序存储的,位图的第一行像素存储在 dibBuffer 开头.
调色板数组 - 可选,用文件头中的偏移地址减去文件头和信息头的长度就是调色板数组的长度.
DIB像素流 - 就是 dibBuffer[] 数组.
特别要注意的一点是,在实际编程中, 24位 DIB 数据的存放顺序是 BGR 即 dibBuffer[0] 存放的是最后一行的第一个像素的 B 分量, dibBuffer[1] 是 G 分量, dibBuffer[2] 是 R 分量, 而 JPG 压缩时要求输入顺序是 RGB, 所以把 dibBuffer 提供给 JPEG 压缩器前需要处理一下, dibBuffer[i] 和 dibBuffer[i + 2] 交换,否则得到的 JPG 图像颜色是不对的.
2.2. DDB
DDB 是"设备相关位图"的意思,把 DIB 数据写入设备之后,设备在内部会把 DIB 数据处理为内部数据格式, Windows GDI 中用 HBITMAP 表述一个 DDB,我们只需要调用相关的 API 就可以了,具体细节不用理会.
DDB 是"设备相关位图"的意思,把 DIB 数据写入设备之后,设备在内部会把 DIB 数据处理为内部数据格式, Windows GDI 中用 HBITMAP 表述一个 DDB,我们只需要调用相关的 API 就可以了,具体细节不用理会.
3. 什么是 JPEG
JPEG是 DIB 数据的一种编码规则,前面我们提到 BMP 文件,直接把 DIB 数组 dibBuffer[] 直接写到文件中,所以BMP文件是原始的,无损失的保存了内存中的图像数据.如果用某种算法把 dibBuffer 数组编码压缩,那么我们也许就没必要把整个 dibBuffer (通常是一个很大的数组) 直接写入文件中,从而大大节省磁盘空间. JPEG 就是这样一种算法.
JPEG是 DIB 数据的一种编码规则,前面我们提到 BMP 文件,直接把 DIB 数组 dibBuffer[] 直接写到文件中,所以BMP文件是原始的,无损失的保存了内存中的图像数据.如果用某种算法把 dibBuffer 数组编码压缩,那么我们也许就没必要把整个 dibBuffer (通常是一个很大的数组) 直接写入文件中,从而大大节省磁盘空间. JPEG 就是这样一种算法.
4. libjpeg
C语言实现的 JPEG 库,官网地址: http://www.ijg.org/
C语言实现的 JPEG 库,官网地址: http://www.ijg.org/
4.1 编译
我写这篇文章的时候 JPEG 库的版本是 jpeg-9b,从官网上下载源码 jpegsr9b.zip 解压后,启动Visual Studio,进入命令行模式,切换到 jpeg 源码目录,输入: nmake /f makefile.vc 就会看到 jpeg.sln - VS工程文件出现了,用Visual Studio 打开编译即可.
如果执行 nmake 命令时提示找不到 win32.mak,就编辑一下 makefile.vc 把第12行 !include <win32.mak> 注释掉就可以,其实 nmake /f makefile.vc 并不是真正编译,这是重命名了几个文件而已.
编译完成后得到: jpeg.lib 这就是你需要的库文件了,再把 jconfig.h, jerror.h, jinclude.h, jmorecfg.h, jpeglib.h 复制到你的工程中就算配置完成了.
我写这篇文章的时候 JPEG 库的版本是 jpeg-9b,从官网上下载源码 jpegsr9b.zip 解压后,启动Visual Studio,进入命令行模式,切换到 jpeg 源码目录,输入: nmake /f makefile.vc 就会看到 jpeg.sln - VS工程文件出现了,用Visual Studio 打开编译即可.
如果执行 nmake 命令时提示找不到 win32.mak,就编辑一下 makefile.vc 把第12行 !include <win32.mak> 注释掉就可以,其实 nmake /f makefile.vc 并不是真正编译,这是重命名了几个文件而已.
编译完成后得到: jpeg.lib 这就是你需要的库文件了,再把 jconfig.h, jerror.h, jinclude.h, jmorecfg.h, jpeglib.h 复制到你的工程中就算配置完成了.
4.2 example.c
libjpeg 的使用实例在源码包中的 example.c 文件里, 我们只要把 write_JPEG_file / read_JPEG_file 两个函数看明白就可以应付大多数应用了.
4.3 内存 JPG 压缩解压缩及其它
example.c 中的实例是使用文件io的, 用 jpeg_mem_src / jpeg_mem_dest 函数代替 jpeg_stdio_src / jpeg_stdio_dest 就可以实现内存io了.
libjpeg 的使用实例在源码包中的 example.c 文件里, 我们只要把 write_JPEG_file / read_JPEG_file 两个函数看明白就可以应付大多数应用了.
4.3 内存 JPG 压缩解压缩及其它
example.c 中的实例是使用文件io的, 用 jpeg_mem_src / jpeg_mem_dest 函数代替 jpeg_stdio_src / jpeg_stdio_dest 就可以实现内存io了.
JPEG库是不知道调色板之类的东西的,它只是很单纯的把输入的 DIB 像素流压缩输出为一个更短的输出数据流.所以对于包含了调色板的 BMP 文件,由于 DIB 数组内保存的是调色板的索引号而并不是颜色值,在提交给 JPEG 库之前需要根据调色板查表构造一个真正的包含颜色信息的 DIB 像素流,这样 JPEG 库才能正常工作.
====================================================================================================
附录: 截取windows桌面,并保存为 .jpg 文件
附录: 截取windows桌面,并保存为 .jpg 文件
int save_screen_to_jpeg(const char* filename, int quality)
{/** 把屏幕内容保存为一个 HBITMAP DDB*/HDC hScrnDC = CreateDC(_T("DISPLAY"), NULL, NULL, NULL);HDC hMemDC = CreateCompatibleDC(hScrnDC);// 获取屏幕分辨率int xScrn = GetDeviceCaps(hScrnDC, HORZRES);int yScrn = GetDeviceCaps(hScrnDC, VERTRES);// 创建位图,并选中HBITMAP hScrnBmp = CreateCompatibleBitmap(hScrnDC, xScrn, yScrn);SelectObject(hMemDC, hScrnBmp);// 复制屏幕内容BitBlt(hMemDC, 0, 0, xScrn, yScrn, hScrnDC, 0, 0, SRCCOPY);// 现在得到了一个 HBITMAP DDB - hScrnBmp/** 通过 hScrnBmp DDB 取得 DIB 数据*/// 获取色深 JPG 只能处理 24 位色,所以不管当前系统设置的色深是多少,我们都要求 GetDIBits 函数返回 24 位的 DIB 数据,同时也不需要调色板//int colorDeepBits = GetDeviceCaps(hScrnBmp, BITSPIXEL);//if(colorDeepBits > 24) colorDeepBits = 24;int colorDeepBits = 24;// 每行像素占用的字节数,每行要对齐4字节.int imageRowSize = (xScrn * colorDeepBits + 31) / 32 * 4; // 分配 DIB 数组unsigned char* dibBuffer = new unsigned char[imageRowSize * yScrn];assert(dibBuffer);memset(dibBuffer, 0, imageRowSize * yScrn); // 清零是个好习惯 // 填充 BMP 信息头BITMAPINFO bmi = {0};bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);bmi.bmiHeader.biWidth = xScrn;bmi.bmiHeader.biHeight = yScrn * -1; // JPG 压缩需要正序的 DIB 像素流,所以要负数.bmi.bmiHeader.biPlanes = 1;bmi.bmiHeader.biBitCount = colorDeepBits;bmi.bmiHeader.biCompression = BI_RGB;// 获取 DIB 像素数组(DIB_RGB_COLORS 表示获取 RGB 值而不是调色板索引,当然24位位图也没有调色板)int gdiRet = GetDIBits(hMemDC, hScrnBmp, 0, yScrn, dibBuffer, &bmi, DIB_RGB_COLORS);assert(gdiRet == yScrn);assert(bmi.bmiHeader.biSizeImage == imageRowSize * yScrn);// DIB 数据已经获取,所有的 GDI 对象可以释放了.DeleteDC(hScrnDC);DeleteDC(hMemDC);DeleteObject(hScrnBmp);/** 把 DIB 数据压缩为 JPG 数据,用 example.c 中的代码*/// DIB 中颜色的存放顺序是 BGR, 而 JPG 要求的顺序是 RGB, 所以要交换 R 和 B.// 由于有行对齐因素,所以逐行处理for(int row = 0; row < yScrn; ++row){unsigned char* rowData = dibBuffer + imageRowSize * row;for(int col = 0; col < xScrn * 3; col += 3){unsigned char swap = rowData[col];rowData[col] = rowData[col + 2];rowData[col + 2] = swap;}}//把位图数据压缩为 jpegstruct jpeg_compress_struct cinfo;struct jpeg_error_mgr jerr;FILE * outfile; /* target file */JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */int row_stride; /* physical row width in image buffer */int image_width = xScrn;int image_height = yScrn;JSAMPLE* image_buffer = dibBuffer; // DIB bufferint image_buffer_len = imageRowSize * image_height; // DIB buffer 长度if(fopen_s(&outfile, filename, "wb"))//if ((outfile = fopen_s(filename, "wb")) == NULL) {fprintf(stderr, "can't open %s\n", filename);assert(0);}else{/* Step 1: allocate and initialize JPEG compression object */cinfo.err = jpeg_std_error(&jerr);/* Now we can initialize the JPEG compression object. */jpeg_create_compress(&cinfo);/* Step 2: specify data destination (eg, a file) *//* Note: steps 2 and 3 can be done in either order. */jpeg_stdio_dest(&cinfo, outfile);/* Step 3: set parameters for compression *//* First we supply a description of the input image.* Four fields of the cinfo struct must be filled in:*/cinfo.image_width = image_width; /* image width and height, in pixels */cinfo.image_height = image_height;cinfo.input_components = 3; /* # of color components per pixel */ // 因为DIB数据是24位的,所以每个像素占用3个字节cinfo.in_color_space = JCS_RGB; /* colorspace of input image *//* Now use the library's routine to set default compression parameters.* (You must set at least cinfo.in_color_space before calling this,* since the defaults depend on the source color space.)*/jpeg_set_defaults(&cinfo);/* Now you can set any non-default parameters you wish to.* Here we just illustrate the use of quality (quantization table) scaling:*/jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);/* Step 4: Start compressor *//* TRUE ensures that we will write a complete interchange-JPEG file.* Pass TRUE unless you are very sure of what you're doing.*/jpeg_start_compress(&cinfo, TRUE);/* Step 5: while (scan lines remain to be written) *//* jpeg_write_scanlines(...); *//* Here we use the library's state variable cinfo.next_scanline as the* loop counter, so that we don't have to keep track ourselves.* To keep things simple, we pass one scanline per call; you can pass* more if you wish, though.*/row_stride = imageRowSize;while (cinfo.next_scanline < cinfo.image_height) {/* jpeg_write_scanlines expects an array of pointers to scanlines.* Here the array is only one element long, but you could pass* more than one scanline at a time if that's more convenient.*/row_pointer[0] = &image_buffer[cinfo.next_scanline * row_stride];//row_pointer[0] = &image_buffer[image_buffer_len - (cinfo.next_scanline + 1) * row_stride];(void)jpeg_write_scanlines(&cinfo, row_pointer, 1);}/* Step 6: Finish compression */jpeg_finish_compress(&cinfo);/* After finish_compress, we can close the output file. */fclose(outfile);/* Step 7: release JPEG compression object *//* This is an important step since it will release a good deal of memory. */jpeg_destroy_compress(&cinfo);}// 释放 DIB 数组delete []dibBuffer;return 0;
}
PS: 最近一年半在忙一个项目,一直没时间更新博客,也没有回答网友们的提问,非常抱歉.大多数问题都是百度一下就可以处理的,只能说抱歉了.
截屏保存为JPG其实也是我在项目中用到的一个小功能,因为不涉及什么商业上的东西,就贴出来分享一下.
以后有机会我会把项目中的一些功能分解成可以复用的代码,大家交流一下.