作者:星隕
來源:
音視訊開發進階在之前的文章中已經陸續介紹了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的主要步驟如下:
- 設定壓縮後的輸出方式,可以的是檔案的形式,也可以是記憶體資料格式
- 配置壓縮的相關設定項,尺寸壓縮後的圖像寬高,壓縮品質等
- 進行壓縮,逐行讀取資料源内容
- 壓縮結束,得到壓縮後的資料
對應到代碼的邏輯如下:
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
安卓以及音視訊雜談 「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
