在 Android 系统上, Camera 输出的图像通常为 NV21(YUV420SP 系列) 格式, 当咱们想进行录像处理时, 会面临两个问题html
图像的旋转问题java
处理好镜头的旋转后, 当咱们尝试使用 MediaCodec 进行 H.264 的硬编时, 便会发现偏色的问题android
这是由于 MediaCodec 的 COLOR_FormatYUV420SemiPlanar 格式为 NV12, 并不是是 NV21, 虽然都是 YUV420SP 系列, 但他们的排列不一样, 都是先存储 Y 的数据, NV21 是 vu 交替存储, NV12 是 uv 交替存储git
- NV21: yyyy yyyy vu vu
- NV12: yyyy yyyy uv uv
复制代码
为了解决这个问题, 对于这个问题网上有不少的解决思路, 咱们能够在 Java 层使用进行数据操做, 不过通过测试以后发现, 在 Samsung S7 Edge 上, 录制 1080pgithub
消耗时长约为 40ms, 这也仅仅是勉强可以进行 25 帧的录制, 在使用 opencv 进行人脸识别或滤镜处理时, 可以感受到明显的卡顿感bash
libyuv 即是 google 为了解决移动端 NV21 数据处理不便所提供的开源库, 它提供了旋转, 裁剪, 镜像, 缩放等功能ide
接下来看看 libyuv 的编译与使用工具
MacOS Mojave version 10.14.5测试
chromium.googlesource.com/libyuv/liby…ui
git clone https://chromium.googlesource.com/libyuv/libyuv
复制代码
NDK16
➜ ~ 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 库和头文件拷贝到 AS 中, 即可以进行代码的编写了, 这里编写一个 Libyuv 的工具类, 方便后续使用
这里以 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);
......
}
复制代码
这里以将 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 数目
对经常使用的色彩空间不熟悉, 请点击这里查看
经过 libyuv 进行旋转镜像转码等操做, 其时长以下
能够看到比起 java 代码, 几乎快了 3 倍, 这已经可以知足流畅录制的需求了
笔者将经常使用的 YUV 操做整理成了demo 点击查看, 若有须要能够将代码直接拷走使用