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编码:

  • QCOM(高通)平台JPEG硬件编码
  • MTK平台JPEG硬件编码
  • Android系统JPEG软件编解码
  • 其他软件编解码
  • 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好处都说完了, 接下来说一下几个小坑:

  • JpegFactory.cpp中有几个函数(jpgToYV12(), yv12ToJpg())虽然名字里面yuv格式是yv12, 实际是I420, 如果你当做yv12去用, 会发现出来的图片颜色红蓝是反的.
  • 在比较低端的平台(mt6737), 这个接口可能存在问题(encode图片由色块, decode失败等等), 需要向MTK提单解决
  • 部分平台(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;