28

Android音视频——Libyuv使用实战 - 简书

 4 years ago
source link: https://www.jianshu.com/p/9e062ba44a83?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
102019.09.20 15:38:48字数 2,355阅读 5,517

近期换部门,从事之前从未接触过的Android音视频开发,主要涉及到USB摄像头调用、libyuv处理Nv21图像、直播推流等功能,对应的库有【UVCCamera】【libyuv】等,刚接触没经验也没人带挺难搞的,而且网上资料很凌乱,所以,开此篇总结&汇总一下近期的研究,兴许可以帮助到别人,本人亦是新手,文中如有不正确的地方,欢迎指出点评。

一、libyuv入门

先简单说明一下,不管是Android手机的Camera,或是外接的UVCCamera(免驱摄像头),它们获取到的yuv图像格式都是nv21格式的,针对业务,我们可能需要对摄像头获取到的图像进行各种处理,如:镜像、旋转、缩放、裁剪等。

1、yuv概念

总的来说,我们要做的yuv数据处理,无非就是针对各种图像格式下yuv数据(byte[])的转换、调整。举个例子:

  1. NV21:安卓的模式。存储顺序是先存Y,再存U,再VU交替存储,格式为:YYYYVUVUVU。
  2. I420:又叫YU12,安卓的模式。存储顺序是先存Y,再存U,最后存V,格式为YYYYUUUVVV。

可以看到,NV21与I420(都属于YUV420)之间的差别在于U和V的存储位置,所以,NV21要转换成I420,就必须把NV21中的U和V调整为I420的方式存储即可,其他格式之间的转换以此类推。

2、libyuv概念

libyuv是Google开源的yuv图像处理库,实现对各种yuv数据之间的转换,包括数据转换,裁剪,缩放,旋转。尽管libyuv对yuv数据处理的核心进行了封装,但还是要求开发者对各种格式的区别有所了解,这样才能正常调用对应方法,进行转换。在使用这个库之前,如有时间,建议先去了解下yuv的相关知识,相关的文章推荐如下:

3、libyuv核心方法

通过git下载下来的libyuv源码目录,有几个文件需要我们了解下,分别是:

// 格式转换(NV21、NV12、I420等格式互转)
libyuv\include\libyuv\convert_from.h
// 图像处理(镜像、旋转、缩放、裁剪)
libyuv\include\libyuv\planar_functions.h
libyuv\include\libyuv\rotate.h
libyuv\include\libyuv\scale.h
libyuv\include\libyuv\convert.h

以上的几个头文件中声明了libyuv对yuv数据处理的一些函数,我们后续需要使用到这些函数来处理yuv数据的转换和修改。

二、libyuv进阶

通过上面的入门内容与资料,应该对yuv与libyuv有比较表面的理解了,但要完全理解透还是得靠自己再多看看其他资料才行,下面直接使用libyuv这个库,实现一些实际的代码逻辑,完全干货分享,如有错误请不吝赐教。

1、yuv转换格式

因为libyuv对于图像的处理基本上都是针对i420格式的,所以,不管摄像头获取到的图像格式如何,都需要在进行图像处理之前转换成i420格式才行。这里整理了比较常用的nv21与i420、nv12与i420互转的cpp代码实现:

nv21是Android摄像头获取到的图像格式。
nv12是iOS摄像头获取到的图像格式。

// nv21 --> i420
void nv21ToI420(jbyte *src_nv21_data, jint width, jint height, jbyte *src_i420_data) {
    jint src_y_size = width * height;
    jint src_u_size = (width >> 1) * (height >> 1);

    jbyte *src_nv21_y_data = src_nv21_data;
    jbyte *src_nv21_vu_data = src_nv21_data + src_y_size;

    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_y_size + src_u_size;

    libyuv::NV21ToI420((const uint8 *) src_nv21_y_data, width,
                       (const uint8 *) src_nv21_vu_data, width,
                       (uint8 *) src_i420_y_data, width,
                       (uint8 *) src_i420_u_data, width >> 1,
                       (uint8 *) src_i420_v_data, width >> 1,
                       width, height);
}

// i420 --> nv21
void i420ToNv21(jbyte *src_i420_data, jint width, jint height, jbyte *src_nv21_data) {
    jint src_y_size = width * height;
    jint src_u_size = (width >> 1) * (height >> 1);

    jbyte *src_nv21_y_data = src_nv21_data;
    jbyte *src_nv21_uv_data = src_nv21_data + src_y_size;

    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_y_size + src_u_size;


    libyuv::I420ToNV21(
            (const uint8 *) src_i420_y_data, width,
            (const uint8 *) src_i420_u_data, width >> 1,
            (const uint8 *) src_i420_v_data, width >> 1,
            (uint8 *) src_nv21_y_data, width,
            (uint8 *) src_nv21_uv_data, width,
            width, height);
}

// nv12 --> i420 
void nv12ToI420(jbyte *Src_data, jint src_width, jint src_height, jbyte *Dst_data) {
    // NV12 video size
    jint NV12_Size = src_width * src_height * 3 / 2;
    jint NV12_Y_Size = src_width * src_height;

    // YUV420 video size
    jint I420_Size = src_width * src_height * 3 / 2;
    jint I420_Y_Size = src_width * src_height;
    jint I420_U_Size = (src_width >> 1)*(src_height >> 1);
    jint I420_V_Size = I420_U_Size;

    // src: buffer address of Y channel and UV channel
    jbyte *Y_data_Src = Src_data;
    jbyte *UV_data_Src = Src_data + NV12_Y_Size;
    jint src_stride_y = src_width;
    jint src_stride_uv = src_width;

    //dst: buffer address of Y channel、U channel and V channel
    jbyte *Y_data_Dst = Dst_data;
    jbyte *U_data_Dst = Dst_data + I420_Y_Size;
    jbyte *V_data_Dst = Dst_data + I420_Y_Size + I420_U_Size;
    jint Dst_Stride_Y = src_width;
    jint Dst_Stride_U = src_width >> 1;
    jint Dst_Stride_V = Dst_Stride_U;

    libyuv::NV12ToI420((const uint8 *) Y_data_Src, src_stride_y,
                         (const uint8 *) UV_data_Src, src_stride_uv,
                         (uint8 *) Y_data_Dst, Dst_Stride_Y,
                         (uint8 *) U_data_Dst, Dst_Stride_U,
                         (uint8 *) V_data_Dst, Dst_Stride_V,
                         src_width, src_height);
}

// i420 --> nv12 
void i420ToNv12(jbyte *src_i420_data, jint width, jint height, jbyte *src_nv12_data) {
    jint src_y_size = width * height;
    jint src_u_size = (width >> 1) * (height >> 1);

    jbyte *src_nv12_y_data = src_nv12_data;
    jbyte *src_nv12_uv_data = src_nv12_data + src_y_size;

    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_y_size + src_u_size;

    libyuv::I420ToNV12(
            (const uint8 *) src_i420_y_data, width,
            (const uint8 *) src_i420_u_data, width >> 1,
            (const uint8 *) src_i420_v_data, width >> 1,
            (uint8 *) src_nv12_y_data, width,
            (uint8 *) src_nv12_uv_data, width,
            width, height);
}

2、yuv处理图像

针对常见的图像处理,在这里也整理了一些,主要包括 镜像、旋转、缩放、剪裁。
要注意的是,所有的图像处理,都是基于i420数据格式的!

// 镜像
void mirrorI420(jbyte *src_i420_data, jint width, jint height, jbyte *dst_i420_data) {
    jint src_i420_y_size = width * height;
    // jint src_i420_u_size = (width >> 1) * (height >> 1);
    jint src_i420_u_size = src_i420_y_size >> 2;

    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_i420_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_i420_y_size + src_i420_u_size;

    jbyte *dst_i420_y_data = dst_i420_data;
    jbyte *dst_i420_u_data = dst_i420_data + src_i420_y_size;
    jbyte *dst_i420_v_data = dst_i420_data + src_i420_y_size + src_i420_u_size;

    libyuv::I420Mirror((const uint8 *) src_i420_y_data, width,
                       (const uint8 *) src_i420_u_data, width >> 1,
                       (const uint8 *) src_i420_v_data, width >> 1,
                       (uint8 *) dst_i420_y_data, width,
                       (uint8 *) dst_i420_u_data, width >> 1,
                       (uint8 *) dst_i420_v_data, width >> 1,
                       width, height);
}

// 旋转
void rotateI420(jbyte *src_i420_data, jint width, jint height, jbyte *dst_i420_data, jint degree) {
    jint src_i420_y_size = width * height;
    jint src_i420_u_size = (width >> 1) * (height >> 1);

    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_i420_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_i420_y_size + src_i420_u_size;

    jbyte *dst_i420_y_data = dst_i420_data;
    jbyte *dst_i420_u_data = dst_i420_data + src_i420_y_size;
    jbyte *dst_i420_v_data = dst_i420_data + src_i420_y_size + src_i420_u_size;

    //要注意这里的width和height在旋转之后是相反的
    if (degree == libyuv::kRotate90 || degree == libyuv::kRotate270) {
        libyuv::I420Rotate((const uint8 *) src_i420_y_data, width,
                           (const uint8 *) src_i420_u_data, width >> 1,
                           (const uint8 *) src_i420_v_data, width >> 1,
                           (uint8 *) dst_i420_y_data, height,
                           (uint8 *) dst_i420_u_data, height >> 1,
                           (uint8 *) dst_i420_v_data, height >> 1,
                           width, height,
                           (libyuv::RotationMode) degree);
    }else{
        libyuv::I420Rotate((const uint8 *) src_i420_y_data, width,
                           (const uint8 *) src_i420_u_data, width >> 1,
                           (const uint8 *) src_i420_v_data, width >> 1,
                           (uint8 *) dst_i420_y_data, width,
                           (uint8 *) dst_i420_u_data, width >> 1,
                           (uint8 *) dst_i420_v_data, width >> 1,
                           width, height,
                           (libyuv::RotationMode) degree);
    }
}

// 缩放
void scaleI420(jbyte *src_i420_data, jint width, jint height, jbyte *dst_i420_data, jint dst_width,
               jint dst_height, jint mode) {

    jint src_i420_y_size = width * height;
    jint src_i420_u_size = (width >> 1) * (height >> 1);
    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_i420_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_i420_y_size + src_i420_u_size;

    jint dst_i420_y_size = dst_width * dst_height;
    jint dst_i420_u_size = (dst_width >> 1) * (dst_height >> 1);
    jbyte *dst_i420_y_data = dst_i420_data;
    jbyte *dst_i420_u_data = dst_i420_data + dst_i420_y_size;
    jbyte *dst_i420_v_data = dst_i420_data + dst_i420_y_size + dst_i420_u_size;

    libyuv::I420Scale((const uint8 *) src_i420_y_data, width,
                      (const uint8 *) src_i420_u_data, width >> 1,
                      (const uint8 *) src_i420_v_data, width >> 1,
                      width, height,
                      (uint8 *) dst_i420_y_data, dst_width,
                      (uint8 *) dst_i420_u_data, dst_width >> 1,
                      (uint8 *) dst_i420_v_data, dst_width >> 1,
                      dst_width, dst_height,
                      (libyuv::FilterMode) mode);
}

// 裁剪
void cropI420(jbyte *src_i420_data, jint src_length, jint width, jint height, 
                jbyte *dst_i420_data, jint dst_width, jint dst_height, jint left, jint top){
    jint dst_i420_y_size = dst_width * dst_height;
    jint dst_i420_u_size = (dst_width >> 1) * (dst_height >> 1);

    jbyte *dst_i420_y_data = dst_i420_data;
    jbyte *dst_i420_u_data = dst_i420_data + dst_i420_y_size;
    jbyte *dst_i420_v_data = dst_i420_data + dst_i420_y_size + dst_i420_u_size;

    libyuv::ConvertToI420((const uint8 *) src_i420_data, src_length,
                          (uint8 *) dst_i420_y_data, dst_width,
                          (uint8 *) dst_i420_u_data, dst_width >> 1,
                          (uint8 *) dst_i420_v_data, dst_width >> 1,
                          left, top,
                          width, height,
                          dst_width, dst_height,
                          libyuv::kRotate0, libyuv::FOURCC_I420);
}

3、jni实现YuvUtil

下面编写YuvUtil.java,并通过jni实现上述方法的调用,需要在自己的libyuv module目录下,分别建议3个文件:

  • src/main/cpp/YuvJni.cpp
  • src/main/java/com/libyuv/util/YuvUtil.java
  • CMakeLists.txt

cpp/libyuv就是Google官方的libyuv源码,偷懒的话,可以直接“借鉴”这个开源项目:【LibyuvDemo】,我也是抄这里的,感谢作者~但请注意,【LibyuvDemo】中的代码是有问题的,主要是YuvJni.cpp的代码逻辑没处理好,下面的YuvJni.cpp是我修复后的代码。

1)YuvJni.cpp

以下是YuvJni.cpp代码实现,因为篇幅太长,不利用阅读,故删去上述已贴出代码,这里只贴出YuvJni.cpp中其余核心代码。
注意,这并非是完全代码,需要整合上面代码后(很简单的~),方可使用。

#include <jni.h>
#include <string>
#include "libyuv.h"

...
---------- 因为篇幅太长,这里去掉了上述重复的代码,需要使用者手动修正! ----------
---------- 1、这里需要添加yuv转换格式代码 ----------
---------- 2、这里需要添加yuv处理图像代码 ----------
...

extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_yuvCompress(JNIEnv *env, jclass type,
                                         jbyteArray nv21Src, jint width,
                                         jint height, jbyteArray i420Dst,
                                         jint dst_width, jint dst_height,
                                         jint mode, jint degree,
                                         jboolean isMirror) {

    jbyte *src_nv21_data = env->GetByteArrayElements(nv21Src, NULL);
    jbyte *dst_i420_data = env->GetByteArrayElements(i420Dst, NULL);
    jbyte *tmp_dst_i420_data = NULL;

    // nv21转化为i420
    jbyte *i420_data = (jbyte *) malloc(sizeof(jbyte) * width * height * 3 / 2);
    nv21ToI420(src_nv21_data, width, height, i420_data);
    tmp_dst_i420_data = i420_data;

    // 镜像
    jbyte *i420_mirror_data = NULL;
    if(isMirror){
        i420_mirror_data = (jbyte *)malloc(sizeof(jbyte) * width * height * 3 / 2);
        mirrorI420(tmp_dst_i420_data, width, height, i420_mirror_data);
        tmp_dst_i420_data = i420_mirror_data;
    }

    // 缩放
    jbyte *i420_scale_data = NULL;
    if(width != dst_width || height != dst_height){
        i420_scale_data = (jbyte *)malloc(sizeof(jbyte) * width * height * 3 / 2);
        scaleI420(tmp_dst_i420_data, width, height, i420_scale_data, dst_width, dst_height, mode);
        tmp_dst_i420_data = i420_scale_data;
        width = dst_width;
        height = dst_height;
    }

    // 旋转
    jbyte *i420_rotate_data = NULL;
    if (degree == libyuv::kRotate90 || degree == libyuv::kRotate180 || degree == libyuv::kRotate270){
        i420_rotate_data = (jbyte *)malloc(sizeof(jbyte) * width * height * 3 / 2);
        rotateI420(tmp_dst_i420_data, width, height, i420_rotate_data, degree);
        tmp_dst_i420_data = i420_rotate_data;
    }

    // 同步数据
    // memcpy(dst_i420_data, tmp_dst_i420_data, sizeof(jbyte) * width * height * 3 / 2);
    jint len = env->GetArrayLength(i420Dst);
    memcpy(dst_i420_data, tmp_dst_i420_data, len);
    tmp_dst_i420_data = NULL;
    env->ReleaseByteArrayElements(i420Dst, dst_i420_data, 0);

    // 释放
    if(i420_data != NULL) free(i420_data);
    if(i420_mirror_data != NULL) free(i420_mirror_data);
    if(i420_scale_data != NULL) free(i420_scale_data);
    if(i420_rotate_data != NULL) free(i420_rotate_data);
}


extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_yuvCropI420(JNIEnv *env, jclass type, jbyteArray src_, jint width,
                                     jint height, jbyteArray dst_, jint dst_width, jint dst_height,
                                     jint left, jint top) {
    //裁剪的区域大小不对
    if (left + dst_width > width || top + dst_height > height) {
        return;
    }
    //left和top必须为偶数,否则显示会有问题
    if (left % 2 != 0 || top % 2 != 0) {
        return;
    }
    // i420数据裁剪
    jint src_length = env->GetArrayLength(src_);
    jbyte *src_i420_data = env->GetByteArrayElements(src_, NULL);
    jbyte *dst_i420_data = env->GetByteArrayElements(dst_, NULL);
    cropI420(src_i420_data, src_length, width, height, dst_i420_data, dst_width, dst_height, left, top);
    env->ReleaseByteArrayElements(dst_, dst_i420_data, 0);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_yuvMirrorI420(JNIEnv *env, jclass type, jbyteArray i420Src,
                                          jint width, jint height, jbyteArray i420Dst) {
    jbyte *src_i420_data = env->GetByteArrayElements(i420Src, NULL);
    jbyte *dst_i420_data = env->GetByteArrayElements(i420Dst, NULL);
    // i420数据镜像
    mirrorI420(src_i420_data, width, height, dst_i420_data);
    env->ReleaseByteArrayElements(i420Dst, dst_i420_data, 0);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_yuvScaleI420(JNIEnv *env, jclass type, jbyteArray i420Src,
                                          jint width, jint height, jbyteArray i420Dst,
                                          jint dstWidth, jint dstHeight, jint mode) {
    jbyte *src_i420_data = env->GetByteArrayElements(i420Src, NULL);
    jbyte *dst_i420_data = env->GetByteArrayElements(i420Dst, NULL);
    // i420数据缩放
    scaleI420(src_i420_data, width, height, dst_i420_data, dstWidth, dstHeight, mode);
    env->ReleaseByteArrayElements(i420Dst, dst_i420_data, 0);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_yuvRotateI420(JNIEnv *env, jclass type, jbyteArray i420Src,
                                           jint width, jint height, jbyteArray i420Dst, jint degree) {
    jbyte *src_i420_data = env->GetByteArrayElements(i420Src, NULL);
    jbyte *dst_i420_data = env->GetByteArrayElements(i420Dst, NULL);
    // i420数据旋转
    rotateI420(src_i420_data, width, height, dst_i420_data, degree);
    env->ReleaseByteArrayElements(i420Dst, dst_i420_data, 0);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_yuvNV21ToI420(JNIEnv *env, jclass type, jbyteArray nv21Src,
                                           jint width, jint height, jbyteArray i420Dst) {
    jbyte *src_nv21_data = env->GetByteArrayElements(nv21Src, NULL);
    jbyte *dst_i420_data = env->GetByteArrayElements(i420Dst, NULL);
    // nv21转化为i420
    nv21ToI420(src_nv21_data, width, height, dst_i420_data);
    env->ReleaseByteArrayElements(i420Dst, dst_i420_data, 0);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_libyuv_util_YuvUtil_yuvI420ToNV21(JNIEnv *env, jclass type, jbyteArray i420Src,
                                           jint width, jint height, jbyteArray nv21Dst) {

    jbyte *src_i420_data = env->GetByteArrayElements(i420Src, NULL);
    jbyte *dst_nv21_data = env->GetByteArrayElements(nv21Dst, NULL);
    // i420转化为nv21
    i420ToNv21(src_i420_data, width, height, dst_nv21_data);
    env->ReleaseByteArrayElements(nv21Dst, dst_nv21_data, 0);
}

2)YuvUtil.java

以下是YuvUtil.java全部代码,与开源库中的有所不同,修复个别bug,并增加多个图像处理方法及注释。

提示:原Demo中的YuvUtil#compressYUV()在处理镜像时,会导致图像花屏、app闪退等问题,使用本文中修复后的代码,亲测可稳定处理yuv图像流数据。这里改名为yuvCompress()。

package com.libyuv.util;

public class YuvUtil {

    static {
        System.loadLibrary("yuvutil");
    }

    /**
     * YUV数据的基本的处理(nv21-->i420-->mirror-->scale-->rotate)
     *
     * @param nv21Src    原始数据
     * @param width      原始的宽
     * @param height     原始的高
     * @param dst_width  缩放的宽
     * @param i420Dst    目标数据
     * @param dst_height 缩放的高
     * @param mode       压缩模式。这里为0,1,2,3 速度由快到慢,质量由低到高,一般用0就好了,因为0的速度最快
     * @param degree     旋转的角度,90,180和270三种。切记,如果角度是90或270,则最终i420Dst数据的宽高会调换。
     * @param isMirror   是否镜像,一般只有270的时候才需要镜像
     */
    public static native void yuvCompress(byte[] nv21Src, int width, int height, byte[] i420Dst, int dst_width, int dst_height, int mode, int degree, boolean isMirror);

    /**
     * yuv数据的裁剪操作
     *
     * @param i420Src    原始数据
     * @param width      原始的宽
     * @param height     原始的高
     * @param i420Dst    输出数据
     * @param dst_width  输出的宽
     * @param dst_height 输出的高
     * @param left       裁剪的x的开始位置,必须为偶数,否则显示会有问题
     * @param top        裁剪的y的开始位置,必须为偶数,否则显示会有问题
     **/
    public static native void yuvCropI420(byte[] i420Src, int width, int height, byte[] i420Dst, int dst_width, int dst_height, int left, int top);

    /**
     * yuv数据的镜像操作
     *
     * @param i420Src i420原始数据
     * @param width
     * @param height
     * @param i420Dst i420目标数据
     */
    public static native void yuvMirrorI420(byte[] i420Src, int width, int height, byte[] i420Dst);

    /**
     * yuv数据的缩放操作
     *
     * @param i420Src   i420原始数据
     * @param width     原始宽度
     * @param height    原始高度
     * @param i420Dst   i420目标数据
     * @param dstWidth  目标宽度
     * @param dstHeight 目标高度
     * @param mode      压缩模式 ,0~3,质量由低到高,一般传入0
     */
    public static native void yuvScaleI420(byte[] i420Src, int width, int height, byte[] i420Dst, int dstWidth, int dstHeight, int mode);

    /**
     * yuv数据的旋转操作
     *
     * @param i420Src i420原始数据
     * @param width
     * @param height
     * @param i420Dst i420目标数据
     * @param degree  旋转角度
     */
    public static native void yuvRotateI420(byte[] i420Src, int width, int height, byte[] i420Dst, int degree);

    /**
     * 将NV21转化为I420
     *
     * @param nv21Src 原始I420数据
     * @param width   原始的宽
     * @param width   原始的高
     * @param i420Dst 转化后的NV21数据
     */
    public static native void yuvNV21ToI420(byte[] nv21Src, int width, int height, byte[] i420Dst);

    /**
     * 将I420转化为NV21
     *
     * @param i420Src 原始I420数据
     * @param width   原始的宽
     * @param width   原始的高
     * @param nv21Src 转化后的NV21数据
     **/
    public static native void yuvI420ToNV21(byte[] i420Src, int width, int height, byte[] nv21Src);
}

3)CMakeLists.txt

CMakeLists.txt全部内容如下:

cmake_minimum_required(VERSION 3.4.1)
include_directories(src/main/cpp/libyuv/include)
add_subdirectory(src/main/cpp/libyuv ./build)
aux_source_directory(src/main/cpp SRC_FILE)
add_library(yuvutil SHARED ${SRC_FILE})
find_library(log-lib log)
target_link_libraries(yuvutil ${log-lib} yuv)

4)build.gradle

需要在module的build.gradle中指定下NDK的相关配置:

android {
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }
    externalNativeBuild {
        cmake {
            path 'CMakeLists.txt'
        }
    }
}

5)编译so动态库

通过点击执行 Build->Mark Module 'libyuv' ,编译完成后,在build/intermediates/cmake目录下,可以得到各平台的so库文件了。

注意,如果你想生成包含armeabi平台的so动态库,那么需要在local.properties中指定低版本的NDK,比如:r14b。
点击【旧版NDK下载页面】,找到你想使用的NDK版本下载后配置下即可,我建议用r14b。

三、libyuv实战

  1. 使用UVCCamera(免驱摄像头)充当Android设备前置摄像头,获取实时视频图像数据。
  2. APP需要显示2个图像窗口,窗口1显示UVCCamera实时图像,窗口2显示使用YuvUtil处理过后的yuv数据图像。
  1. 使用 saki4510t的【UVCCamera】 实现USB摄像头的启动和预览。
  2. 使用 YuvUtil对yuv数据进行各种处理后,再利用YuvImage将yuv转成Bitmap。
  3. 最后,通过SurfaceView将转换后的Bitmap绘制并显示出来。

1、界面布局

根据上述需求,在布局中放置2个图像窗口控件,分别是

  1. UVCCameraTextureView:用于UVCCamera直接显示摄像头的预览图像。
  2. BitmapSurfaceView:用于绘制Bitmap的SurfaceView。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="@android:color/black"
              android:orientation="horizontal">

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="本地镜像图像"
            android:textColor="@android:color/white"/>

        <com.serenegiant.usb.widget.UVCCameraTextureView
            android:id="@+id/camera_view_L"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"/>

    </LinearLayout>

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="LibYUV处理图像"
            android:textColor="@android:color/white"/>

        <com.lqr.demo.widget.BitmapSurfaceView
            android:id="@+id/camera_view_R"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"/>

    </LinearLayout>

</LinearLayout>

2、BitmapSurfaceView

很简单,在子线程中,不断使用SurfaceHolder+Canvas绘制Bitmap而已。
要绘制的Bitmap由外界通过 BitmapSurfaceView#drawBitmap(Bitmap bitmap) 方法传入。

/**
 * @创建者 LQR
 * @时间 2019/9/18
 * @描述 专门绘制Bitmap的SurfaceView
 */
public class BitmapSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    private SurfaceHolder mHolder;
    private Thread mThread;
    private boolean mIsDrawing;
    private Bitmap mBitmap;
    private Paint mPaint;

    public BitmapSurfaceView(Context context) {
        this(context, null);
    }

    public BitmapSurfaceView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BitmapSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mHolder = getHolder();
        mHolder.addCallback(this);
        mPaint = new Paint();
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mThread = new Thread(this);
        mThread.start();
        mIsDrawing = true;
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mIsDrawing = false;
    }

    @Override
    public void run() {
        while (mIsDrawing) {
            try {
                if (mHolder != null && mBitmap != null) {
                    Canvas canvas = mHolder.lockCanvas();
                    canvas.drawBitmap(mBitmap, 0, 0, mPaint);
                    mHolder.unlockCanvasAndPost(canvas);
                    Thread.sleep(10);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void drawBitmap(final Bitmap bitmap) {
        post(new Runnable() {
            @Override
            public void run() {
                mBitmap = bitmap;
            }
        });
    }
}

至此,布局完成,下面是逻辑代码。

3、UVCCamera本地预览镜像

需求是将USB摄像头充当设备的前置摄像头,所以需要将摄像头捕获到的图像进行镜像处理,需要自定义一个UVCCameraHandler,具体看代码注释。

镜像:就是将图像左右像素对调,从而看起来的效果像照镜子一样。
UVCCameraHandler:是UVCCamera开源库中的摄像头控制类,用于控制摄像头的开启、预览、监听等功能。

/**
 * @创建者 LQR
 * @时间 2019/9/18
 * @描述 自定义的UVCCameraHandler
 * <p>
 * 参照{@link com.serenegiant.usb.common.UVCCameraHandlerMultiSurface},对RendererHolder进行设置,
 * 实现SurfaceView或TextureView图像本地镜像功能,关键API:
 * mRendererHolder = new RendererHolder(thread.getWidth(), thread.getHeight(), null);
 * mRendererHolder.setMirror(IRendererCommon.MIRROR_HORIZONTAL);
 */
public class MyUVCCameraHandler extends AbstractUVCCameraHandler {

    public static final MyUVCCameraHandler createHandler(
            final Activity parent, final CameraViewInterface cameraView,
            final int width, final int height) {
        return createHandler(parent, cameraView, 1, width, height, UVCCamera.FRAME_FORMAT_MJPEG, UVCCamera.DEFAULT_BANDWIDTH);
    }

    public static final MyUVCCameraHandler createHandler(
            final Activity parent, final CameraViewInterface cameraView,
            final int encoderType, final int width, final int height, final float bandwidthFactor) {
        return createHandler(parent, cameraView, encoderType, width, height, UVCCamera.FRAME_FORMAT_MJPEG, bandwidthFactor);
    }

    public static final MyUVCCameraHandler createHandler(
            final Activity parent, final CameraViewInterface cameraView,
            final int encoderType, final int width, final int height) {
        return createHandler(parent, cameraView, encoderType, width, height, UVCCamera.FRAME_FORMAT_MJPEG, UVCCamera.DEFAULT_BANDWIDTH);
    }

    public static final MyUVCCameraHandler createHandler(
            final Activity parent, final CameraViewInterface cameraView,
            final int encoderType, final int width, final int height, final int format) {
        return createHandler(parent, cameraView, encoderType, width, height, format, UVCCamera.DEFAULT_BANDWIDTH);
    }

    public static final MyUVCCameraHandler createHandler(
            final Activity parent, final CameraViewInterface cameraView,
            final int encoderType, final int width, final int height, final int format, final float bandwidthFactor) {
        final CameraThread thread = new CameraThread(MyUVCCameraHandler.class, parent, cameraView, encoderType, width, height, format, bandwidthFactor);
        thread.start();
        return (MyUVCCameraHandler) thread.getHandler();
    }

    private RendererHolder mRendererHolder;

    protected MyUVCCameraHandler(CameraThread thread) {
        super(thread);
        mRendererHolder = new RendererHolder(thread.getWidth(), thread.getHeight(), null);
        mRendererHolder.setMirror(IRendererCommon.MIRROR_HORIZONTAL);
    }

    public synchronized void release() {
        if (mRendererHolder != null) {
            mRendererHolder.release();
            mRendererHolder = null;
        }
        super.release();
    }

    public synchronized void resize(int width, int height) {
        super.resize(width, height);
        if (mRendererHolder != null) {
            mRendererHolder.resize(width, height);
        }
    }

    public synchronized void startPreview() {
        checkReleased();
        if (mRendererHolder != null) {
            super.startPreview(mRendererHolder.getSurface());
        } else {
            throw new IllegalStateException();
        }
    }

    public synchronized void addSurface(int surfaceId, Surface surface, boolean isRecordable) {
        checkReleased();
        mRendererHolder.addSurface(surfaceId, surface, isRecordable);
    }

    public synchronized void removeSurface(int surfaceId) {
        if (mRendererHolder != null) {
            mRendererHolder.removeSurface(surfaceId);
        }
    }

    @Override
    public void captureStill(String path, OnCaptureListener listener) {
        checkReleased();
        post(new Runnable() {
            @Override
            public void run() {
                synchronized (MyUVCCameraHandler.this) {
                    if (mRendererHolder != null) {
                        mRendererHolder.captureStill(path);
                        updateMedia(path);
                    }
                }
            }
        });
    }
}

4、UVCCamera开启图像预览

这一部分的代码,借鉴【USBCameraTest6】使用多个Surface显示图像的案例,主要的类说明一下:

  1. USBMonitor:与CameraDialog搭配使用,用于检测USB摄像头状态,包括连接、断开等。
  2. MyUVCCameraHandler:前面自定义的UVCCameraHandler,用于多个Surface显示图像,同时可以控制图像是否镜像。
  3. UVCCameraTextureView:USB摄像头的图像预览窗口,该控件可以根据摄像头分辨率调整窗口大小。
  4. BitmapSurfaceView:用于绘制Bitmap的图像窗口(即:专门显示经过YuvUtil处理后的yuv数据图像)。
/**
 * @创建者 LQR
 * @时间 2019/9/18
 * @描述 UVCCamera + YuvUtil 处理USB摄像头图像数据
 * 
 * 1、使用UVCCamera实现Usb摄像头图像预览。
 * 2、使用YuvUtil进行图像预处理:旋转、裁剪、缩放、镜像。
 */
public class PreprocessActivity extends BaseActivity implements CameraDialog.CameraDialogParent {

    private int WIDTH = UVCCamera.DEFAULT_PREVIEW_WIDTH;
    private int HEIGHT = UVCCamera.DEFAULT_PREVIEW_HEIGHT;

    private Object mSync = new Object();
    private USBMonitor mUSBMonitor;
    private MyUVCCameraHandler mCameraHandler;

    private UVCCameraTextureView mCameraViewL;
    private BitmapSurfaceView mCameraViewR;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_preprocess_test);
        mCameraViewL = findViewById(R.id.camera_view_L);
        mCameraViewL.setAspectRatio(WIDTH / (float) HEIGHT);
        mCameraViewL.setCallback(mCallback);
        mCameraViewR = findViewById(R.id.camera_view_R);

        synchronized (mSync) {
            mUSBMonitor = new USBMonitor(this, mOnDeviceConnectListener);
            mCameraHandler = MyUVCCameraHandler.createHandler(this, mCameraViewL, WIDTH, HEIGHT);
        }

        // 开启UVCCamera授权提示对话框
        CameraDialog.showDialog(this);
    }

    @Override
    protected void onStart() {
        super.onStart();
        synchronized (mSync) {
            mUSBMonitor.register();
        }
        if (mCameraViewL != null) {
            mCameraViewL.onResume();
        }
    }

    @Override
    protected void onStop() {
        synchronized (mSync) {
            mCameraHandler.close();
            mUSBMonitor.unregister();
        }
        if (mCameraViewL != null) {
            mCameraViewL.onPause();
        }
        super.onStop();
    }

    @Override
    protected void onDestroy() {
        synchronized (mSync) {
            if (mCameraHandler != null) {
                mCameraHandler.release();
                mCameraHandler = null;
            }
            if (mUSBMonitor != null) {
                mUSBMonitor.destroy();
                mUSBMonitor = null;
            }
        }
        mCameraViewL = null;
        super.onDestroy();
    }

    @Override
    public USBMonitor getUSBMonitor() {
        return mUSBMonitor;
    }

    @Override
    public void onDialogResult(boolean canceled) {

    }

    private CameraViewInterface.Callback mCallback = new CameraViewInterface.Callback() {
        @Override
        public void onSurfaceCreated(CameraViewInterface view, Surface surface) {
            // 当TextureView的Surface被创建时,将其添加至CameraHandler中保存并管理。
            mCameraHandler.addSurface(surface.hashCode(), surface, false);
        }

        @Override
        public void onSurfaceChanged(CameraViewInterface view, Surface surface, int width, int height) {

        }

        @Override
        public void onSurfaceDestroy(CameraViewInterface view, Surface surface) {
            // 当TextureView的Surface销毁时,将其从CameraHandler中移除。
            mCameraHandler.removeSurface(surface.hashCode());
        }
    };

    private USBMonitor.OnDeviceConnectListener mOnDeviceConnectListener = new USBMonitor.OnDeviceConnectListener() {
        @Override
        public void onAttach(UsbDevice device) {

        }

        @Override
        public void onDettach(UsbDevice device) {

        }

        @Override
        public void onConnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock, boolean createNew) {
            synchronized (mSync) {
                // 当检测到USB连接时
                if (mCameraHandler != null) {
                    // 开启摄像头
                    mCameraHandler.open(ctrlBlock);
                    // 开启预览,CameraHandler会将图像绘制至关联的Surface上
                    mCameraHandler.startPreview();
                    // 开启YUV数据转视频流(H.264编码)
                    mCameraHandler.startRecording(null, onEncodeResultListener);
                    // 设置YUV帧数据监听
                    mCameraHandler.setOnPreViewResultListener(mOnPreViewResultListener);
                }
            }
        }

        @Override
        public void onDisconnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock) {
            synchronized (mSync) {
                // 当检测到USB断开时,关闭CameraHandler
                if (mCameraHandler != null) {
                    queueEvent(new Runnable() {
                        @Override
                        public void run() {
                            if (mCameraHandler != null) {
                                mCameraHandler.close();
                            }
                        }
                    }, 0);
                }
            }
        }

        @Override
        public void onCancel(UsbDevice device) {

        }
    };

    /**
     * H.264视频编码数据流
     */
    AbstractUVCCameraHandler.OnEncodeResultListener onEncodeResultListener = new AbstractUVCCameraHandler.OnEncodeResultListener() {
        @Override
        public void onEncodeResult(byte[] data, int offset, int length, long timestamp, int type) {
            // 这里可以使用rtmp进行推流...
        }

        @Override
        public void onRecordResult(String videoPath) {
            
        }
    };

    /**
     * 摄像头YUV数据流
     */
    AbstractUVCCameraHandler.OnPreViewResultListener mOnPreViewResultListener = new AbstractUVCCameraHandler.OnPreViewResultListener() {
        @Override
        public void onPreviewResult(byte[] data) { // data就是摄像头获取到的nv21格式的yuv数据
            try {
                ...
                ---------- 1、使用YuvUtil进行yuv数据处理 ----------
                ---------- 2、将处理后的yuv数据转成Bitmap传给SurfaceView绘制 ----------
                ...
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };

}

5、YuvUtil对yuv图像进行处理

这里针对yuv图像处理提供了2个方法,分别是:

  1. yuvProcessAndDraw1():完完全全手动处理好每一步的图像处理,自由度较高,相对的,也麻烦。
  2. yuvProcessAndDraw2():使用YuvUtil.yuvCompress()一步完成除裁剪以外的图像处理操作,比较便捷。

在上面AbstractUVCCameraHandler.OnPreViewResultListener的onPreviewResult(byte[] data)回调中,可以任意选择这2个方法中的1个进行处理,效果是一样的。

/**
 * 使用YuvUtil完全手动处理YUV图像数据,要求理解byte[]的创建长度:
 * yuvNV21ToI420():nv21转i420
 * yuvMirrorI420():镜像
 * yuvScaleI420():缩放
 * yuvCropI420():裁剪
 * yuvRotateI420():旋转
 * yuvI420ToNV21():i420转nv21
 *
 * @param data 摄像头获取到的nv21数据
 */
private void yuvProcessAndDraw1(byte[] data) {
    int width = WIDTH;
    int height = HEIGHT;

    // nv21 --> i420
    byte[] nv21Data = data;
    byte[] i420Data = new byte[width * height * 3 / 2];
    YuvUtil.yuvNV21ToI420(nv21Data, width, height, i420Data);

    // 镜像
    byte[] i420MirrorData = new byte[width * height * 3 / 2];
    YuvUtil.yuvMirrorI420(i420Data, width, height, i420MirrorData);
    i420Data = i420MirrorData;

    // 缩放
    byte[] i420ScaleData = new byte[width * height * 3 / 2];
    int scaleWidth = 320;
    int scaleHeight = 240;
    YuvUtil.yuvScaleI420(i420Data, width, height, i420ScaleData, scaleWidth, scaleHeight, 0);
    i420Data = i420ScaleData;
    width = scaleWidth;
    height = scaleHeight;

    // 裁剪
    byte[] i420CropData = new byte[width * height * 3 / 2];
    int cropWidth = 240;
    int cropHeight = 240;
    YuvUtil.yuvCropI420(i420Data, width, height, i420CropData, cropWidth, cropHeight, 0, 0);
    i420Data = i420CropData;
    width = cropWidth;
    height = cropHeight;

    // 旋转
    byte[] i420RotateData = new byte[width * height * 3 / 2];
    int degree = 90;
    YuvUtil.yuvRotateI420(i420Data, width, height, i420RotateData, degree);
    i420Data = i420RotateData;
    if (degree == 90 || degree == 270) {
        int temp = width;
        width = height;
        height = temp;
    }

    // i420 --> nv21
    YuvUtil.yuvI420ToNV21(i420Data, width, height, nv21Data);

    // 绘制图像
    drawSurfaceView(data, width, height);
}

/**
 * 使用YuvUtil半自动处理YUV图像数据:
 * yuvCompress():nv21转i420、镜像、缩放、旋转
 * yuvCropI420():裁剪
 * yuvI420ToNV21():i420转nv21
 *
 * @param data 摄像头获取到的nv21数据
 */
private void yuvProcessAndDraw2(byte[] data) {
    int width = WIDTH;
    int height = HEIGHT;
    int dstWidth = 320;
    int dstHeight = 240;

    // nv21 --> i420 --> 镜像 --> 缩放 --> 旋转
    byte[] nv21Data = data;
    byte[] i420Data = new byte[dstWidth * dstHeight * 3 / 2];
    int degree = 90;
    YuvUtil.yuvCompress(nv21Data, width, height, i420Data, dstWidth, dstHeight, 0, 90, true);
    // 旋转过后,需要手动校正宽高
    if (degree == 90 || degree == 270) {
        width = dstHeight;
        height = dstWidth;
    } else {
        width = dstWidth;
        height = dstHeight;
    }

    // 裁剪
    byte[] i420CropData = new byte[width * height * 3 / 2];
    int cropWidth = 240;
    int cropHeight = 240;
    YuvUtil.yuvCropI420(i420Data, width, height, i420CropData, cropWidth, cropHeight, 0, 0);
    i420Data = i420CropData;
    width = cropWidth;
    height = cropHeight;

    // i420 --> nv21
    YuvUtil.yuvI420ToNV21(i420Data, width, height, nv21Data);

    // 绘制图像
    drawSurfaceView(data, width, height);
}

/**
 * 使用SurfaceView绘制Bitmap图像
 * @param data nv21数据
 * @param width 图像宽
 * @param height 图像高
 */
private void drawSurfaceView(byte[] data, int width, int height) {
    YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, width, height, null);
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, out);
    byte[] bytes = out.toByteArray();
    Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    mCameraViewR.drawBitmap(bitmap);
}

要注意的点有2个:

  1. 要明白每次创建byte[]时的长度是多少。
  2. 要知道旋转如果是90或270,则宽高需要对调。

录制了一小段屏幕,左边是使用UVCCameraTextureView预览USB摄像头镜像后图像,右边是使用YuvUtil对yuv数据进行 镜像、旋转、缩放、裁剪 后的图像,分辨率640*480,流畅度还可以,是镜面效果,完美,撒花。

7、Github

https://github.com/GitLqr/LQRLibyuv

欢迎关注微信公众号:全栈行动

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK