在Linux控制檯下使用libjpeg顯示JPEG圖像在framebuffer上

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,這就是顏色細節的丟失。爲了處理這個問題,可以採用誤差擴散算法或者抖動算法來完成顏色轉換。爲了保持程序的簡單,本文中仍然只採用上面提到的最簡單的轉換方式——而且事實表明,這樣做得到的結果並不差。

4、程序實現
以下就是程序的完整實現,鑑於已經對Framebuffer和libjpeg的編程接口做了說明,以下不再對代碼進行說明:

/*

 * $Id: fv.c

 * $Desp: draw jpeg to framebuffer

 * $Author: rockins

 * $Date: Wed Jan  3 20:15:49 CST 2007

 */

#include <stdio.h>

#include <stdlib.h>

#include <fcntl.h>

#include <linux/fb.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <sys/mman.h>

#include <jpeglib.h>

#include <jerror.h>

#define    FB_DEV  "/dev/fb0"

/***************** function declaration ******************/

void            usage(char *msg);

unsigned short  RGB888toRGB565(unsigned char red,

                                                    unsigned char green, unsigned char blue);

int             fb_open(char *fb_device);

int             fb_close(int fd);

int             fb_stat(int fd, int *width, int *height, int *depth);

void           *fb_mmap(int fd, unsigned int screensize);

int             fb_munmap(void *start, size_t length);

int             fb_pixel(void *fbmem, int width, int height,

                                           int x, int y, unsigned short color);

/************ function implementation ********************/

int

main(int argc, char *argv[])

{

       /*

        * declaration for jpeg decompression

        */

       struct jpeg_decompress_struct cinfo;

       struct jpeg_error_mgr jerr;

       FILE           *infile;

       unsigned char  *buffer;

       /*

        * declaration for framebuffer device

        */

       int             fbdev;

       char           *fb_device;

       unsigned char  *fbmem;

       unsigned int    screensize;

       unsigned int    fb_width;

       unsigned int    fb_height;

       unsigned int    fb_depth;

       unsigned int    x;

       unsigned int    y;

       /*

        * check auguments

        */

       if (argc != 2) {

              usage("insuffient auguments");

              exit(-1);

       }

       /*

        * open framebuffer device

        */

       if ((fb_device = getenv("FRAMEBUFFER")) == NULL)

              fb_device = FB_DEV;

       fbdev = fb_open(fb_device);

       /*

        * get status of framebuffer device

        */

       fb_stat(fbdev, &fb_width, &fb_height, &fb_depth);

       /*

        * map framebuffer device to shared memory

        */

       screensize = fb_width * fb_height * fb_depth / 8;

       fbmem = fb_mmap(fbdev, screensize);

       /*

        * open input jpeg file

        */

       if ((infile = fopen(argv[1], "rb")) == NULL) {

              fprintf(stderr, "open %s failed/n", argv[1]);

              exit(-1);

       }

       /*

        * init jpeg decompress object error handler

        */

       cinfo.err = jpeg_std_error(&jerr);

       jpeg_create_decompress(&cinfo);

       /*

        * bind jpeg decompress object to infile

        */

       jpeg_stdio_src(&cinfo, infile);

       /*

        * read jpeg header

        */

       jpeg_read_header(&cinfo, TRUE);

       /*

        * decompress process.

        * note: after jpeg_start_decompress() is called

        * the dimension infomation will be known,

        * so allocate memory buffer for scanline immediately

        */

       jpeg_start_decompress(&cinfo);

       if ((cinfo.output_width > fb_width) ||

              (cinfo.output_height > fb_height)) {

              printf("too large JPEG file,cannot display/n");

              return (-1);

       }

       buffer = (unsigned char *) malloc(cinfo.output_width *

                                                                 cinfo.output_components);

       y = 0;

       while (cinfo.output_scanline < cinfo.output_height) {

              jpeg_read_scanlines(&cinfo, &buffer, 1);

              if (fb_depth == 16) {

                     unsigned short  color;

                     for (x = 0; x < cinfo.output_width; x++) {

                            color = RGB888toRGB565(buffer[x * 3],

                                          buffer[x * 3 + 1], buffer[x * 3 + 2]);

                            fb_pixel(fbmem, fb_width, fb_height, x, y, color);

                     }

              } else if (fb_depth == 24) {

                     memcpy((unsigned char *) fbmem + y * fb_width * 3,

                               buffer, cinfo.output_width * cinfo.output_components);

              }

              y++;                                   // next scanline

       }

       /*

        * finish decompress, destroy decompress object

        */

       jpeg_finish_decompress(&cinfo);

       jpeg_destroy_decompress(&cinfo);

       /*

        * release memory buffer

        */

       free(buffer);

       /*

        * close jpeg inputing file

        */

       fclose(infile);

       /*

        * unmap framebuffer's shared memory

        */

       fb_munmap(fbmem, screensize);

       /*

        * close framebuffer device

        */

       fb_close(fbdev);

       return (0);

}

void

usage(char *msg)

{

       fprintf(stderr, "%s/n", msg);

       printf("Usage: fv some-jpeg-file.jpg/n");

}

/*

 * convert 24bit RGB888 to 16bit RGB565 color format

 */

unsigned short

RGB888toRGB565(unsigned char red, unsigned char green, unsigned char blue)

{

       unsigned short  B = (blue >> 3) & 0x001F;

       unsigned short  G = ((green >> 2) << 5) & 0x07E0;

       unsigned short  R = ((red >> 3) << 11) & 0xF800;

       return (unsigned short) (R | G | B);

}

/*

 * open framebuffer device.

 * return positive file descriptor if success,

 * else return -1.

 */

int

fb_open(char *fb_device)

{

       int             fd;

       if ((fd = open(fb_device, O_RDWR)) < 0) {

              perror(__func__);

              return (-1);

       }

       return (fd);

}

/*

 * get framebuffer's width,height,and depth.

 * return 0 if success, else return -1.

 */

int

fb_stat(int fd, int *width, int *height, int *depth)

{

       struct fb_fix_screeninfo fb_finfo;

       struct fb_var_screeninfo fb_vinfo;

       if (ioctl(fd, FBIOGET_FSCREENINFO, &fb_finfo)) {

              perror(__func__);

              return (-1);

       }

       if (ioctl(fd, FBIOGET_VSCREENINFO, &fb_vinfo)) {

              perror(__func__);

              return (-1);

       }

       *width = fb_vinfo.xres;

       *height = fb_vinfo.yres;

       *depth = fb_vinfo.bits_per_pixel;

       return (0);

}

/*

 * map shared memory to framebuffer device.

 * return maped memory if success,

 * else return -1, as mmap dose.

 */

void           *

fb_mmap(int fd, unsigned int screensize)

{

       caddr_t         fbmem;

       if ((fbmem = mmap(0, screensize, PROT_READ | PROT_WRITE,

                                     MAP_SHARED, fd, 0)) == MAP_FAILED) {

              perror(__func__);

              return (void *) (-1);

       }

       return (fbmem);

}

/*

 * unmap map memory for framebuffer device.

 */

int

fb_munmap(void *start, size_t length)

{

       return (munmap(start, length));

}

/*

 * close framebuffer device

 */

int

fb_close(int fd)

{

       return (close(fd));

}

/*

 * display a pixel on the framebuffer device.

 * fbmem is the starting memory of framebuffer,

 * width and height are dimension of framebuffer,

 * x and y are the coordinates to display,

 * color is the pixel's color value.

 * return 0 if success, otherwise return -1.

 */

int

fb_pixel(void *fbmem, int width, int height,

               int x, int y, unsigned short color)

{

       if ((x > width) || (y > height))

              return (-1);

       unsigned short *dst = ((unsigned short *) fbmem + y * width + x);

       *dst = color;

       return (0);

}

5、測試結果
圖1是對fv的一個測試結果,測試用的圖像名爲FreeBSD.jpg。

圖1 命令./fv FreeBSD.jpg的執行結果

圖2是對fv的第二個測試結果,測試用的圖像名爲loveatharvard.jpg。

圖2 命令./fv loveatharvard.jpg的執行結果

圖3是對fv的第三個測試結果,測試用的圖像名爲xinbai.jpg。

圖3 命令./fv xinbai.jpg的執行結果

6、結論
上述測試結果表明,通過Framebuffer在控制檯顯示JPEG圖像是能夠實現的,並且效果能夠接受。但是,程序fv目前只能接受JPEG格式的圖像,並且圖像大小不能超出屏幕大小範圍,比如,對於作者測試時所用的Framebuffer設置而言,大小超過800x600的圖像是無法顯示的。顯然,可以通過圖像縮放來顯示超出屏幕大小的JPEG圖像。其次,也可以讓fv能夠識別更多格式的圖像,比如可以通過libpng識別並顯示png格式的圖像。最後,fv還可以考慮採用誤差擴散算法或者抖動算法來完成顏色轉換,以減小顏色丟失。


本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/yruilin/archive/2009/01/17/3808715.aspx

發佈了1 篇原創文章 · 獲贊 1 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章