Android JPEG編解碼(YUV,RGB,JPEG格式轉(zhuǎn)換)

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編碼:

  1. QCOM(高通)平臺JPEG硬件編碼
  2. MTK平臺JPEG硬件編碼
  3. Android系統(tǒng)JPEG軟件編解碼
  4. 其他軟件編解碼

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好處都說完了, 接下來說一下幾個小坑:

  1. JpegFactory.cpp中有幾個函數(shù)(jpgToYV12(), yv12ToJpg())雖然名字里面yuv格式是yv12, 實際是I420, 如果你當(dāng)做yv12去用, 會發(fā)現(xiàn)出來的圖片顏色紅藍是反的.
  2. 在比較低端的平臺(mt6737), 這個接口可能存在問題(encode圖片由色塊, decode失敗等等), 需要向MTK提單解決
  3. 部分平臺(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/libyuvexternal/libjpeg(或者external/libjpeg-turbo) ,引入相關(guān)頭文件和庫就能使用了.如果你是開發(fā)第三方App, 則需把編譯的libyuv.so和libjpeg.so打包到apk中.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末位隶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖翠储,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件枣察,死亡現(xiàn)場離奇詭異,居然都是意外死亡恃泪,警方通過查閱死者的電腦和手機外臂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門坐儿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人宋光,你說我怎么就攤上這事貌矿。” “怎么了罪佳?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵逛漫,是天一觀的道長。 經(jīng)常有香客問我赘艳,道長酌毡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任蕾管,我火速辦了婚禮枷踏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘掰曾。我一直安慰自己旭蠕,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掏熬,像睡著了一般佑稠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上孽江,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天讶坯,我揣著相機與錄音番电,去河邊找鬼岗屏。 笑死,一個胖子當(dāng)著我的面吹牛漱办,可吹牛的內(nèi)容都是我干的这刷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼娩井,長吁一口氣:“原來是場噩夢啊……” “哼暇屋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起洞辣,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤咐刨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扬霜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體定鸟,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年著瓶,在試婚紗的時候發(fā)現(xiàn)自己被綠了联予。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡材原,死狀恐怖沸久,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情余蟹,我是刑警寧澤卷胯,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站威酒,受9級特大地震影響窑睁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜兼搏,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一卵慰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧佛呻,春花似錦裳朋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽送挑。三九已至,卻和暖如春暖眼,著一層夾襖步出監(jiān)牢的瞬間惕耕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工诫肠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留司澎,地道東北人。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓栋豫,卻偏偏與公主長得像挤安,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子丧鸯,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容