在Linux控制檯下顯示JPEG影像

jackie_gnu發表於2011-12-02
 在Linux控制檯下顯示JPEG影像



(陳雲川 ybc2084@163.com UESTC,成都)



1、引言
通常情況下,在Linux控制檯下是無法檢視影像檔案的,要想檢視影像檔案,比如要檢視JPEG格式的影像檔案,可能必須啟動X-Windows,通過GNOME或者KDE之類的桌面管理器提供的影像檢視工具檢視圖片內容。那麼,能不能有辦法在控制檯下面簡單地瀏覽影像內容呢。實際上,這是完全可以的。在Linux下有一個名為zgv的看圖軟體就是工作在控制檯下的。不過,由於它所使用的底層圖形庫svgalib已經是一個比較“古老”的圖形庫了,所以現在知道zgv的人並不是很多,用的人就更少了。

目前Linux上的底層圖形支援通常是由Framebuffer提供的,因此,作者試圖在本文中說明如何通過Framebuffer和libjpeg在控制檯上顯示JPEG影像。需要說明的是,本文中所編寫的程式fv並非zgv的替代品,而只是一個出於驗證想法的簡單程式(fv的含義是Framebuffer Vision)。本文將先對Framebuffer和libjpeg的程式設計做一個簡略的說明,然後再給出程式fv的具體實現。

2、Framebuffer介紹
Framebuffer在Linux中是作為裝置來實現的,它是對圖形硬體的一種抽象[1],代表著顯示卡中的幀緩衝區(Framebuffer)。通過Framebuffer裝置,上層軟體可以通過一個良好定義的軟體介面訪問圖形硬體,而不需要關心底層圖形硬體是如何工作的,比如,上層軟體不用關心應該如何讀寫顯示卡暫存器,也不需要知道顯示卡中的幀緩衝區從什麼地址開始,所有這些工作都由Framebuffer去處理,上層軟體只需要集中精力在自己要做的事情上就是了。

Framebuffer的優點在於它是一種低階的通用裝置,而且能夠跨平臺工作,比如Framebuffer既可以工作在x86平臺上,也能工作在PPC平臺上,甚至也能工作在m68k和SPARC等平臺上,在很多嵌入式裝置上Framebuffer也能正常工作。諸如Minigui之類的GUI軟體包也傾向於採用Framebuffer作為硬體抽象層(HAL)。

從使用者的角度來看,Framebuffer裝置與其它裝置並沒有什麼不同。Framebuffer裝置位於/dev下,通常裝置名為fb*,這裡*的取值從0到31。對於常見的計算機系統而言,32個Framebuffer裝置已經綽綽有餘了(至少作者還沒有看到過有32個監視器的計算機)。最常用到的Framebuffer裝置是/dev/fb0。通常,使用Framebuffer的程式通過環境變數FRAMEBUFFER來取得要使用的Framebuffer裝置,環境變數FRAMEBUFFER通常被設定為”/dev/fb0”。

從程式設計師的角度來看,Framebuffer裝置其實就是一個檔案而已,可以像對待普通檔案那樣讀寫Framebuffer裝置檔案,可以通過mmap()將其對映到記憶體中,也可以通過ioctl()讀取或者設定其引數,等等。最常見的用法是將Framebuffer裝置通過mmap()對映到記憶體中,這樣可以大大提高IO效率。

要在PC平臺上啟用Framebuffer,首先必須要核心支援,這通常需要重新編譯核心。另外,還需要修改核心啟動引數。在作者的系統上,為了啟用Framebuffer,需要將/boot/grub/menu.lst中的下面這一行:

kernel /boot/vmlinuz-2.4.20-8 ro root=LABEL=/1

修改為

kernel /boot/vmlinuz-2.4.20-8 ro root=LABEL=/1 vga=0x0314

即增加了vga=0x0314這樣一個核心啟動引數。這個核心啟動參數列示的意思是:Framebuffer裝置的大小是800x600,顏色深度是16bits/畫素。

下面,來了解一下如何程式設計使用Framebuffer裝置。由於對Framebuffer裝置的讀寫應該是不緩衝的,但是標準IO庫預設是要進行緩衝的,因此通常不使用標準IO庫讀寫Framebuffer裝置,而是直接通過read()、write()或者mmap()等系統呼叫來完成與Framebuffer有關的IO操作。又由於mmap()能夠大大降低IO的開銷,因此與Framebuffer裝置有關的IO通常都是通過mmap()系統呼叫來完成的。mmap()的函式原型如下(Linux系統上的定義):

#include <sys/mman.h>

#ifdef _POSIX_MAPPED_FILES



void  *  mmap(void *start, size_t length, int prot , int flags, int fd,

       off_t offset);



int munmap(void *start, size_t length);



#endif

系統呼叫mmap()用來實現記憶體對映IO。所謂記憶體對映IO,是指將一個磁碟檔案的內容與記憶體中的一個空間相對映。當從這個對映記憶體空間中取資料時,就相當於從檔案中讀取相應的位元組,而當向此對映記憶體空間寫入資料時,就相當於向磁碟檔案中寫入資料。這就是記憶體對映IO的含義。

具體到對mmap()而言,當呼叫成功時,返回值就是與磁碟檔案建立了對映關係的記憶體空間的起始地址,當呼叫失敗時,mmap()的返回值是-1。第一個引數start通常設定為0,表示由系統選擇對映記憶體空間;第二個引數length指定了要對映的位元組數;第三個引數指明瞭對映記憶體空間的保護屬性,對於Framebuffer通常將其設定為PROT_READ | PROT_WRITE,表示既可讀也可寫;第四個引數flags指明瞭影響對映記憶體空間行為的標誌,對於Framebuffer程式設計而言,要將flags設定為MAP_SHARED,表明當向對映記憶體空間寫入資料時,將資料寫入磁碟檔案中;第五個引數fd是要對映的檔案的檔案描述符;第六個引數offset指明瞭要對映的位元組在檔案中的偏移量。

如果mmap()呼叫成功,就可以在程式中對得到的對映記憶體空間進行讀寫操作了。所有的讀寫都將由作業系統核心轉換成IO操作。

在使用完對映記憶體空間之後,應當將其釋放,這是通過munmap()系統呼叫完成的。munmap()的第一個引數是對映記憶體空間的起始地址,第二個引數length是對映記憶體空間的長度,單位為位元組。如果釋放成功,munmap()返回0,否則返回-1。

如果應用程式需要知道Framebuffer裝置的相關引數,必須通過ioctl()系統呼叫來完成。在標頭檔案<linux/fb.h>中定義了所有的ioctl命令字,不過,最常用的ioctl命令字是下面這兩個:FBIOGET_FSCREENINFO和FBIOGET_VSCREENINFO,前者返回與Framebuffer有關的固定的資訊,比如圖形硬體上實際的幀快取空間的大小、能否硬體加速等資訊;而後者返回的是與Framebuffer有關的可變資訊,之所以可變,是因為對同樣的圖形硬體,可以工作在不同的模式下,簡單來講,一個支援1024x768x24圖形模式的硬體通常也能工作在800x600x16的圖形模式下,可變的資訊就是指Framebuffer的長度、寬度以及顏色深度等資訊。這兩個命令字相關的結構體有兩個:struct fb_fix_screeninfo和struct fb_var_screeninfo,這兩個結構體都比較大,前者用於儲存Framebuffer裝置的固定資訊,後者用於儲存Framebuffer裝置的可變資訊。在呼叫ioctl()的時候,要用到這兩個結構體。應用程式中通常要用到struct fb_var_screeninfo的下面這幾個欄位:xres、yres、bits_per_pixel,分別表示x軸的解析度、y軸的解析度以及每畫素的顏色深度(顏色深度的單位為bit/pixel),其型別定義都是無符號32位整型數。

3、libjpeg函式庫介紹
JPEG是CCITT和ISO定義的一種連續色調影像壓縮標準[2]。JPEG是一種有損影像壓縮標準,其基礎是DCT變換(離散餘弦變換)。JPEG影像的壓縮過程分為三步:DCT計算,量化,變長編碼分配。儘管CCITT定義了JPEG影像壓縮標準,但是卻並沒有為JPEG定義標準的檔案格式。這導致了現實世界中出現了各種各樣的JPEG檔案格式,而一種被稱為JFIF的JPEG檔案格式逐漸成為JPEG檔案格式的主流。

libjpeg是一個被廣泛使用的JPEG壓縮/解壓縮函式庫(至少在Unix類系統下是廣泛使用的),它能夠讀寫JFIF格式的JPEG影像檔案,通常這類檔案是以.jpg或者.jpeg為字尾名的。通過libjpeg庫,應用程式可以每次從JPEG壓縮影像中讀取一個或多個掃描線(scanline,所謂掃描線,是指由一行畫素點構成的一條影像線條),而諸如顏色空間轉換、降取樣/增取樣、顏色量化之類的工作則都由libjpeg去完成了。

要使用libjpeg,需要讀者對數字影像的基本知識有初步的瞭解。對於libjpeg而言,影像資料是一個二維的畫素矩陣。對於彩色影像,每個畫素通常用三個分量表示,即R(Red)、G(Green)、B(Blue)三個分量,每個分量用一個位元組表示,因此每個分量的取值範圍從0到255;對於灰度影像,每個畫素通常用一個分量表示,一個分量同樣由一個位元組表示,取值範圍從0到255。由於本文不會涉及到索引影像,因此這裡略去對索引影像的說明。

在libjpeg中,影像資料是以掃描線的形式存放的。每一條掃描線由一行畫素點構成,畫素點沿著掃描線從左到右依次排列。對於彩色影像,每個分量由三個位元組組成,因此這三個位元組以R、G、B的順序構成掃描線上的一個畫素點。一個典型的掃描線形式如下:

       R,G,B,R,G,B,R,G,B,…

通過libjpeg解壓出來的影像資料也是以掃描線的形式存放的。

在本文中,只涉及到JPEG的解壓縮,因此只對libjpeg的解壓過程進行說明,有關libjpeg的壓縮過程和其它高階用法,請參考[3]。一般地,libjpeg的解壓過程如下:

1、分配並初始化一個JPEG解壓物件(本文中將JPEG解壓物件命名為cinfo):

    struct jpeg_decompress_struct cinfo;

    struct jpeg_error_mgr jerr;

    ...

    cinfo.err = jpeg_std_error(&jerr);

    jpeg_create_decompress(&cinfo);

2、指定要解壓縮的影像檔案:

    FILE * infile;

    ...

    if ((infile = fopen(filename, "rb")) == NULL) {

        fprintf(stderr, "can't open %s/n", filename);

        exit(1);

    }

    jpeg_stdio_src(&cinfo, infile);

3、呼叫jpeg_read_header()獲取影像資訊:

    jpeg_read_header(&cinfo, TRUE);

4、這是一個可選步驟,用於設定JPEG解壓縮物件cinfo的一些引數,本文可忽略;

5、呼叫jpeg_start_decompress()開始解壓過程:

    jpeg_start_decompress(&cinfo);

呼叫jpeg_start_decompress()函式之後,JPEG解壓縮物件cinfo中的下面這幾個欄位將會比較有用:

l output_width                這是影像輸出的寬度

l output_height                這是影像輸出的高度

l output_components              每個畫素的分量數,也即位元組數

這是因為在呼叫jpeg_start_decompress()之後往往需要為解壓後的掃描線上的所有畫素點分配儲存空間,這個空間的大小可以通過output_width * output_componets確定,而要讀取的掃描線的總數為output_height行。

6、讀取一行或者多行掃描線資料並處理,通常的程式碼是這樣的:

       while (cinfo.output_scanline < cinfo.ouput_height) {

              jpeg_read_scanlines();

              /* deal with scanlines */

       }

對掃描線的讀取是按照從上到下的順序進行的,也就是說影像最上方的掃描線最先被jpeg_read_scanlines()讀入儲存空間中,緊接著是第二個掃描線,最後是影像底邊的掃描線被讀入儲存空間中。

7、呼叫jpeg_finish_decompress()完成解壓過程:

    jpeg_finish_decompress(&cinfo);

8、呼叫jpeg_destroy_decompress()釋放JPEG解壓物件cinfo:

    jpeg_destroy_decompress(&cinfo);

以上就是通過libjpeg函式解壓JPEG壓縮影像的基本過程,由於本文不涉及libjpeg的高階特性和用法,因此,上面的介紹對於說明本文中要用到的libjpeg的功能已經足夠了。

另外一個需要說明地方是:由於作者所用的Framebuffer裝置的顏色深度為16位,顏色格式為5-6-5格式——即R(紅色)在16bit中佔據高5位,G(綠色)在16bit中佔據中間6位,B(藍色)在16bit中佔據低5位;而libjpeg解壓出來的影像資料為24位RGB格式,因此必須進行轉換。對於24位的RGB,每個位元組表示一個顏色分量,因此轉換的方式為:對於R位元組,右移3位,對於G位元組,右移2位,對於B位元組,右移3位,然後將右移得到的值拼接起來,就得到了16位的顏色值。在後面的程式中,將把24位的顏色稱為RGB888,而把16位顏色值稱為RGB565,這種命名方式可能不太規範,不過無論如何,在本文中就這樣稱呼了。另外,讀者可能會想到,上面這種直接將顏色分量的低位丟棄的方式不是會導致影像細節的丟失嗎?比如,對於24位顏色的R位元組,假如原來低3位的值在0~7之間均勻分佈,轉換之後,所有這低3位的值全部都變成了0,這就是顏色細節的丟失。為了處理這個問題,可以採用誤差擴散演算法或者抖動演算法來完成顏色轉換。為了保持程式的簡單,本文中仍然只採用上面提到的最簡單的轉換方式——而且事實表明,這樣做得到的結果並不差。

相關文章