BMP 和 JPEG
作者: 闕榮文
時間: 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位色的像素流.
對于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位及以上色深的位圖不需要調色板了.
2.1. BMP 文件格式和DIB
DIB就是"設備無關位圖"的意思,我們可以理解為一個像素數組,這是編程時我們需要處理的數據,非常簡單,就是一個定長數組,如果是一個24位的DIB數據,那么在編程時就可以認為是一個 BYTE dibBuffer[], dibBuffer[0],dibBuffer[1],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 文件,版本號,文件長度等,最重要的時文件頭結構中的 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 就可以了,具體細節不用理會.
3. 什么是 JPEG
JPEG是 DIB 數據的一種編碼規則,前面我們提到 BMP 文件,直接把 DIB 數組 dibBuffer[] 直接寫到文件中,所以BMP文件是原始的,無損失的保存了內存中的圖像數據.如果用某種算法把 dibBuffer 數組編碼壓縮,那么我們也許就沒必要把整個 dibBuffer (通常是一個很大的數組) 直接寫入文件中,從而大大節省磁盤空間. JPEG 就是這樣一種算法.
4. libjpeg
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 復制到你的工程中就算配置完成了.
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.
JPEG庫是不知道調色板之類的東西的,它只是很單純的把輸入的 DIB 像素流壓縮輸出為一個更短的輸出數據流.所以對于包含了調色板的 BMP 文件,由于 DIB 數組內保存的是調色板的索引號而并不是顏色值,在提交給 JPEG 庫之前需要根據調色板查表構造一個真正的包含顏色信息的 DIB 像素流,這樣 JPEG 庫才能正常工作.
=======================================
我之前的的博客地址是 http://blog.csdn.net/querw 還有一些我以前寫的文章.實在受不了他們網站的各種問題,決定改變陣地.
====================================================================================================
附錄: 截取windows桌面,并保存為 .jpg 文件
1 #include "stdafx.h"
2 #include <tchar.h>
3
4 extern "C"
5 {
6 #include "jpeglib.h"
7 }
8
9 int save_screen_to_jpeg(const char* filename, int quality)
10 {
11 /*
12 * 把屏幕內容保存為一個 HBITMAP DDB
13 */
14 HDC hScrnDC = CreateDC(_T("DISPLAY"), NULL, NULL, NULL);
15 HDC hMemDC = CreateCompatibleDC(hScrnDC);
16
17 // 獲取屏幕分辨率
18 int xScrn = GetDeviceCaps(hScrnDC, HORZRES);
19 int yScrn = GetDeviceCaps(hScrnDC, VERTRES);
20
21 // 創建位圖,并選中
22 HBITMAP hScrnBmp = CreateCompatibleBitmap(hScrnDC, xScrn, yScrn);
23 SelectObject(hMemDC, hScrnBmp);
24
25 // 復制屏幕內容
26 BitBlt(hMemDC, 0, 0, xScrn, yScrn, hScrnDC, 0, 0, SRCCOPY);
27
28 // 現在得到了一個 HBITMAP DDB - hScrnBmp
29
30 /*
31 * 通過 hScrnBmp DDB 取得 DIB 數據
32 */
33 // 獲取色深 JPG 只能處理 24 位色,所以不管當前系統設置的色深是多少,我們都要求 GetDIBits 函數返回 24 位的 DIB 數據,同時也不需要調色板
34 //int colorDeepBits = GetDeviceCaps(hScrnBmp, BITSPIXEL);
35 //if(colorDeepBits > 24) colorDeepBits = 24;
36 int colorDeepBits = 24;
37
38 // 每行像素占用的字節數,每行要對齊4字節.
39 int imageRowSize = (xScrn * colorDeepBits + 31) / 32 * 4;
40
41 // 分配 DIB 數組
42 unsigned char* dibBuffer = new unsigned char[imageRowSize * yScrn];
43 assert(dibBuffer);
44 memset(dibBuffer, 0, imageRowSize * yScrn); // 清零是個好習慣
45
46 // 填充 BMP 信息頭
47 BITMAPINFO bmi = {0};
48 bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
49 bmi.bmiHeader.biWidth = xScrn;
50 bmi.bmiHeader.biHeight = yScrn * -1; // JPG 壓縮需要正序的 DIB 像素流,所以要負數.
51 bmi.bmiHeader.biPlanes = 1;
52 bmi.bmiHeader.biBitCount = colorDeepBits;
53 bmi.bmiHeader.biCompression = BI_RGB;
54
55 // 獲取 DIB 像素數組(DIB_RGB_COLORS 表示獲取 RGB 值而不是調色板索引,當然24位位圖也沒有調色板)
56 int gdiRet = GetDIBits(hMemDC, hScrnBmp, 0, yScrn, dibBuffer, &bmi, DIB_RGB_COLORS);
57 assert(gdiRet == yScrn);
58 assert(bmi.bmiHeader.biSizeImage == imageRowSize * yScrn);
59
60 // DIB 數據已經獲取,所有的 GDI 對象可以釋放了.
61 DeleteDC(hScrnDC);
62 DeleteDC(hMemDC);
63 DeleteObject(hScrnBmp);
64
65 /*
66 * 把 DIB 數據壓縮為 JPG 數據,用 example.c 中的代碼
67 */
68
69 // DIB 中顏色的存放順序是 BGR, 而 JPG 要求的順序是 RGB, 所以要交換 R 和 B.
70 // 由于有行對齊因素,所以逐行處理
71 for(int row = 0; row < yScrn; ++row)
72 {
73 unsigned char* rowData = dibBuffer + imageRowSize * row;
74 for(int col = 0; col < xScrn * 3; col += 3)
75 {
76 unsigned char swap = rowData[col];
77 rowData[col] = rowData[col + 2];
78 rowData[col + 2] = swap;
79 }
80 }
81
82 //把位圖數據壓縮為 jpeg
83 struct jpeg_compress_struct cinfo;
84 struct jpeg_error_mgr jerr;
85 FILE * outfile; /* target file */
86 JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */
87 int row_stride; /* physical row width in image buffer */
88 int image_width = xScrn;
89 int image_height = yScrn;
90 JSAMPLE* image_buffer = dibBuffer; // DIB buffer
91 int image_buffer_len = imageRowSize * image_height; // DIB buffer 長度
92
93 if(fopen_s(&outfile, filename, "wb"))
94 //if ((outfile = fopen_s(filename, "wb")) == NULL)
95 {
96 fprintf(stderr, "can't open %s\n", filename);
97 assert(0);
98 }
99 else
100 {
101 /* Step 1: allocate and initialize JPEG compression object */
102 cinfo.err = jpeg_std_error(&jerr);
103
104 /* Now we can initialize the JPEG compression object. */
105 jpeg_create_compress(&cinfo);
106
107 /* Step 2: specify data destination (eg, a file) */
108 /* Note: steps 2 and 3 can be done in either order. */
109 jpeg_stdio_dest(&cinfo, outfile);
110
111 /* Step 3: set parameters for compression */
112
113 /* First we supply a description of the input image.
114 * Four fields of the cinfo struct must be filled in:
115 */
116 cinfo.image_width = image_width; /* image width and height, in pixels */
117 cinfo.image_height = image_height;
118 cinfo.input_components = 3; /* # of color components per pixel */ // 因為DIB數據是24位的,所以每個像素占用3個字節
119 cinfo.in_color_space = JCS_RGB; /* colorspace of input image */
120 /* Now use the library's routine to set default compression parameters.
121 * (You must set at least cinfo.in_color_space before calling this,
122 * since the defaults depend on the source color space.)
123 */
124 jpeg_set_defaults(&cinfo);
125 /* Now you can set any non-default parameters you wish to.
126 * Here we just illustrate the use of quality (quantization table) scaling:
127 */
128 jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);
129
130 /* Step 4: Start compressor */
131
132 /* TRUE ensures that we will write a complete interchange-JPEG file.
133 * Pass TRUE unless you are very sure of what you're doing.
134 */
135 jpeg_start_compress(&cinfo, TRUE);
136
137 /* Step 5: while (scan lines remain to be written) */
138 /* jpeg_write_scanlines(
); */
139
140 /* Here we use the library's state variable cinfo.next_scanline as the
141 * loop counter, so that we don't have to keep track ourselves.
142 * To keep things simple, we pass one scanline per call; you can pass
143 * more if you wish, though.
144 */
145 row_stride = imageRowSize;
146 while (cinfo.next_scanline < cinfo.image_height)
147 {
148 /* jpeg_write_scanlines expects an array of pointers to scanlines.
149 * Here the array is only one element long, but you could pass
150 * more than one scanline at a time if that's more convenient.
151 */
152 row_pointer[0] = &image_buffer[cinfo.next_scanline * row_stride];
153 //row_pointer[0] = &image_buffer[image_buffer_len - (cinfo.next_scanline + 1) * row_stride];
154 (void)jpeg_write_scanlines(&cinfo, row_pointer, 1);
155 }
156
157 /* Step 6: Finish compression */
158 jpeg_finish_compress(&cinfo);
159
160 /* After finish_compress, we can close the output file. */
161 fclose(outfile);
162
163 /* Step 7: release JPEG compression object */
164 /* This is an important step since it will release a good deal of memory. */
165 jpeg_destroy_compress(&cinfo);
166 }
167
168 // 釋放 DIB 數組
169 delete []dibBuffer;
170 return 0;
171 }
172