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 推流前的格式转换),我可以给你完整的端到端代码。

相关推荐
dialect_1 小时前
KMP算法
开发语言·c++·算法
总要学点什么2 小时前
第89篇:三维特效-螺旋圆效果——让螺旋臂卷起一圈圈外扩的涟漪
开发语言·javascript·ecmascript·webgl·cesium·空间分析·1024程序员节
前端精髓2 小时前
结构化输出with_structured_output(抽卡)
python·langchain
尹人入圣3 小时前
目前热门的IP驱动产业新场景新工具
网络·python·网络协议·tcp/ip
开开心心_Every3 小时前
快速隐藏程序工具自定义快捷键一键切换
java·开发语言·前端·spring·智能手机·word·intellij-idea
chenbingjie_c4 小时前
C++ 多态完全指南:从虚函数重写到虚函数表原理
开发语言·c++
Nuanyt4 小时前
Agent 学习笔记(下篇三):上线工程、进阶选择与操作界面(Stage 7 / 7.5 / 8 + 延伸路线)
笔记·python·学习·memory·aiagent·harness
吴卫斌4 小时前
定投ETF实战指南(四):行业/主题ETF——定投的“进攻端“
开发语言·python·股票·量化
2601_967987374 小时前
Python——入门级(定义功能def函数)
python·函数·可变参数·默认参数·关键字参数
超级架构师5 小时前
PRAXO-CORE 2.5.0:用 conformance 和显式边界构建 Go Agent 框架
开发语言·人工智能·后端·golang·ai编程·智能体框架·praxo-core