OpenGL中的畫素操作

孫群發表於2012-08-04

轉載自:http://bbs.pfan.cn/showtxt.asp?id=227694

注:關於OpenGL中畫素操作的更多內容可以參考《OpenGL程式設計指南第七版》第八章“繪製畫素、點陣圖、字型和影象”。

學過多媒體技術的朋友可能知道,計算機儲存圖象的方法通常有兩種:一是“向量圖”,一是“畫素圖”。向量圖儲存了圖象中每一幾何物體的位置、形狀、大小等資訊,在顯示圖象時,根據這些資訊計算得到完整的圖象。“畫素圖”是將完整的圖象縱橫分為若干的行、列,這些行列使得圖象被分割為很細小的分塊,每一分塊稱為畫素,儲存每一畫素的顏色也就儲存了整個圖象。
這兩種方法各有優缺點。“向量圖”在圖象進行放大、縮小時很方便,不會失真,但如果圖象很複雜,那麼就需要用非常多的幾何體,資料量和運算量都很龐大。“畫素圖”無論圖象多麼複雜,資料量和運算量都不會增加,但在進行放大、縮小等操作時,會產生失真的情況。
前面我們曾介紹瞭如何使用OpenGL來繪製幾何體,我們通過重複的繪製許多幾何體,可以繪製出一幅向量圖。那麼,應該如何繪製畫素圖呢?這就是我們今天要學習的內容了。

1、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 函式的引數說明
該函式總共有七個引數。前四個引數可以得到一個矩形,該矩形所包括的畫素都會被讀取出來。(第一、二個參數列示了矩形的左下角橫、縱座標,座標以視窗最左下角為零,最右上角為最大值;第三、四個參數列示了矩形的寬度和高度)
第五個參數列示讀取的內容,例如: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);
}

把這段程式碼複製到以前任何課程的樣例程式中,在繪製函式的最後呼叫grab函式,即可把圖象內容儲存為BMP檔案了。(在我寫這個教程的時候,不少地方都用這樣的程式碼進行截圖工作,這段程式碼一旦寫好,執行起來是很方便的。)


4、glDrawPixels的用法和舉例
glDrawPixels函式與glReadPixels函式相比,引數內容大致相同。它的第一、二、三、四個引數分別對應於glReadPixels函式的第三、四、五、六個引數,依次表示圖象寬度、圖象高度、畫素資料內容、畫素資料在記憶體中的格式。兩個函式的最後一個引數也是對應的,glReadPixels中表示畫素讀取後存放在記憶體中的位置,glDrawPixels則表示用於繪製的畫素資料在記憶體中的位置。
注意到glDrawPixels函式比glReadPixels函式少了兩個引數,這兩個引數在glReadPixels中分別是表示圖象的起始位置。在glDrawPixels中,不必顯式的指定繪製的位置,這是因為繪製的位置是由另一個函式glRasterPos*來指定的。glRasterPos*函式的引數與glVertex*類似,通過指定一個二維/三維/四維座標,OpenGL將自動計算出該座標對應的螢幕位置,並把該位置作為繪製畫素的起始位置。
很自然的,我們可以從BMP檔案中讀取畫素資料,並使用glDrawPixels繪製到螢幕上。我們選擇Windows XP預設的桌面背景Bliss.bmp作為繪製的內容(如果你使用的是Windows XP系統,很可能可以在硬碟中搜尋到這個檔案。當然你也可以使用其它BMP檔案來代替,只要它是24位的BMP檔案。注意需要修改程式碼開始部分的FileName的定義),先把該檔案複製一份放到正確的位置,我們在程式開始時,就讀取該檔案,從而獲得圖象的大小後,根據該大小來建立合適的OpenGL視窗,並繪製畫素。
繪製畫素本來是很簡單的過程,但是這個程式在骨架上與前面的各種示例程式稍有不同,所以我還是打算給出一份完整的程式碼。

#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函式有五個引數,第一、二個參數列示複製畫素來源的矩形的左下角座標,第三、四個參數列示複製畫素來源的舉行的寬度和高度,第五個引數通常使用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();
}


相關文章