Android NV21 转 YUV 系列格式

发布时间:2026/5/20 0:55:52

Android NV21 转 YUV 系列格式 一、先理清概念NV21 与常见 YUV 格式的区别NV21安卓摄像头默认输出格式存储顺序为「Y 平面 → UV 平面U 和 V 交替存储V 在前」属于 YUV420SP 类型I420/YUV420PPlanar 格式存储顺序为「Y 平面 → U 平面 → V 平面」3 个独立平面YV12Planar 格式存储顺序为「Y 平面 → V 平面 → U 平面」转换核心是拆分 UV 分量按目标格式重新排列。二、方案 1Java 层实现易理解适合小尺寸图像NV21 转 I420最常用/** * NV21 转 I420YUV420P * param nv21 原始 NV21 数据 * param width 图像宽度 * param height 图像高度 * return I420 数据 */publicstaticbyte[]nv21ToI420(byte[]nv21,intwidth,intheight){if(nv21null||nv21.length!width*height*3/2){thrownewIllegalArgumentException(NV21 数据长度不匹配);}byte[]i420newbyte[width*height*3/2];intySizewidth*height;// 第一步复制 Y 平面NV21 和 I420 的 Y 分量完全一致System.arraycopy(nv21,0,i420,0,ySize);// 第二步拆分 NV21 的 UV 平面为 I420 的 U、V 平面intuvIndexySize;// NV21 中 UV 起始位置intuIndexySize;// I420 中 U 起始位置intvIndexySize(width*height/4);// I420 中 V 起始位置// NV21 的 UV 是 VU 交替步长 2for(inti0;iwidth*height/2;i2){i420[vIndex]nv21[uvIndex];// V 分量i420[uIndex]nv21[uvIndex];// U 分量}returni420;}NV21 转 YV12/** * NV21 转 YV12 * param nv21 原始 NV21 数据 * param width 图像宽度 * param height 图像高度 * return YV12 数据 */publicstaticbyte[]nv21ToYv12(byte[]nv21,intwidth,intheight){if(nv21null||nv21.length!width*height*3/2){thrownewIllegalArgumentException(NV21 数据长度不匹配);}byte[]yv12newbyte[width*height*3/2];intySizewidth*height;// 复制 Y 平面System.arraycopy(nv21,0,yv12,0,ySize);intuvIndexySize;intvIndexySize;// YV12 中 V 在前intuIndexySize(width*height/4);// YV12 中 U 在后for(inti0;iwidth*height/2;i2){yv12[vIndex]nv21[uvIndex];// V 分量yv12[uIndex]nv21[uvIndex];// U 分量}returnyv12;}三、方案 2C 层实现性能最优适合实时采集Java 层循环操作字节数组效率低安卓摄像头实时采集场景建议用 JNI/C 实现下面是完整的 JNI 代码C 核心转换函数nv21_convert.cpp#includejni.h#includestring.h// NV21 转 I420externCJNIEXPORTjbyteArrayJNICALLJava_com_example_nv21convert_Nv21ConvertUtils_nv21ToI420(JNIEnv*env,jobject/* this */,jbyteArray nv21,jint width,jint height){// 获取 NV21 数据指针jbyte*nv21Dataenv-GetByteArrayElements(nv21,NULL);intySizewidth*height;inttotalSizeySize*3/2;// 创建 I420 数组jbyteArray i420Arrayenv-NewByteArray(totalSize);jbyte*i420Dataenv-GetByteArrayElements(i420Array,NULL);// 复制 Y 分量memcpy(i420Data,nv21Data,ySize);// 拆分 UV 分量jbyte*nv21UVnv21DataySize;jbyte*i420Ui420DataySize;jbyte*i420Vi420DataySizeySize/4;for(inti0;iySize/2;i2){*i420V*nv21UV;// V*i420U*nv21UV;// U}// 释放资源env-ReleaseByteArrayElements(nv21,nv21Data,0);env-ReleaseByteArrayElements(i420Array,i420Data,0);returni420Array;}Java 层调用 JNI 方法publicclassNv21ConvertUtils{static{// 加载编译好的 libnv21convert.soSystem.loadLibrary(nv21convert);}// 声明 JNI 方法publicnativebyte[]nv21ToI420(byte[]nv21,intwidth,intheight);// 调用示例publicstaticvoidtestConvert(){// 假设从摄像头获取的 NV21 数据byte[]nv21Datanewbyte[1920*1080*3/2];// 1080P 数据intwidth1920;intheight1080;Nv21ConvertUtilsutilsnewNv21ConvertUtils();byte[]i420Datautils.nv21ToI420(nv21Data,width,height);}}CMakeLists.txt 配置编译 C 为 .socmake_minimum_required(VERSION3.22.1)project(nv21convert)add_library(nv21convertSHAREDsrc/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~255UV 分量范围 -128~127。总结1 NV21 转 YUV 核心是复用 Y 分量拆分并重新排列 UV 分量2 Java 实现易理解但性能低C 实现适合实时场景是安卓音视频开发的主流方案3 转换前务必校验数据长度JNI 层注意内存释放避免崩溃 / 内存泄漏。如果需要适配特定场景比如摄像头预览回调、RTMP 推流前的格式转换我可以给你完整的端到端代码。

相关新闻