一、 BMP文件格式簡單介紹 算法
BMP文件是一種像素文件,它保存了一幅圖象中全部的像素。這種文件格式能夠保存單色位圖、16色或256色索引模式像素圖、24位真彩色圖象,每種模式種單一像素的大小分別爲1/8字節,1/2字節,1字節和3字節。目前最多見的是256色BMP和24位色BMP。這種文件格式還定義了像素保存的幾種方法,包括不壓縮、RLE壓縮等。常見的BMP文件大可能是不壓縮的。
這裏爲了簡單起見,咱們僅討論24位色、不使用壓縮的BMP。(若是你使用Windows自帶的畫圖程序,很容易繪製出一個符合以上要求的BMP)
Windows所使用的BMP文件,在開始處有一個文件頭,大小爲54字節。保存了包括文件格式標識、顏色數、圖象大小、壓縮方式等信息,由於咱們僅討論24位色不壓縮的BMP,因此文件頭中的信息基本不須要注意,只有「大小」這一項對咱們比較有用。圖象的寬度和高度都是一個32位整數,在文件中的地址分別爲0x0012和0x0016,因而咱們可使用如下代碼來讀取圖象的大小信息: 數組
GLint width, height; // 使用OpenGL的GLint類型,它是32位的。
// 而C語言自己的int則不必定是32位的。
FILE* pFile;
// 在這裏進行「打開文件」的操做
fseek(pFile, 0x0012, SEEK_SET); // 移動到0x0012位置
fread(&width, sizeof(width), 1, pFile); // 讀取寬度
fseek(pFile, 0x0016, SEEK_SET); // 移動到0x0016位置
// 因爲上一句執行後本就應該在0x0016位置
// 因此這一句可省略
fread(&height, sizeof(height), 1, pFile); // 讀取高度
54個字節之後,若是是16色或256色BMP,則還有一個顏色表,但24位色BMP沒有這個,咱們這裏不考慮。接下來就是實際的像素數據了。24位色的BMP文件中,每三個字節表示一個像素的顏色。
注意,OpenGL一般使用RGB來表示顏色,但BMP文件則採用BGR,就是說,順序被反過來了。
另外須要注意的地方是:像素的數據量並不必定徹底等於圖象的高度乘以寬度乘以每一像素的字節數,而是可能略大於這個值。緣由是BMP文件採用了一種「對齊」的機制,每一行像素數據的長度若不是4的倍數,則填充一些數據使它是4的倍數。這樣一來,一個17*15的24位BMP大小就應該是834字節(每行17個像素,有51字節,補充爲52字節,乘以15獲得像素數據總長度780,再加上文件開始的54字節,獲得834字節)。分配內存時,必定要當心,不能直接使用「圖象的高度乘以寬度乘以每一像素的字節數」來計算分配空間的長度,不然有可能致使分配的內存空間長度不足,形成越界訪問,帶來各類嚴重後果。
一個很簡單的計算數據長度的方法以下:
int LineLength, TotalLength;
LineLength = ImageWidth * BytesPerPixel; // 每行數據長度大體爲圖象寬度乘以
// 每像素的字節數
while( LineLength % 4 != 0 ) // 修正LineLength使其爲4的倍數
++LineLenth;
TotalLength = LineLength * ImageHeight; // 數據總長 = 每行長度 * 圖象高度
這並非效率最高的方法,但因爲這個修正自己運算量並不大,使用頻率也不高,咱們就不須要再考慮更快的方法了。
2、簡單的OpenGL像素操做
OpenGL提供了簡潔的函數來操做像素:
glReadPixels:讀取一些像素。當前能夠簡單理解爲「把已經繪製好的像素(它可能已經被保存到顯卡的顯存中)讀取到內存」。
glDrawPixels:繪製一些像素。當前能夠簡單理解爲「把內存中一些數據做爲像素數據,進行繪製」。
glCopyPixels:複製一些像素。當前能夠簡單理解爲「把已經繪製好的像素從一個位置複製到另外一個位置」。雖然從功能上看,好象等價於先讀取像素再繪製像素,但實際上它不須要把已經繪製的像素(它可能已經被保存到顯卡的顯存中)轉換爲內存數據,而後再由內存數據進行從新的繪製,因此要比先讀取後繪製快不少。
這三個函數能夠完成簡單的像素讀取、繪製和複製任務,但實際上也能夠完成更復雜的任務。當前,咱們僅討論一些簡單的應用。因爲這幾個函數的參數數目比較多,下面咱們分別介紹。
3、glReadPixels的用法和舉例
3.1 函數的參數說明
該函數總共有七個參數。前四個參數能夠獲得一個矩形,該矩形所包括的像素都會被讀取出來。(第1、二個參數表示了矩形的左下角橫、縱座標,座標以窗口最左下角爲零,最右上角爲最大值;第3、四個參數表示了矩形的寬度和高度)
第五個參數表示讀取的內容,例如:GL_RGB就會依次讀取像素的紅、綠、藍三種數據,GL_RGBA則會依次讀取像素的紅、綠、藍、alpha四種數據,GL_RED則只讀取像素的紅色數據(相似的還有GL_GREEN,GL_BLUE,以及GL_ALPHA)。若是採用的不是RGBA顏色模式,而是採用顏色索引模式,則也可使用GL_COLOR_INDEX來讀取像素的顏色索引。目前僅須要知道這些,但實際上還能夠讀取其它內容,例如深度緩衝區的深度數據等。
第六個參數表示讀取的內容保存到內存時所使用的格式,例如:GL_UNSIGNED_BYTE會把各類數據保存爲GLubyte,GL_FLOAT會把各類數據保存爲GLfloat等。
第七個參數表示一個指針,像素數據被讀取後,將被保存到這個指針所表示的地址。注意,須要保證該地址有足夠的可使用的空間,以容納讀取的像素數據。例如一幅大小爲256*256的圖象,若是讀取其RGB數據,且每一數據被保存爲GLubyte,總大小就是:256*256*3 = 196608字節,即192千字節。若是是讀取RGBA數據,則總大小就是256*256*4 = 262144字節,即256千字節。
注意:glReadPixels其實是從緩衝區中讀取數據,若是使用了雙緩衝區,則默認是從正在顯示的緩衝(即前緩衝)中讀取,而繪製工做是默認繪製到後緩衝區的。所以,若是須要讀取已經繪製好的像素,每每須要先交換先後緩衝。
再看前面提到的BMP文件中兩個須要注意的地方:
3.2 解決OpenGL經常使用的RGB像素數據與BMP文件的BGR像素數據順序不一致問題
可使用一些代碼交換每一個像素的第一字節和第三字節,使得RGB的數據變成BGR的數據。固然也可使用另外的方式解決問題:新版本的OpenGL除了可使用GL_RGB讀取像素的紅、綠、藍數據外,也可使用GL_BGR按照相反的順序依次讀取像素的藍、綠、紅數據,這樣就與BMP文件格式相吻合了。即便你的gl/gl.h頭文件中沒有定義這個GL_BGR,也沒有關係,能夠嘗試使用GL_BGR_EXT。雖然有的OpenGL實現(尤爲是舊版本的實現)並不能使用GL_BGR_EXT,但我所知道的Windows環境下各類OpenGL實現都對GL_BGR提供了支持,畢竟Windows中各類表示顏色的數據幾乎都是使用BGR的順序,而非RGB的順序。這可能與IBM-PC的硬件設計有關。
3.3 消除BMP文件中「對齊」帶來的影響
實際上OpenGL也支持使用了這種「對齊」方式的像素數據。只要經過glPixelStore修改「像素保存時對齊的方式」就能夠了。像這樣:
int alignment = 4;
glPixelStorei(GL_UNPACK_ALIGNMENT, alignment);
第一個參數表示「設置像素的對齊值」,第二個參數表示實際設置爲多少。這裏像素能夠單字節對齊(實際上就是不使用對齊)、雙字節對齊(若是長度爲奇數,則再補一個字節)、四字節對齊(若是長度不是四的倍數,則補爲四的倍數)、八字節對齊。分別對應alignment的值爲1, 2, 4, 8。實際上,默認的值是4,正好與BMP文件的對齊方式相吻合。
glPixelStorei也能夠用於設置其它各類參數。但咱們這裏並不須要深刻討論了。
如今,咱們已經能夠把屏幕上的像素讀取到內存了,若是須要的話,咱們還能夠將內存中的數據保存到文件。正確的對照BMP文件格式,咱們的程序就能夠把屏幕中的圖象保存爲BMP文件,達到屏幕截圖的效果。
咱們並無詳細介紹BMP文件開頭的54個字節的全部內容,不過這無傷大雅。從一個正確的BMP文件中讀取前54個字節,修改其中的寬度和高度信息,就能夠獲得新的文件頭了。假設咱們先創建一個1*1大小的24位色BMP,文件名爲dummy.bmp,又假設新的BMP文件名稱爲grab.bmp。則能夠編寫以下代碼:
FILE* pOriginFile = fopen("dummy.bmp", "rb);
FILE* pGrabFile = fopen("grab.bmp", "wb");
char BMP_Header[54];
GLint width, height;
/* 先在這裏設置好圖象的寬度和高度,即width和height的值,並計算像素的總長度 */
// 讀取dummy.bmp中的頭54個字節到數組
fread(BMP_Header, sizeof(BMP_Header), 1, pOriginFile);
// 把數組內容寫入到新的BMP文件
fwrite(BMP_Header, sizeof(BMP_Header), 1, pGrabFile);
// 修改其中的大小信息
fseek(pGrabFile, 0x0012, SEEK_SET);
fwrite(&width, sizeof(width), 1, pGrabFile);
fwrite(&height, sizeof(height), 1, pGrabFile);
// 移動到文件末尾,開始寫入像素數據
fseek(pGrabFile, 0, SEEK_END);
/* 在這裏寫入像素數據到文件 */
fclose(pOriginFile);
fclose(pGrabFile); 數據結構
咱們給出完整的代碼,演示如何把整個窗口的圖象抓取出來並保存爲BMP文件。 函數
#define WindowWidth 400
#define WindowHeight 400
#include <stdio.h>
#include <stdlib.h>
/* 函數grab
* 抓取窗口中的像素
* 假設窗口寬度爲WindowWidth,高度爲WindowHeight
*/
#define BMP_Header_Length 54
void grab(void)
{
FILE* pDummyFile;
FILE* pWritingFile;
GLubyte* pPixelData;
GLubyte BMP_Header[BMP_Header_Length];
GLint i, j;
GLint PixelDataLength;
// 計算像素數據的實際長度
i = WindowWidth * 3; // 獲得每一行的像素數據長度
while( i%4 != 0 ) // 補充數據,直到i是的倍數
++i; // 原本還有更快的算法,
// 但這裏僅追求直觀,對速度沒有過高要求
PixelDataLength = i * WindowHeight;
// 分配內存和打開文件
pPixelData = (GLubyte*)malloc(PixelDataLength);
if( pPixelData == 0 )
exit(0);
pDummyFile = fopen("dummy.bmp", "rb");
if( pDummyFile == 0 )
exit(0);
pWritingFile = fopen("grab.bmp", "wb");
if( pWritingFile == 0 )
exit(0);
// 讀取像素
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glReadPixels(0, 0, WindowWidth, WindowHeight,
GL_BGR_EXT, GL_UNSIGNED_BYTE, pPixelData);
// 把dummy.bmp的文件頭複製爲新文件的文件頭
fread(BMP_Header, sizeof(BMP_Header), 1, pDummyFile);
fwrite(BMP_Header, sizeof(BMP_Header), 1, pWritingFile);
fseek(pWritingFile, 0x0012, SEEK_SET);
i = WindowWidth;
j = WindowHeight;
fwrite(&i, sizeof(i), 1, pWritingFile);
fwrite(&j, sizeof(j), 1, pWritingFile);
// 寫入像素數據
fseek(pWritingFile, 0, SEEK_END);
fwrite(pPixelData, PixelDataLength, 1, pWritingFile);
// 釋放內存和關閉文件
fclose(pDummyFile);
fclose(pWritingFile);
free(pPixelData);
} oop
把這段代碼複製到之前任何課程的樣例程序中,在繪製函數的最後調用grab函數,便可把圖象內容保存爲BMP文件了。(在我寫這個教程的時候,很多地方都用這樣的代碼進行截圖工做,這段代碼一旦寫好,運行起來是很方便的。)
4、glDrawPixels的用法和舉例
glDrawPixels函數與glReadPixels函數相比,參數內容大體相同。它的第1、2、3、四個參數分別對應於glReadPixels函數的第3、4、5、六個參數,依次表示圖象寬度、圖象高度、像素數據內容、像素數據在內存中的格式。兩個函數的最後一個參數也是對應的,glReadPixels中表示像素讀取後存放在內存中的位置,glDrawPixels則表示用於繪製的像素數據在內存中的位置。
注意到glDrawPixels函數比glReadPixels函數少了兩個參數,這兩個參數在glReadPixels中分別是表示圖象的起始位置。在glDrawPixels中,沒必要顯式的指定繪製的位置,這是由於繪製的位置是由另外一個函數glRasterPos*來指定的。glRasterPos*函數的參數與glVertex*相似,經過指定一個二維/三維/四維座標,OpenGL將自動計算出該座標對應的屏幕位置,並把該位置做爲繪製像素的起始位置。
很天然的,咱們能夠從BMP文件中讀取像素數據,並使用glDrawPixels繪製到屏幕上。咱們選擇Windows XP默認的桌面背景Bliss.bmp做爲繪製的內容(若是你使用的是Windows XP系統,極可能能夠在硬盤中搜索到這個文件。固然你也可使用其它BMP文件來代替,只要它是24位的BMP文件。注意須要修改代碼開始部分的FileName的定義),先把該文件複製一份放到正確的位置,咱們在程序開始時,就讀取該文件,從而得到圖象的大小後,根據該大小來建立合適的OpenGL窗口,並繪製像素。
繪製像素原本是很簡單的過程,可是這個程序在骨架上與前面的各類示例程序稍有不一樣,因此我仍是打算給出一份完整的代碼。 spa
#include <gl/glut.h>
#define FileName "Bliss.bmp"
static GLint ImageWidth;
static GLint ImageHeight;
static GLint PixelLength;
static GLubyte* PixelData;
#include <stdio.h>
#include <stdlib.h>
void display(void)
{
// 清除屏幕並沒必要要
// 每次繪製時,畫面都覆蓋整個屏幕
// 所以不管是否清除屏幕,結果都同樣
// glClear(GL_COLOR_BUFFER_BIT);
// 繪製像素
glDrawPixels(ImageWidth, ImageHeight,
GL_BGR_EXT, GL_UNSIGNED_BYTE, PixelData);
// 完成繪製
glutSwapBuffers();
}
int main(int argc, char* argv[])
{
// 打開文件
FILE* pFile = fopen("Bliss.bmp", "rb");
if( pFile == 0 )
exit(0);
// 讀取圖象的大小信息
fseek(pFile, 0x0012, SEEK_SET);
fread(&ImageWidth, sizeof(ImageWidth), 1, pFile);
fread(&ImageHeight, sizeof(ImageHeight), 1, pFile);
// 計算像素數據長度
PixelLength = ImageWidth * 3;
while( PixelLength % 4 != 0 )
++PixelLength;
PixelLength *= ImageHeight;
// 讀取像素數據
PixelData = (GLubyte*)malloc(PixelLength);
if( PixelData == 0 )
exit(0);
fseek(pFile, 54, SEEK_SET);
fread(PixelData, PixelLength, 1, pFile);
// 關閉文件
fclose(pFile);
// 初始化GLUT並運行
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowPosition(100, 100);
glutInitWindowSize(ImageWidth, ImageHeight);
glutCreateWindow(FileName);
glutDisplayFunc(&display);
glutMainLoop();
// 釋放內存
// 實際上,glutMainLoop函數永遠不會返回,這裏也永遠不會到達
// 這裏寫釋放內存只是出於一種我的習慣
// 不用擔憂內存沒法釋放。在程序結束時操做系統會自動回收全部內存
free(PixelData);
return 0;
} 操作系統
這裏僅僅是一個簡單的顯示24位BMP圖象的程序,若是讀者對BMP文件格式比較熟悉,也能夠寫出適用於各類BMP圖象的顯示程序,在像素處理時,它們所使用的方法是相似的。
OpenGL在繪製像素以前,能夠對像素進行若干處理。最經常使用的可能就是對整個像素圖象進行放大/縮小。使用glPixelZoom來設置放大/縮小的係數,該函數有兩個參數,分別是水平方向係數和垂直方向係數。例如設置glPixelZoom(0.5f, 0.8f);則表示水平方向變爲原來的50%大小,而垂直方向變爲原來的80%大小。咱們甚至可使用負的係數,使得整個圖象進行水平方向或垂直方向的翻轉(默認像素從左繪製到右,但翻轉後將從右繪製到左。默認像素從下繪製到上,但翻轉後將從上繪製到下。所以,glRasterPos*函數設置的「開始位置」不必定就是矩形的左下角)。
5、glCopyPixels的用法和舉例
從效果上看,glCopyPixels進行像素複製的操做,等價於把像素讀取到內存,再從內存繪製到另外一個區域,所以能夠經過glReadPixels和glDrawPixels組合來實現複製像素的功能。然而咱們知道,像素數據一般數據量很大,例如一幅1024*768的圖象,若是使用24位BGR方式表示,則須要至少1024*768*3字節,即2.25兆字節。這麼多的數據要進行一次讀操做和一次寫操做,而且由於在glReadPixels和glDrawPixels中設置的數據格式不一樣,極可能涉及到數據格式的轉換。這對CPU無疑是一個不小的負擔。使用glCopyPixels直接從像素數據複製出新的像素數據,避免了多餘的數據的格式轉換,而且也可能減小一些數據複製操做(由於數據可能直接由顯卡負責複製,不須要通過主內存),所以效率比較高。
glCopyPixels函數也經過glRasterPos*系列函數來設置繪製的位置,由於不須要涉及到主內存,因此不須要指定數據在內存中的格式,也不須要使用任何指針。
glCopyPixels函數有五個參數,第1、二個參數表示複製像素來源的矩形的左下角座標,第3、四個參數表示複製像素來源的舉行的寬度和高度,第五個參數一般使用GL_COLOR,表示複製像素的顏色,但也能夠是GL_DEPTH或GL_STENCIL,分別表示複製深度緩衝數據或模板緩衝數據。
值得一提的是,glDrawPixels和glReadPixels中設置的各類操做,例如glPixelZoom等,在glCopyPixels函數中一樣有效。
下面看一個簡單的例子,繪製一個三角形後,複製像素,並同時進行水平和垂直方向的翻轉,而後縮小爲原來的一半,並繪製。繪製完畢後,調用前面的grab函數,將屏幕中全部內容保存爲grab.bmp。其中WindowWidth和WindowHeight是表示窗口寬度和高度的常量。 設計
void display(void)
{
// 清除屏幕
glClear(GL_COLOR_BUFFER_BIT);
// 繪製
glBegin(GL_TRIANGLES);
glColor3f(1.0f, 0.0f, 0.0f); glVertex2f(0.0f, 0.0f);
glColor3f(0.0f, 1.0f, 0.0f); glVertex2f(1.0f, 0.0f);
glColor3f(0.0f, 0.0f, 1.0f); glVertex2f(0.5f, 1.0f);
glEnd();
glPixelZoom(-0.5f, -0.5f);
glRasterPos2i(1, 1);
glCopyPixels(WindowWidth/2, WindowHeight/2,
WindowWidth/2, WindowHeight/2, GL_COLOR);
// 完成繪製,並抓取圖象保存爲BMP文件
glutSwapBuffers();
grab();
} 指針
小結:
本課結合Windows系統常見的BMP圖象格式,簡單介紹了OpenGL的像素處理功能。包括使用glReadPixels讀取像素、glDrawPixels繪製像素、glCopyPixels複製像素。
本課僅介紹了像素處理的一些簡單應用,但相信你們已經能夠體會到,圍繞這三個像素處理函數,還存在一些「外圍」函數,好比glPixelStore*,glRasterPos*,以及glPixelZoom等。咱們僅使用了這些函數的一少部分功能。
本課內容並很少,例子足夠豐富,三個像素處理函數都有例子,你們能夠結合例子來體會。 orm
附錄(其它位色的BMP文件簡介):
BMP文件組成
BMP文件由文件頭、位圖信息頭、顏色信息和圖形數據四部分組成。
BMP文件頭
BMP文件頭數據結構含有BMP文件的類型、文件大小和位圖起始位置等信息。
其結構定義以下:
typedef struct tagBITMAPFILEHEADER
{
WORDbfType; // 位圖文件的類型,必須爲BM
DWORD bfSize; // 位圖文件的大小,以字節爲單位
WORDbfReserved1; // 位圖文件保留字,必須爲0
WORDbfReserved2; // 位圖文件保留字,必須爲0
DWORD bfOffBits; // 位圖數據的起始位置,以相對於位圖
// 文件頭的偏移量表示,以字節爲單位
} BITMAPFILEHEADER;
位圖信息頭
BMP位圖信息頭數據用於說明位圖的尺寸等信息。
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; // 本結構所佔用字節數
LONGbiWidth; // 位圖的寬度,以像素爲單位
LONGbiHeight; // 位圖的高度,以像素爲單位
WORD biPlanes; // 目標設備的級別,必須爲1
WORD biBitCount// 每一個像素所需的位數,必須是1(雙色),
// 4(16色),8(256色)或24(真彩色)之一
DWORD biCompression; // 位圖壓縮類型,必須是 0(不壓縮),
// 1(BI_RLE8壓縮類型)或2(BI_RLE4壓縮類型)之一
DWORD biSizeImage; // 位圖的大小,以字節爲單位
LONGbiXPelsPerMeter; // 位圖水平分辨率,每米像素數
LONGbiYPelsPerMeter; // 位圖垂直分辨率,每米像素數
DWORD biClrUsed;// 位圖實際使用的顏色表中的顏色數
DWORD biClrImportant;// 位圖顯示過程當中重要的顏色數
} BITMAPINFOHEADER;
顏色表
顏色表用於說明位圖中的顏色,它有若干個表項,每個表項是一個RGBQUAD類型的結構,定義一種顏色。RGBQUAD結構的定義以下:
typedef struct tagRGBQUAD {
BYTErgbBlue;// 藍色的亮度(值範圍爲0-255)
BYTErgbGreen; // 綠色的亮度(值範圍爲0-255)
BYTErgbRed; // 紅色的亮度(值範圍爲0-255)
BYTErgbReserved;// 保留,必須爲0
} RGBQUAD;
顏色表中RGBQUAD結構數據的個數有biBitCount來肯定:
當biBitCount=1,4,8時,分別有2,16,256個表項;
當biBitCount=24時,沒有顏色表項。
位圖信息頭和顏色表組成位圖信息,BITMAPINFO結構定義以下:
typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader; // 位圖信息頭
RGBQUAD bmiColors[1]; // 顏色表
} BITMAPINFO;
位圖數據
位圖數據記錄了位圖的每個像素值,記錄順序是在掃描行內是從左到右,掃描行之間是從下到上。位圖的一個像素值所佔的字節數:
當biBitCount=1時,8個像素佔1個字節;
當biBitCount=4時,2個像素佔1個字節;
當biBitCount=8時,1個像素佔1個字節;
當biBitCount=24時,1個像素佔3個字節;
Windows規定一個掃描行所佔的字節數必須是
4的倍數(即以long爲單位),不足的以0填充,
一個掃描行所佔的字節數計算方法:
DataSizePerLine= (biWidth* biBitCount+31)/8;
// 一個掃描行所佔的字節數
DataSizePerLine= DataSizePerLine/4*4; // 字節數必須是4的倍數
位圖數據的大小(不壓縮狀況下):
DataSize= DataSizePerLine* biHeight;