Android NV21 转 YUV 系列格式

一、先理清概念:NV21 与常见 YUV 格式的区别
NV21:安卓摄像头默认输出格式,存储顺序为「Y 平面 → UV 平面(U 和 V 交替存储,V 在前)」,属于 YUV420SP 类型;

I420/YUV420P:Planar 格式,存储顺序为「Y 平面 → U 平面 → V 平面」(3 个独立平面);

YV12:Planar 格式,存储顺序为「Y 平面 → V 平面 → U 平面」;

转换核心是拆分 UV 分量,按目标格式重新排列。

二、方案 1:Java 层实现(易理解,适合小尺寸图像)

  1. NV21 转 I420(最常用)
java 复制代码
/**
 * NV21 转 I420(YUV420P)
 * @param nv21 原始 NV21 数据
 * @param width 图像宽度
 * @param height 图像高度
 * @return I420 数据
 */
public static byte[] nv21ToI420(byte[] nv21, int width, int height) {
    if (nv21 == null || nv21.length != width * height * 3 / 2) {
        throw new IllegalArgumentException("NV21 数据长度不匹配");
    }

    byte[] i420 = new byte[width * height * 3 / 2];
    int ySize = width * height;

    // 第一步:复制 Y 平面(NV21 和 I420 的 Y 分量完全一致)
    System.arraycopy(nv21, 0, i420, 0, ySize);

    // 第二步:拆分 NV21 的 UV 平面为 I420 的 U、V 平面
    int uvIndex = ySize; // NV21 中 UV 起始位置
    int uIndex = ySize;  // I420 中 U 起始位置
    int vIndex = ySize + (width * height / 4); // I420 中 V 起始位置

    // NV21 的 UV 是 VU 交替,步长 2
    for (int i = 0; i < width * height / 2; i += 2) {
        i420[vIndex++] = nv21[uvIndex++]; // V 分量
        i420[uIndex++] = nv21[uvIndex++]; // U 分量
    }

    return i420;
}
  1. NV21 转 YV12
php 复制代码
/**
 * NV21 转 YV12
 * @param nv21 原始 NV21 数据
 * @param width 图像宽度
 * @param height 图像高度
 * @return YV12 数据
 */
public static byte[] nv21ToYv12(byte[] nv21, int width, int height) {
    if (nv21 == null || nv21.length != width * height * 3 / 2) {
        throw new IllegalArgumentException("NV21 数据长度不匹配");
    }

    byte[] yv12 = new byte[width * height * 3 / 2];
    int ySize = width * height;

    // 复制 Y 平面
    System.arraycopy(nv21, 0, yv12, 0, ySize);

    int uvIndex = ySize;
    int vIndex = ySize; // YV12 中 V 在前
    int uIndex = ySize + (width * height / 4); // YV12 中 U 在后

    for (int i = 0; i < width * height / 2; i += 2) {
        yv12[vIndex++] = nv21[uvIndex++]; // V 分量
        yv12[uIndex++] = nv21[uvIndex++]; // U 分量
    }

    return yv12;
}

三、方案 2:C++ 层实现(性能最优,适合实时采集)

Java 层循环操作字节数组效率低,安卓摄像头实时采集场景建议用 JNI/C++ 实现,下面是完整的 JNI 代码:

  1. C++ 核心转换函数(nv21_convert.cpp)
java 复制代码
#include <jni.h>
#include <string.h>

// NV21 转 I420
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_example_nv21convert_Nv21ConvertUtils_nv21ToI420(
        JNIEnv* env,
        jobject /* this */,
        jbyteArray nv21,
        jint width,
        jint height) {
    // 获取 NV21 数据指针
    jbyte* nv21Data = env->GetByteArrayElements(nv21, NULL);
    int ySize = width * height;
    int totalSize = ySize * 3 / 2;

    // 创建 I420 数组
    jbyteArray i420Array = env->NewByteArray(totalSize);
    jbyte* i420Data = env->GetByteArrayElements(i420Array, NULL);

    // 复制 Y 分量
    memcpy(i420Data, nv21Data, ySize);

    // 拆分 UV 分量
    jbyte* nv21UV = nv21Data + ySize;
    jbyte* i420U = i420Data + ySize;
    jbyte* i420V = i420Data + ySize + ySize / 4;

    for (int i = 0; i < ySize / 2; i += 2) {
        *i420V++ = *nv21UV++; // V
        *i420U++ = *nv21UV++; // U
    }

    // 释放资源
    env->ReleaseByteArrayElements(nv21, nv21Data, 0);
    env->ReleaseByteArrayElements(i420Array, i420Data, 0);

    return i420Array;
}
  1. Java 层调用 JNI 方法
java 复制代码
public class Nv21ConvertUtils {
    static {
        // 加载编译好的 libnv21convert.so
        System.loadLibrary("nv21convert");
    }

    // 声明 JNI 方法
    public native byte[] nv21ToI420(byte[] nv21, int width, int height);

    // 调用示例
    public static void testConvert() {
        // 假设从摄像头获取的 NV21 数据
        byte[] nv21Data = new byte[1920 * 1080 * 3 / 2]; // 1080P 数据
        int width = 1920;
        int height = 1080;

        Nv21ConvertUtils utils = new Nv21ConvertUtils();
        byte[] i420Data = utils.nv21ToI420(nv21Data, width, height);
    }
}
  1. CMakeLists.txt 配置(编译 C++ 为 .so)
java 复制代码
cmake_minimum_required(VERSION 3.22.1)
project("nv21convert")

add_library(
        nv21convert
        SHARED
        src/main/cpp/nv21_convert.cpp)

find_library(
        log-lib
        log)

target_link_libraries(
        nv21convert
        ${log-lib})

四、关键注意事项
数据长度校验 :NV21/YUV420 系列的总长度固定为 width * height * 3 / 2,转换前必须校验,避免数组越界;
性能选择:

小尺寸图像(如 640x480)用 Java 层即可,开发成本低;

大尺寸 / 实时场景(如 1080P 摄像头采集)必须用 C++ 层,效率提升 5~10 倍;
内存释放 :JNI 层操作字节数组后,必须调用 ReleaseByteArrayElements 释放资源,避免内存泄漏;
格式验证:转换后可通过「取像素点校验」验证正确性(比如 Y 分量值范围 0~255,UV 分量范围 -128~127)。

总结

1 NV21 转 YUV 核心是复用 Y 分量,拆分并重新排列 UV 分量;

2 Java 实现易理解但性能低,C++ 实现适合实时场景,是安卓音视频开发的主流方案;

3 转换前务必校验数据长度,JNI 层注意内存释放,避免崩溃 / 内存泄漏。

如果需要适配特定场景(比如摄像头预览回调、RTMP 推流前的格式转换),我可以给你完整的端到端代码。

相关推荐
add45a2 小时前
C++中的原型模式
开发语言·c++·算法
2401_844221322 小时前
C++类型推导(auto/decltype)
开发语言·c++·算法
2201_753877792 小时前
高性能计算中的C++优化
开发语言·c++·算法
无限进步_2 小时前
深入解析C++容器适配器:stack、queue与deque的实现与应用
linux·开发语言·c++·windows·git·github·visual studio
2501_945425152 小时前
分布式系统容错设计
开发语言·c++·算法
匆忙拥挤repeat2 小时前
Android Compose 《编程思想》解读
android
阿成学长_Cain2 小时前
Linux 命令:ldconfig —— 动态链接库管理命令
java·开发语言·spring
2401_884563242 小时前
C++代码重构实战
开发语言·c++·算法
技术小甜甜2 小时前
[Python实战] 用 pathlib 彻底统一文件路径处理,比字符串拼接稳得多
开发语言·人工智能·python·ai·效率化