Android 端处理 YUV 数据 - Libyuv 的编译与使用

前言

在 Android 系统上, Camera 输出的图像一般为 NV21(YUV420SP 系列) 格式, 当我们想进行录像处理时, 会面临两个问题

问题 1

图像的旋转问题

  • 后置镜头: 需要旋转 90°
  • 前置镜头: 需要旋转 270° 然后再进行镜像处理

问题 2

处理好镜头的旋转后, 当我们尝试使用 MediaCodec 进行 H.264 的硬编时, 便会发现偏色的问题

这是因为 MediaCodec 的 COLOR_FormatYUV420SemiPlanar 格式为 NV12, 并非是 NV21, 虽然都是 YUV420SP 系列, 但他们的排列不同, 都是先存储 Y 的数据, NV21 是 vu 交替存储, NV12 是 uv 交替存储

- NV21: yyyy yyyy vu vu
- NV12: yyyy yyyy uv uv

为了解决这个问题, 对于这个问题网上有很多的解决思路, 我们可以在 Java 层使用进行数据操作, 不过经过测试之后发现, 在 Samsung S7 Edge 上, 录制 1080p

  • 旋转与镜像: 20ms
  • NV21 转 NV12: 16ms

消耗时长约为 40ms, 这也仅仅是勉强能够进行 25 帧的录制, 在使用 opencv 进行人脸识别或滤镜处理时, 能够感觉到明显的卡顿感

libyuv 便是 google 为了解决移动端 NV21 数据处理不便所提供的开源库, 它提供了旋转, 裁剪, 镜像, 缩放等功能

接下来看看 libyuv 的编译与使用

一. 环境

操作系统

MacOS Mojave version 10.14.5

Libyuv

https://chromium.googlesource.com/libyuv/libyuv/

git clone https://chromium.googlesource.com/libyuv/libyuv
libyuv 源码

NDK 版本

NDK16

cmake 版本

➜  ~ cmake -version
cmake version 3.14.5

二. 编译脚本

从 libyuv 的源码中, 可以看到 libyuv 已经提供了 CMakeLists.txt, 因此我们可以直接通过 cmake 生成 Makefile, 然后通过 make 对 Makefile 进行编译

ARCH=arm
ANDROID_ARCH_ABI=armeabi-v7a
NDK_PATH=/Users/sharrychoo/Library/Android/ndk/android-ndk-r16b
PREFIX=`pwd`/android/${ARCH}/${CPU}

# cmake 传参
cmake -G"Unix Makefiles" \
    -DANDROID_NDK=${NDK_PATH} \
    -DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=${ANDROID_ARCH_ABI} \
    -DANDROID_NATIVE_API_LEVE=16 \
    -DCMAKE_INSTALL_PREFIX=${PREFIX} \
    -DANDROID_ARM_NEON=TRUE \
    ..
    
# 生成动态库
make 
make install
编译结果

输出的 so 库

so 库

三. 代码编写

我们将 so 库和头文件拷贝到 AS 中, 便可以进行代码的编写了, 这里编写一个 Libyuv 的工具类, 方便后续使用

一) Java 代码

这里以 NV21 转 I420 为例

/**
 * 处理 YUV 的工具类
 *
 * @author Sharry <a href="sharrychoochn@gmail.com">Contact me.</a>
 * @version 1.0
 * @since 2019-07-23
 */
public class LibyuvUtil {

    static {
        System.loadLibrary("smedia-camera");
    }

    /**
     * 将 NV21 转 I420
     */
    public static native void convertNV21ToI420(byte[] src, byte[] dst, int width, int height);
    
    ......
}

二) native 实现

这里以将 NV21 转 I420 为例

namespace libyuv_util {

    void convertI420ToNV12(JNIEnv *env, jclass, jbyteArray i420_src, jbyteArray nv12_dst, int width,
                           int height) {
        jbyte *src = env->GetByteArrayElements(i420_src, NULL);
        jbyte *dst = env->GetByteArrayElements(nv12_dst, NULL);
        // 执行转换 I420 -> NV12 的转换
        LibyuvUtil::I420ToNV12(src, dst, width, height);
        // 释放资源
        env->ReleaseByteArrayElements(i420_src, src, 0);
        env->ReleaseByteArrayElements(nv12_dst, dst, 0);
    }
    
}

void LibyuvUtil::NV21ToI420(jbyte *src, jbyte *dst, int width, int height) {
    // NV21 参数
    jint src_y_size = width * height;
    jbyte *src_y = src;
    jbyte *src_vu = src + src_y_size;
    // I420 参数
    jint dst_y_size = width * height;
    jint dst_u_size = dst_y_size >> 2;
    jbyte *dst_y = dst;
    jbyte *dst_u = dst + dst_y_size;
    jbyte *dst_v = dst + dst_y_size + dst_u_size;
    /**
    * <pre>
    * int NV21ToI420(const uint8_t* src_y,
    *          int src_stride_y,
    *          const uint8_t* src_vu,
    *          int src_stride_vu,
    *          uint8_t* dst_y,
    *          int dst_stride_y,
    *          uint8_t* dst_u,
    *          int dst_stride_u,
    *          uint8_t* dst_v,
    *          int dst_stride_v,
    *          int width,
    *          int height);
    * </pre>
    * <p>
    * stride 为颜色分量的跨距: 它描述一行像素中, 该颜色分量所占的 byte 数目, YUV 每个通道均为 1byte(8bit)
    * <p>
    * stride_y: Y 是最全的, 一行中有 width 个像素, 也就有 width 个 Y
    * stride_u: YUV420 的采样为 Y:U:V = 4:1:1, 从整体的存储来看, 一个 Y 分量的数目为 U/V 的四倍
    * 但从一行上来看, width 个 Y, 它会用到 width/2 个 U
    * stride_v: 同 stride_u 的分析方式
    */
    libyuv::NV21ToI420(
            (uint8_t *) src_y, width,
            (uint8_t *) src_vu, width,
            (uint8_t *) dst_y, width,
            (uint8_t *) dst_u, width >> 1,
            (uint8_t *) dst_v, width >> 1,
            width, height
    );
}

可以看到方法的调用也非常的简单, 只需要传入相关参数即可, 其中有个非常重要的参数, stride 跨距, 它描述一行像素中, 该颜色分量所占的 byte 数目

  • YUV420 系列
    • NV21
      • Y: 跨距为 width
      • VU: 跨距为 width
    • I420P(YU12):
      • Y: 跨距为 width
      • U: 跨距为 width/2
      • V: 跨距为 width/2
  • ABGR: 跨距为 4 *width

对常用的色彩空间不熟悉, 请点击这里查看

总结

通过 libyuv 进行旋转镜像转码等操作, 其时长如下

  • 旋转镜像: 5~8ms
  • NV21 转 NV12: 0~3ms

可以看到比起 java 代码, 几乎快了 3 倍, 这已经能够满足流畅录制的需求了

笔者将常用的 YUV 操作整理成了demo 点击查看, 如有需要可以将代码直接拷走使用

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

推荐阅读更多精彩内容