JPEG( Joint Photographic Experts Group)是一種圖像壓縮標(biāo)準(zhǔn), 也是目前使用最廣泛的圖片壓縮技術(shù), 圖片之所以要壓縮, 原因肯定是占用空間太大, 如果不壓縮的話, 對于800萬像素的手機, 一個RGB圖片文件占用空間為24M(3264*2448*3), 如果是YUV420格式, 也要占用12M(3264*2448*1.5), 而一張高質(zhì)量JPEG格式只占用 3M左右的空間, 可見壓縮對于圖片存儲和傳輸都有非常重要的意義. 本文主要講在Android系統(tǒng)中有哪些方法將YUV或者RGB圖片轉(zhuǎn)為JPEG, 其中主要分為以下幾種類型的JPEG編碼:
- QCOM(高通)平臺JPEG硬件編碼
- MTK平臺JPEG硬件編碼
- Android系統(tǒng)JPEG軟件編解碼
- 其他軟件編解碼
QCOM(高通)平臺硬件JPEG編碼
JPEG硬件編碼是指芯片中針對JPEG編碼有特殊的硬件設(shè)計, 可以加速編碼速度, 我在高通msm8937平臺測試過, 對于分辨率為3264x2448大小的圖片, 編碼質(zhì)量為 90左右的情況下, 硬件編碼只需 150ms左右, 而軟件編碼則需600ms左右(數(shù)據(jù)不一定準(zhǔn)確, 但大體上差不多), 可以看到硬件編碼速度是軟件的好幾倍, 對于Camera相關(guān)應(yīng)用來說,軟件編碼這個速度是非常影響用戶體驗的. 如果提升編碼速度, 就必須使用硬件編碼, 但硬件編碼是有局限性的: 接口和平臺相關(guān), 沒有通用Android接口, 只有系統(tǒng)App才有可能使用, 下面就講一下QCOM平臺如何使用JPEG硬件編碼.
代碼路徑
高通平臺JPEG硬件編碼接口路徑為:
hardware/qcom/camera/QCamera2/stack/mm-jpeg-interface/
當(dāng)然, 只知道接口, 是沒法用的, 因為里面很多參數(shù)你根本不知道怎么設(shè)置, 又沒有文檔, 不過好在高通提供了一個測試用例, 路徑如下:
hardware/qcom/camera/QCamera2/stack/mm-jpeg-interface/test/
這個測試用例覆蓋了編碼(encode)和解碼(decode), 我們主要看編碼, 測試用例是一個可執(zhí)行程序, 通過輸入yuv文件路徑, 最終輸出JPEG文件. 但我們一般使用編碼是用Buffer方式作為輸入,而不是文件路徑, 所以要想調(diào)用, 我們得自己改造重新封裝代碼, 測試代碼有500行左右, 要完全讀懂也需要花點時間, 我曾經(jīng)封裝過, 并測試通過, 所以只要看懂測試代碼封裝起來肯定沒問題(由于當(dāng)時沒備份代碼,所以沒法給示例代碼).
確認芯片是否支持JPEG硬件編碼
雖然有接口, 但如果沒有硬件支持, 接口調(diào)用的就是軟件編碼了,可通過adb命令來查看手機是否有JPEG相關(guān)的設(shè)備:
$ adb shell ls -lZ /dev/ |grep -i jpeg
crw-rw---- 1 system camera u:object_r:video_device:s0 235, 0 1970-04-26 21:43 jpeg0
crw-rw---- 1 system camera u:object_r:video_device:s0 234, 0 1970-04-26 21:43 jpeg1
crw-rw---- 1 system camera u:object_r:video_device:s0 233, 0 1970-04-26 21:43 jpeg2
crw-rw---- 1 system camera u:object_r:video_device:s0 232, 0 1970-04-26 21:43 jpeg3
如果輸出只有一個jpep dev, 基本上當(dāng)前芯片只支持硬件編碼, 有多個則支持硬件編碼和解碼(上面輸出信息手機是 Sony Xperia Z5), 沒有就說明手機只有軟件編碼.
根據(jù)需要添加權(quán)限
如果你代碼封裝好了, 并且通過編譯為可執(zhí)行文件也能測試通過, 接下來就是編譯為動態(tài)庫(.so)來供其他程序調(diào)用了, 但即便你封裝好了動態(tài)庫, 也不能直接使用, 因為存在權(quán)限問題, 硬件編碼并不是所有模塊默認都有權(quán)限使用, 就我知道的, Camera HAL層是默認有使用權(quán)限的, 如果你想給App調(diào)用,需要添加設(shè)備節(jié)點的權(quán)限(selinux), 這個權(quán)限一般BSP同事都知道如何添加,基本上做法如下:
在你所在的權(quán)限組(如system_app, platform_app等等,不知道可以先學(xué)下seLinux)的.te文件中加入如下權(quán)限:
allow mediaserver video_device:dir r_dir_perms;
allow mediaserver video_device:chr_file rw_file_perms;
注意: 上面的 mediaserver 只是舉例用的, 需替換為調(diào)用硬件編碼的程序所在的權(quán)限組.比如如果是系統(tǒng)默認的App需要調(diào)用, 一般就是在 system/sepolicy/platform_app.te
中加入:
allow platform_app video_device:dir r_dir_perms;
allow platform_app video_device:chr_file rw_file_perms;
修改后需編譯boot.img(make bootimage)或者全部編譯, 然后刷到手機中.
說明: 上面所有方法只針對系統(tǒng)App(預(yù)置或者系統(tǒng)本身App), 安裝App是沒法使用的, 因為Android N及以后, 安裝的App都沒有權(quán)限調(diào)用系統(tǒng)動態(tài)庫.
說明: 根據(jù)平臺芯片不同, 上述權(quán)限添加方法可能有差異, 出現(xiàn)問題時, 可 adb logcat |grep avc
或者 adb logcat |grep -i jpeg
看下selinux 和 jpeg相關(guān)log來定位并解決問題.
注意: Android O及以后, 由于引入了Project Treble計劃,對于seLinux權(quán)限的添加, 請加在編譯所對應(yīng)的產(chǎn)品目錄下, 比如device/qcom/msm8909w/sepolicy/common/
中的對應(yīng)te文件中
MTK平臺JPEG硬件編碼
和高通平臺相比, MTK平臺就比較厚道, MTK直接封裝了硬件JPEG調(diào)用的C++接口, 而且簡單易懂, 不用文檔也能看懂, 這里多扯幾句, 雖然MTK芯片沒高通好, 但代碼框架還是可以的, MTK Camera App代碼寫的很好, 比高通的SnapdragonCamera要好太多了(當(dāng)然SnapdragonCamera好像并不是高通自己寫的), 并且MTK一些接口設(shè)計比較好, 比如雙攝框架 Stereo Mode, 比高通也好不少.
廢話不多說了,封裝的JPEG接口代碼路徑如下:
packages/apps/Gallery2/jni_stereo/refocus/JpegFactory.cpp
packages/apps/Gallery2/jni_stereo/refocus/JpegFactory.h
里面不僅有編碼, 也有解碼接口, 并且不需要你設(shè)置一些你不知道的參數(shù), 只需設(shè)置輸入輸出相關(guān)參數(shù)即可, App可以通過JNI直接調(diào)用, 非常nice.
當(dāng)然, MTK好處都說完了, 接下來說一下幾個小坑:
- JpegFactory.cpp中有幾個函數(shù)(jpgToYV12(), yv12ToJpg())雖然名字里面yuv格式是yv12, 實際是I420, 如果你當(dāng)做yv12去用, 會發(fā)現(xiàn)出來的圖片顏色紅藍是反的.
- 在比較低端的平臺(mt6737), 這個接口可能存在問題(encode圖片由色塊, decode失敗等等), 需要向MTK提單解決
- 部分平臺(MT6750, MT6737)JPEG圖片調(diào)用接口轉(zhuǎn)為yuv會導(dǎo)圖致yuv圖片動態(tài)范圍降低(圖片亮度看起來比JPEG圖片要低一些), 但如果再次通過其接口將yuv轉(zhuǎn)為Jpeg, 圖片就會恢復(fù)正常的.
Android系統(tǒng)軟件JPEG編解碼
軟件編解碼Android系統(tǒng)中提供了一些格式的支持, 主要是JPEG轉(zhuǎn)RGB, RGB轉(zhuǎn)JPEG, YUV轉(zhuǎn)JPEG.
JPEG轉(zhuǎn)RGB 和 RGB轉(zhuǎn)JPEG
這個是個Android開發(fā)者都用過的, 就是常用的BitmapFactory.decodeXxx()
, BitmapFactory的decode方法其實就是一個將JPEG解碼為RGB的過程, 但這里的RGB也分為多種格式, 主要有:
Bitmap.Config.ARGB_8888
Bitmap.Config.ARGB_4444
Bitmap.Config.RGB_565
正常情況下, 如果我們decode的時候沒有設(shè)置BitmapFactory.Options
, 則一般使用的是ARGB_8888, 如果你確切的知道你需要那種RGB格式, 請手動指定decode的參數(shù)BitmapFactory.Options.inPreferredConfig = Bitmap.Config.xxx
ARGB_8888是效果最好的格式, 占用內(nèi)存也最大, 其他格式對效果有損失, 但占用內(nèi)存小.
如果你需要對decode后的圖片進行二次處理, 就需要獲取Bitmap里面的像素點數(shù)據(jù)(buffer), 有兩種做法:
- 利用Bitmap方法
copyPixelsToBuffer(Buffer dst)
將像素數(shù)據(jù)復(fù)制到ByteBuffer
中, 然后將ByteBuffer
中的數(shù)組或者ByteBuffer
對象通過JNI傳到native層, 然后處理, 處理完后通過Bitmap方法copyPixelsFromBuffer(Buffer src)
將數(shù)據(jù)復(fù)制回來即可, 但這種方法效率低, 占用額外內(nèi)存, 不推薦. - 直接使用Bitmap的NDK接口來操作Bitmap數(shù)據(jù), 基本做法就是通過JNI將Bitmap對象傳到native層, 然后通過NDK提供的接口進行操作, 部分代碼如下:
#include <android/bitmap.h>
AndroidBitmapInfo info;
void* pixels;
int ret;
void test((JNIEnv * env, jobject obj, jobject bitmap) {
//獲取bitmap信息
if ((ret = AndroidBitmap_getInfo(env, bitmap, &info)) < 0) {
LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
return;
}
//獲取像素數(shù)據(jù)
if ((ret = AndroidBitmap_lockPixels(env, bitmap, &pixels)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
}
//此時pixels就是我們要的buffer, 可直接轉(zhuǎn)為unsigned char* 傳給算法進行處理
//釋放
AndroidBitmap_unlockPixels(env, bitmap);
}
詳細代碼可參考Google NDK Sample bitmap-plasma
RGB轉(zhuǎn)為JPEG則比較簡單, 直接調(diào)用Bitmap方法public boolean compress(CompressFormat format, int quality, OutputStream stream)
這個方法底層是通過libjpeg來實現(xiàn)的, 速度和壓縮的quality(0 ~ 100)相關(guān), 越大速度越慢.
YUV轉(zhuǎn)JPEG
一般做Camera和算法集成會遇到比較多的YUV格式, Android系統(tǒng)提供了一個類YuvImage
, 用來將YUV轉(zhuǎn)為JPEG,用法很簡單:
//構(gòu)造參數(shù)分別為: yuv數(shù)據(jù)數(shù)組, 格式, 寬, 高, 步長
YuvImage yuvImage = new YuvImage(byte[] yuv, int format, int width, int height, int[] strides);
//參數(shù)分別為: 裁剪的rect, 質(zhì)量, outputStream對象
yuvImage.compressToJpeg(Rect rectangle, int quality, OutputStream stream);
其中需要注意的是, 步長stride指如果yuv數(shù)據(jù)有padding(右側(cè)有綠邊或黑邊), stride值就是圖片 寬+黑邊, 沒有則不用設(shè)置. Rect是你要壓縮為JPEG的區(qū)域,一般都是 new Rect(0, 0, width, height);
, 即整個圖像.
YuvImage 支持的格式非常有限, 只支持NV21和YUY2.構(gòu)造函數(shù)源碼如下
public YuvImage(byte[] yuv, int format, int width, int height, int[] strides) {
if (format != ImageFormat.NV21 &&
format != ImageFormat.YUY2) {
throw new IllegalArgumentException(
"only support ImageFormat.NV21 " +
"and ImageFormat.YUY2 for now");
}
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException(
"width and height must large than 0");
}
if (yuv == null) {
throw new IllegalArgumentException("yuv cannot be null");
}
if (strides == null) {
mStrides = calculateStrides(width, format);
} else {
mStrides = strides;
}
mData = yuv;
mFormat = format;
mWidth = width;
mHeight = height;
}
其他和JPEG相關(guān)的軟件編解碼
如果上述系統(tǒng)編碼解碼都滿足不了你的需求,你就的自己使用一些通用的軟件編解碼或格式處理庫了, 比較常用的有 libyuv和libjpeg, libyuv主要是對yuv進行格式轉(zhuǎn)換,旋轉(zhuǎn)等, libjpeg則是和JPEG編解碼相關(guān)的. libyuv和libjpeg源碼Android系統(tǒng)中都有, 路徑分別為external/libyuv
和external/libjpeg(或者external/libjpeg-turbo)
,引入相關(guān)頭文件和庫就能使用了.如果你是開發(fā)第三方App, 則需把編譯的libyuv.so和libjpeg.so打包到apk中.