Android JPEG编解码(YUV,RGB,JPEG格式转换)

JPEG( Joint Photographic Experts Group)是一种图像压缩标准, 也是目前使用最广泛的图片压缩技术, 图片之所以要压缩, 原因肯定是占用空间太大, 如果不压缩的话, 对于800万像素的手机, 一个RGB图片文件占用空间为24M(3264*2448*3), 如果是YUV420格式, 也要占用12M(3264*2448*1.5), 而一张高质量JPEG格式只占用 3M左右的空间, 可见压缩对于图片存储和传输都有非常重要的意义. 本文主要讲在Android系统中有哪些方法将YUV或者RGB图片转为JPEG, 其中主要分为以下几种类型的JPEG编码:

  1. QCOM(高通)平台JPEG硬件编码
  2. MTK平台JPEG硬件编码
  3. Android系统JPEG软件编解码
  4. 其他软件编解码

QCOM(高通)平台硬件JPEG编码

JPEG硬件编码是指芯片中针对JPEG编码有特殊的硬件设计, 可以加速编码速度, 我在高通msm8937平台测试过, 对于分辨率为3264x2448大小的图片, 编码质量为 90左右的情况下, 硬件编码只需 150ms左右, 而软件编码则需600ms左右(数据不一定准确, 但大体上差不多), 可以看到硬件编码速度是软件的好几倍, 对于Camera相关应用来说,软件编码这个速度是非常影响用户体验的. 如果提升编码速度, 就必须使用硬件编码, 但硬件编码是有局限性的: 接口和平台相关, 没有通用Android接口, 只有系统App才有可能使用, 下面就讲一下QCOM平台如何使用JPEG硬件编码.

代码路径

高通平台JPEG硬件编码接口路径为:
hardware/qcom/camera/QCamera2/stack/mm-jpeg-interface/
当然, 只知道接口, 是没法用的, 因为里面很多参数你根本不知道怎么设置, 又没有文档, 不过好在高通提供了一个测试用例, 路径如下:
hardware/qcom/camera/QCamera2/stack/mm-jpeg-interface/test/
这个测试用例覆盖了编码(encode)和解码(decode), 我们主要看编码, 测试用例是一个可执行程序, 通过输入yuv文件路径, 最终输出JPEG文件. 但我们一般使用编码是用Buffer方式作为输入,而不是文件路径, 所以要想调用, 我们得自己改造重新封装代码, 测试代码有500行左右, 要完全读懂也需要花点时间, 我曾经封装过, 并测试通过, 所以只要看懂测试代码封装起来肯定没问题(由于当时没备份代码,所以没法给示例代码).

确认芯片是否支持JPEG硬件编码

虽然有接口, 但如果没有硬件支持, 接口调用的就是软件编码了,可通过adb命令来查看手机是否有JPEG相关的设备:

$ 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, 基本上当前芯片只支持硬件编码, 有多个则支持硬件编码和解码(上面输出信息手机是 Sony Xperia Z5), 没有就说明手机只有软件编码.

根据需要添加权限

如果你代码封装好了, 并且通过编译为可执行文件也能测试通过, 接下来就是编译为动态库(.so)来供其他程序调用了, 但即便你封装好了动态库, 也不能直接使用, 因为存在权限问题, 硬件编码并不是所有模块默认都有权限使用, 就我知道的, Camera HAL层是默认有使用权限的, 如果你想给App调用,需要添加设备节点的权限(selinux), 这个权限一般BSP同事都知道如何添加,基本上做法如下:
在你所在的权限组(如system_app, platform_app等等,不知道可以先学下seLinux)的.te文件中加入如下权限:

allow mediaserver video_device:dir r_dir_perms;
allow mediaserver video_device:chr_file rw_file_perms;

注意: 上面的 mediaserver 只是举例用的, 需替换为调用硬件编码的程序所在的权限组.比如如果是系统默认的App需要调用, 一般就是在 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)或者全部编译, 然后刷到手机中.

说明: 上面所有方法只针对系统App(预置或者系统本身App), 安装App是没法使用的, 因为Android N及以后, 安装的App都没有权限调用系统动态库.

说明: 根据平台芯片不同, 上述权限添加方法可能有差异, 出现问题时, 可 adb logcat |grep avc 或者 adb logcat |grep -i jpeg看下selinux 和 jpeg相关log来定位并解决问题.
注意: Android O及以后, 由于引入了Project Treble计划,对于seLinux权限的添加, 请加在编译所对应的产品目录下, 比如device/qcom/msm8909w/sepolicy/common/中的对应te文件中

MTK平台JPEG硬件编码

和高通平台相比, MTK平台就比较厚道, MTK直接封装了硬件JPEG调用的C++接口, 而且简单易懂, 不用文档也能看懂, 这里多扯几句, 虽然MTK芯片没高通好, 但代码框架还是可以的, MTK Camera App代码写的很好, 比高通的SnapdragonCamera要好太多了(当然SnapdragonCamera好像并不是高通自己写的), 并且MTK一些接口设计比较好, 比如双摄框架 Stereo Mode, 比高通也好不少.
废话不多说了,封装的JPEG接口代码路径如下:

packages/apps/Gallery2/jni_stereo/refocus/JpegFactory.cpp
packages/apps/Gallery2/jni_stereo/refocus/JpegFactory.h

里面不仅有编码, 也有解码接口, 并且不需要你设置一些你不知道的参数, 只需设置输入输出相关参数即可, App可以通过JNI直接调用, 非常nice.
当然, MTK好处都说完了, 接下来说一下几个小坑:

  1. JpegFactory.cpp中有几个函数(jpgToYV12(), yv12ToJpg())虽然名字里面yuv格式是yv12, 实际是I420, 如果你当做yv12去用, 会发现出来的图片颜色红蓝是反的.
  2. 在比较低端的平台(mt6737), 这个接口可能存在问题(encode图片由色块, decode失败等等), 需要向MTK提单解决
  3. 部分平台(MT6750, MT6737)JPEG图片调用接口转为yuv会导图致yuv图片动态范围降低(图片亮度看起来比JPEG图片要低一些), 但如果再次通过其接口将yuv转为Jpeg, 图片就会恢复正常的.

Android系统软件JPEG编解码

软件编解码Android系统中提供了一些格式的支持, 主要是JPEG转RGB, RGB转JPEG, YUV转JPEG.

JPEG转RGB 和 RGB转JPEG

这个是个Android开发者都用过的, 就是常用的BitmapFactory.decodeXxx(), BitmapFactory的decode方法其实就是一个将JPEG解码为RGB的过程, 但这里的RGB也分为多种格式, 主要有:

Bitmap.Config.ARGB_8888
Bitmap.Config.ARGB_4444
Bitmap.Config.RGB_565

正常情况下, 如果我们decode的时候没有设置BitmapFactory.Options, 则一般使用的是ARGB_8888, 如果你确切的知道你需要那种RGB格式, 请手动指定decode的参数BitmapFactory.Options.inPreferredConfig = Bitmap.Config.xxx
ARGB_8888是效果最好的格式, 占用内存也最大, 其他格式对效果有损失, 但占用内存小.

如果你需要对decode后的图片进行二次处理, 就需要获取Bitmap里面的像素点数据(buffer), 有两种做法:

  • 利用Bitmap方法copyPixelsToBuffer(Buffer dst)将像素数据复制到ByteBuffer中, 然后将ByteBuffer中的数组或者ByteBuffer对象通过JNI传到native层, 然后处理, 处理完后通过Bitmap方法copyPixelsFromBuffer(Buffer src)将数据复制回来即可, 但这种方法效率低, 占用额外内存, 不推荐.
  • 直接使用Bitmap的NDK接口来操作Bitmap数据, 基本做法就是通过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;
    }
    //获取像素数据
    if ((ret = AndroidBitmap_lockPixels(env, bitmap, &pixels)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
    }
    //此时pixels就是我们要的buffer, 可直接转为unsigned char* 传给算法进行处理
    //释放
    AndroidBitmap_unlockPixels(env, bitmap);
}

详细代码可参考Google NDK Sample bitmap-plasma

RGB转为JPEG则比较简单, 直接调用Bitmap方法public boolean compress(CompressFormat format, int quality, OutputStream stream)这个方法底层是通过libjpeg来实现的, 速度和压缩的quality(0 ~ 100)相关, 越大速度越慢.

YUV转JPEG

一般做Camera和算法集成会遇到比较多的YUV格式, Android系统提供了一个类YuvImage, 用来将YUV转为JPEG,用法很简单:

//构造参数分别为: yuv数据数组, 格式, 宽, 高, 步长
YuvImage yuvImage = new YuvImage(byte[] yuv, int format, int width, int height, int[] strides);
//参数分别为: 裁剪的rect, 质量, outputStream对象
yuvImage.compressToJpeg(Rect rectangle, int quality, OutputStream stream);

其中需要注意的是, 步长stride指如果yuv数据有padding(右侧有绿边或黑边), stride值就是图片 宽+黑边, 没有则不用设置. Rect是你要压缩为JPEG的区域,一般都是 new Rect(0, 0, width, height);, 即整个图像.

YuvImage 支持的格式非常有限, 只支持NV21和YUY2.构造函数源码如下

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相关的软件编解码

如果上述系统编码解码都满足不了你的需求,你就的自己使用一些通用的软件编解码或格式处理库了, 比较常用的有 libyuv和libjpeg, libyuv主要是对yuv进行格式转换,旋转等, libjpeg则是和JPEG编解码相关的. libyuv和libjpeg源码Android系统中都有, 路径分别为external/libyuvexternal/libjpeg(或者external/libjpeg-turbo) ,引入相关头文件和库就能使用了.如果你是开发第三方App, 则需把编译的libyuv.so和libjpeg.so打包到apk中.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,961评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,444评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,009评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,082评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,101评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,271评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,738评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,395评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,539评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,434评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,481评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,160评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,749评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,816评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,038评论 1 256
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,548评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,140评论 2 341

推荐阅读更多精彩内容