天天看點

圖像庫libjpeg-turbo編譯與實踐

作者:星隕

來源:

音視訊開發進階

在之前的文章中已經陸續介紹了stb_image,libpng的使用,相關連結如下:

簡單易用的圖像解碼庫介紹-stb_image 圖像庫libpng編譯與實踐

而今天的主題就是libjpeg-turbo。

它的官網位址如下:

https://libjpeg-turbo.org/

它的github位址如下:

https://github.com/libjpeg-turbo/libjpeg-turbo

編譯

在libjpeg-turbo的源碼中就已經有了如何編譯的BUILDING.md檔案,還是使用CMake進行編譯,大體方法和參數設定都大同小異了。

參考原始代碼的編譯代碼:

# Set these variables to suit your needs
# 設定交叉編譯的變量
NDK_PATH={full path to the NDK directory-- for example,
  /opt/android/android-ndk-r16b}
TOOLCHAIN={"gcc" or "clang"-- "gcc" must be used with NDK r16b and earlier,
  and "clang" must be used with NDK r17c and later}
ANDROID_VERSION={the minimum version of Android to support-- for example,
  "16", "19", etc.}
cd {build_directory}
cmake -G"Unix Makefiles" \
  -DANDROID_ABI=armeabi-v7a \
  -DANDROID_ARM_MODE=arm \
  -DANDROID_PLATFORM=android-${ANDROID_VERSION} \
  -DANDROID_TOOLCHAIN=${TOOLCHAIN} \
  -DCMAKE_ASM_FLAGS="--target=arm-linux-androideabi${ANDROID_VERSION}" \
  -DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
  [additional CMake flags] {source_directory}
make      

由于CMake跨平台編譯的特性,在進行交叉編譯時要設定很多相關參數,而是編譯的目标系統平台,交叉編譯工具鍊,NDK目錄等。

詳細的編譯腳本可以參考項目中的:

https://github.com/glumes/InstantGLSL/tree/master/libjpeg_turbo_source/build_script

該目錄下定義了編譯的armeabi,armeabi-v7a,arm64-v8a,x86,x86_64等平台的編譯腳本。修改一下相應檔案路徑,可以在MAC系統上編譯so了。

另外如果是在Android Studio中用CMake編譯,那麼,您會發現很少要設定那些參數,這是因為Android Studio中的CMake預設就設定好了那些參數。

是以還有一種更簡單的方式進行編譯,直接将libjpeg-turbo原始内容複制到Android Studio工程目錄的cpp檔案夾下,然後把app的build.gradle中cmake路徑改成libjpeg-turbo的CMakeLists.txt路徑,如下所示:

externalNativeBuild {
        cmake {
            path "src/main/cpp/libjpeg_turbo_source/CMakeLists.txt"
//            path "CMakeLists.txt"
        }
    }      

然後直接編譯,在build / intermediates / cmake路徑下依舊可以找到編譯好的so檔案。

以上兩種方式都可以實作libjpeg-turbo的編譯,看個人喜好了。而且這種庫一旦編譯好了,以後也很少去更改,一勞永逸~~~

實踐

在libjpeg-turbo的原始碼中有個example.txt檔案,詳細介紹了如何利用該庫進行圖檔壓縮和解壓縮。

基本上照着檔案内容看一遍就懂了,在這裡将會大概猜測下,并且會用另一個執行個體來示範,也就是之前常用的,擷取jpeg圖像檔案内容和上傳紋理。

壓縮

在Android中通過Java方法也可以實作Jpeg的檔案,因為可以就是基于libjpeg的。而libjpeg-turbo的壓縮速度會比Android原生的速度加快了。

Android中Jpeg檔案壓縮的方法如下:

compress(CompressFormat format, int quality, OutputStream stream)      

其中重要的參數就是quality,代表要壓縮的品質,而在libjpeg-turbo也會有這樣的參數要設定。

libjpeg-turbo的使用邏輯和libpng有點類似,首先都是要設定一個錯誤傳回點,并且有一個結構體來存儲資訊。

在libjpeg-turbo進行壓縮時,用到的結構體是

jpeg_compress_struct

,解壓則是

jpeg_decompress_struct

,其他名字上都有單詞的不同。

而在libpng中,建立結構體的方法

png_create_write_struct

png_create_read_struct

相配對,一個寫,一個讀。

使用libjpeg-turbo的主要步驟如下:

  1. 設定壓縮後的輸出方式,可以的是檔案的形式,也可以是記憶體資料格式
  2. 配置壓縮的相關設定項,尺寸壓縮後的圖像寬高,壓縮品質等
  3. 進行壓縮,逐行讀取資料源内容
  4. 壓縮結束,得到壓縮後的資料

對應到代碼的邏輯如下:

struct jpeg_compress_struct jpegCompressStruct;
    // 建立代表壓縮的結構體
    jpeg_create_compress(&jpegCompressStruct);
    // 檔案方式輸出 還有一種是記憶體方式
    jpeg_stdio_dest(&jpegCompressStruct, fp);
    // 設定壓縮的相關參數資訊
    jpegCompressStruct.image_width = w;
    jpegCompressStruct.image_height = h;
    jpegCompressStruct.arith_code = false;
    jpegCompressStruct.input_components = nComponent;
    // 設定解壓的顔色
    jpegCompressStruct.in_color_space = JCS_RGB;
    jpeg_set_defaults(&jpegCompressStruct);
    // 壓縮的品質
    jpegCompressStruct.optimize_coding = quality;
    jpeg_set_quality(&jpegCompressStruct, quality, true);      

首先是建立結構體jpeg_compress_struct,通過該結構體來完成壓縮資料輸出,配置壓縮選項操作。

壓縮資料輸出有兩種方式:

// 以檔案的方式
    jpeg_stdio_dest(j_compress_ptr cinfo, FILE *outfile);
    // 以記憶體的方式
    jpeg_mem_dest(j_compress_ptr cinfo, unsigned char **outbuffer,
                           unsigned long *outsize)      

另外,壓縮選項除了常見的寬高資訊,顔色類型,還有最重要的圖像品質參數,通過專門的方法進行設定。

jpeg_set_quality(j_compress_ptr cinfo, int quality,
                                boolean force_baseline)      

設定完要壓縮的相關資訊後,就可以開始壓縮了。

// 開始壓縮
    jpeg_start_compress(&jpegCompressStruct, true);
    // JSAMPROW 代表每行的資料
    JSAMPROW row_point[1];
    int row_stride = jpegCompressStruct.image_width * nComponent;
    while (jpegCompressStruct.next_scanline < jpegCompressStruct.image_height) {
        // data 參數就是要壓縮的資料源
        // 逐行讀取像素内容
        row_point[0] = &data[jpegCompressStruct.next_scanline * row_stride];
        // 寫入資料
        jpeg_write_scanlines(&jpegCompressStruct, row_point, 1);
    }
   // 完成壓縮 
   jpeg_finish_compress(&jpegCompressStruct);
   // 釋放相應的結構體
   jpeg_destroy_compress(&jpegCompressStruct);      

主要的代碼就在于而循環中。

next_scanline

某個一個狀态變量,需要逐行去掃描圖像内容并寫入,每次jpeg_write_scanlines方法之後,

next_scanline

就會逐漸增加,直到退出循環。

解壓縮

解壓和壓縮的代碼結構大緻相同了。

jpeg_create_decompress(&cinfo);
    // 設定資料源資料方式 這裡以檔案的方式,也可以以記憶體資料的方式
    jpeg_stdio_src(&cinfo, fp);
    // 讀取檔案資訊,比如寬高之類的
    jpeg_read_header(&cinfo, TRUE);      

其中jpeg_read_header方法可以擷取要解壓的檔案相關資訊。

接下來設定解壓的相關參數:

jpeg_start_decompress(&cinfo);
    unsigned long width = cinfo.output_width;
    unsigned long height = cinfo.output_height;
    unsigned short depth = cinfo.output_components;
    row_stride = cinfo.output_width * cinfo.output_components;      

定義相關的資料變量,來儲存解壓的資料。

// 儲存每行解壓的資料内容    
    JSAMPARRAY buffer;
    // 初始化
    buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
    // 儲存圖像的所有資料
    unsigned char *src_buff;
    // 初始化并置空
    src_buff = static_cast<unsigned char *>(malloc(width * height * depth));
    memset(src_buff, 0, sizeof(unsigned char) * width * height * depth);      

這裡使用的是

JSAMPARRAY

來儲存,在libjpeg-turbo中其實有多種結構來表示圖像資料類型,壓縮中用到的就是

JSAMPROW

/* Data structures for images (arrays of samples and of DCT coefficients).
 */
typedef unsigned char JSAMPLE;
typedef JSAMPLE *JSAMPROW;      /* ptr to one image row of pixel samples. */
typedef JSAMPROW *JSAMPARRAY;   /* ptr to some rows (a 2-D sample array) */
typedef JSAMPARRAY *JSAMPIMAGE; /* a 3-D sample array: top index is color */      

通過上述代碼不嚴重出,實際上

JSAMPROW

就是一維副本,而

JSAMPARRAY

就是二維數組。以下兩行代碼可以說是等價的:

JSAMPARRAY buffer = nullptr;
    JSAMPROW *row_pointer = nullptr;      

因為

JSAMPARRAY

的定義就是

JSAMPROW *

具體用哪個更好,要看調用方法需要的參數類型了,

jpeg_write_scanlines

jpeg_read_scanlines

這兩個方法需要的都是

JSAMPARRAY

類型。

另外,代碼中還聲明并

src_buff

初始化了變量,該變量就是用來表示解壓後的圖像資料。

具體解壓的邏輯也比較清楚了,逐行掃描圖像,用

buffer

變量去存儲圖像每行解壓的資料,然後把這個資料給到

src_buff

變量,如下代碼所示:

unsigned char *point = src_buff;
    while (cinfo.output_scanline < height) {
        jpeg_read_scanlines(&cinfo, buffer, 1);
        memcpy(point, *buffer, width * depth);
        point += width * depth;
    }
    jpeg_finish_decompress(&cinfo);
    jpeg_destroy_decompress(&cinfo);      

這裡用到了

point

這樣的臨時變量,實際上,這樣的用法在C ++開發中應該算比較常見的。

因為把

buffer

的資料傳到

src_buff

後,

src_buff

指針要移動到下一個點去接收資料,這樣一來,指針指向的位置就不是原始位置了,是以才需要臨時變量去做移動操作,保證

src_buff

指向的位置為起點。

最後,别忘了釋放相應的變量,做一些收尾工作,解壓就完成了。

jpeg釋出紋理渲染

說完了壓縮和解壓縮,最後以一個例子來實際應用,也是之前文章中常用的例子,通過libjpeg-turbo讀取jpeg檔案圖像内容并上傳紋理渲染。

// 封裝 jpeg 相關操作
    JpegHelper jpegHelper;
    // 讀取的圖像内容
    unsigned char *jpegData;
    int jpegSize;
    int jpegWidth;
    int jpegHeight;
    // 讀取操作
    jpegHelper.read_jpeg_file(filePath, &jpegData, &jpegSize, &jpegWidth, &jpegHeight);      

最終要擷取的就是

jpegData

圖像内容,并通過

glTexImage2D

等方法渲染到紋理上。

read_jpeg_file

方法的聲明如下:

int read_jpeg_file(const char *jpeg_file, unsigned char **rgb_buffer, 
                       int *size, int *width,int *height)      

rgb_buffer

參數的資料類型,既可以聲明成

unsigned char **

這樣的指針的位址,也可以聲明成引用

unsigned char *&

,大同小異。

相對具體的讀取操作,和上面的解壓縮過程大緻相同,就沒有重疊一遍了,可以檢視我的項目代碼實踐:

https://github.com/glumes/InstantGLSL

總結

至此,總結了常用的三種圖像庫的編譯和使用。

這三種圖像庫各有特點,要根據實際需要,選擇最合适的。但實際我們用到的無非就是圖像的讀取操作。讀取特定格式圖像的内容,或者将内容寫入特定格式檔案。

平時寫demo,追求簡單友善的,就用

stb_image

,對性能有要求的,針對特定格式的選擇

libpng

或者

libjpeg-turbo

安卓以及音視訊雜談
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。  
圖像庫libjpeg-turbo編譯與實踐