安卓camera2获取到的YUV420_888格式详解

news2024/11/26 4:47:59

da3b4b414f0ae8e48d0b5e7bf999c07c.jpeg

6f47d916faa5e986d8996cab97e13497.gif

本文字数:7885

预计阅读时间:45分钟

d23131023d9c6f41b96947aa1463c5c7.png

安卓音视频开发中的一个环节是摄像头采集数据,而 Android 平台上摄像头采集的 API 有两套,camera1 和 camera2。本文主要讲的是 camera2 这套 API 在采集数据并指明 YUV420_888 格式时,获取到的摄像头 YUV 数据格式具体是怎么样的。

01

背景/问题

之所以写这篇文章也是因为作者在开发过程中遇到了一些坑,然后在网上查阅资料后总结了一下内容。首先先说一下我遇到的问题:按照 API 的写法,获取摄像头数据,是在预览的回调中去获取数据,常用的会设置获取数据的格式为 YUV_420_888 ,如下:mImageReader = ImageReader.newInstance(1920, 1080, ImageFormat.YUV_420_888,2)

然后在回调中去获取数据,比如如下:mImageReader.setOnImageAvailableListener

private byte[] y;
    private byte[] u;
    private byte[] v;
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void onImageAvailable(ImageReader reader) {
        Image image = reader.acquireNextImage();
        // Y:U:V == 4:2:2
        if (image.getFormat() == ImageFormat.YUV_420_888) {
            Image.Plane[] planes = image.getPlanes();
            // 加锁确保y、u、v来源于同一个Image
            lock.lock();
            // 重复使用同一批byte数组,减少gc频率
            if (y == null || u == null || v == null) {
                y = new byte[planes[0].getBuffer().limit() - planes[0].getBuffer().position()];
                u = new byte[planes[1].getBuffer().limit() - planes[1].getBuffer().position()];
                v = new byte[planes[2].getBuffer().limit() - planes[2].getBuffer().position()];
            }
            if (image.getPlanes()[0].getBuffer().remaining() == y.length) {
                planes[0].getBuffer().get(y);
                planes[1].getBuffer().get(u);
                planes[2].getBuffer().get(v);
                
                encodeData(y,u,v);    //拿到y,u,v数据然后处理
            }
            lock.unlock();
        }
        image.close();
    }
}, mainHandler);

作者在一开始的时候,觉得拿到 yuv 数据了,那就可以直接拿去处理了,但是发现最终出来的图像是有问题的。于是觉得有点蹊跷,就开始去查阅相关资料。下面继续:

02

YUV420格式基础知识回顾

我们先回顾下 YUV420 格式相关的基础知识。

首先关于 YUV420 的采样,它并不是指只采样 U 分量而不采样 V 分量。而是指,在每一行扫描时,只扫描一种色度分量(U 或者 V),和 Y 分量按照 2 : 1 的方式采样。比如,第一行扫描时,YU 按照 2 : 1 的方式采样,那么第二行扫描时,YV 分量按照 2:1 的方式采样。对于每个色度分量来说,它的水平方向和竖直方向的采样和 Y 分量相比都是 2:1。

而采样后就是存储格式了,在日常开发中,打交道较多的主要也是围绕存储格式。主要有 YUV 420PYUV 420SP 两种类型,每个类型又对应其他具体格式,常见的基于 YUV 4:2:0 采样的格式如下表:


先U后V

先V后U

YUV420P类型

YU12格式

YV12格式

YUV420SP类型

NV12格式

NV21格式

YUV420PYUV420SP 都是先存储所有的 Y 分量后再存储 UV 分量。在存储 Y 分量后,YUV420P 类型会先存储所有的 U 分量或者 V 分量,而 YUV420SP 则是按照 UV或者 VU 的交替顺序进行存储,具体可看下图:

a5244dd73885d4d4664ce17fd771bacd.jpegYUV420P 的格式(以YU12格式为例子)

eb1dfe61dc74f563f639b2e7a779ac6e.jpegYUV420SP 的格式(以NV12格式为例子)

1、YU12

YU12YV12 格式都属于 YUV 420P 类型,即先存储 Y 分量,再存储 UV 分量,区别在于:YU12 是先 YUV,而 YV12 是先 YV 后 U

YV 12 的存储格式如下图所示:

c30271f86aff07e2e636185b76a3ca57.jpeg

而 YU 12 又称作 I420 格式,它的存储格式就是类似 YV12 但是把 VU 反过来了。

2、NV12和NV21格式

NV12NV21 格式都属于 YUV420SP 类型。它也是先存储了 Y 分量,但接下来并不是再存储所有的 U 或者 V 分量,而是把 UV 分量交替连续存储。

NV12 是 iOS 中常有的模式,它的存储顺序是先存 Y 分量,再 UV 进行交替存储。

a4ffcaf3fc3698bb9d2d51f205fa0a85.jpeg

NV21 是安卓中常有的模式,它的存储顺序是先存 Y 分量,再 VU 交替存储。

03

相关API的官方解释

在 Android 的官方文档上会对这个 YUV_420_888 获取到的格式作一些说明:https://developer.android.com/reference/android/graphics/ImageFormat.html#YUV_420_888

大体意思呢,结合网上大佬们的解读,总结如下:

  1. 它是 YCbCr 的泛化格式,不会具体指明是YU12YV12NV12 ,或是 NV21 。它能够表示任何4:2:0的平面和半平面格式,每个分量用8 bits 表示;

  2. 带有这种格式的图像使用3个独立的 Buffer 表示,每一个 Buffer 表示一个颜色平面( Plane ),除了 Buffer 外,它还提供 rowStridepixelStride 来描述对应的 Plane 。这两个 rowStridepixelStride 是重点。下面会讲解;

  3. 使用 ImagegetPlanes() 获取 plane 数组:Im age.Plane[] planes = image.getPlanes();

    它保证 planes[0] 总是 Yplanes[1] 总是 U(Cb), planes[2] 总是 V(Cr) 。并保证 Y-Plane 永远不会和 U/V 交叉;

  4. U/V-Plane总是有相同的rowStridepixelStride

04

YUV420_888格式详解

经过上面的官方解读,能大体知道意思,但是具体是怎么样的呢?下面就来讲解一下。首先我们知道 yuv420 分为 Planar 格式(P)和 Semi-Planar 格式(SP),这两种由于存储格式不同,在这里的获取的三个 planes 中的分布也会非常不同。

1、rowStride和pixelStride

先来解读两个最重要的参数:

(1) pixelStride(通过 getPixelStride()获得):像素步长,有可能是1、有可能是2。它代表的是行内连续两个颜色值之间的距离(步长)。

也就是说,如果是1,那么每一行中的同一个颜色分量,比如Y分量,是连续的,也就是行内索引为0,1,2,...的颜色分量都是它的。如果是2,那么每一行中的同一个颜色分量,是不连续的,是会中间间隔1个元素的,也就是行内索引为0,2,4,6,...的颜色分量才是它的。

这里还有个重要的点:假如是步长为2,意味索引间隔的原色才是有效的元素,中间间隔的元素其实是没有意义的。而 Android 中确实也是这么做的,比如某个 plane[1]U分量)的步长是2,那么数组下标0,2,4,6,...的数据就是 U 分量的,而中间间隔的元素 Android 会补上 V 分量,也就是会是 UVUVUV......这样去分布。但是当最后一个 U 分量出现后,最后一个没有意义的元素 Android 就不补了,也就是最后的 V 分量就不会补了,直接结束,即是这样分布:UVUVUV...UVUVU

(2) rowStride:(通过 getRowStride()获得)“每行数据”的“宽度”,这个跟分辨率的宽度不是同个回事,它是每一行实际存储的空间宽度,下面的分析会再详细讲述。

2、其他相关参数

除了上面2个重要参数外,还有几个参数也是需要了解的,如下:

(1)width 和 height

       通过 getWidth()getHeight() 获得,一般与视频分辨率一致。

(2)buffer size

这个主要就是 plane 数组的大小,一般就通过 plane[i].length 获取即可。

3、YUV数据的分布和排列

解读了 rowStridepixelStride 这两个参数后,我们可以假设一些例子来理解一下拿到的 YUV 数据的排列。理论上,这两个参数在每个 plane 中是可以任意设置的,这样组合起来的格式可能也是多种多样的。但是在实际场景中,由于 YUV420 分为 Planar 格式(P)和 Semi-Planar 格式(SP)两大类存储格式。所以实际上我们遇到的大体上会分为下面两大类:

(1)Planar 格式(P):

先看一下6*4的假设图片:

532440a911a25202571c301a5e8d6d03.jpeg

plane[0]pixelStride 是1,说明没有间隔,Y 是连续的,rowStride 是6,也就是每行6个,length 数量是24,24/6 = 4,共4行。

plane[1]的 pixelStride 也是1,说明没有间隔,U 是连续的,rowStride 是3,也就是每行3个,length 数量是6,6/3 = 2,共2行,符合 YUV420 的情况,横纵都是2:1采样。

plane[2]plane[1]相同。

这种其实就是 YUV420P 的标准格式,跟我们期望的差不多,不用做多解析,直接按照这样将 yuv分别取出,即是正确的数据。可惜的是,目前测到的手机大部分不是这样的格式,而是下面要介绍的这类 SP 的情况出现的多一些。

(2)Semi-Planar格式(SP):

还是先看一下6*4的假设图片:

b0b14520a8e021d96f18ff6e102a4c16.jpeg

plane[0]pixelStride 是1,说明没有间隔,Y是连续的,rowStride 是6,也就是每行6个,length数量是24,24/6 = 4,共4行。这个 Y 分量跟 Planar 格式是一样的。

plane[1]pixelStride 是2,说明有间隔,U是间隔采取的,这里就回到上面我们分析的两个参数的时候,当 pixelStride 为2的时候,在 U 分量中,就会间隔插入了 V 分量,因此每一行由本来是 Y 的一半也就是3,变成了6(也就是 rowStride 的值)。同时就像上面分析的一样,会放弃掉最后一个无意义的V分量,所以就 length 会看到是6*2-1=11的,行数还是2,纵向是不变的。

plane[2]plane[1]相同

对于这种 Semi-Planar格式的,安卓提供的这种方式确实就让人很意想不到,在这种格式下,其实我们有几种取数据方式,首先Y是完整的,直接取即可。对于UV分量可以有2种方式:

  1. plane[1] 中以索引0,2,4间隔方式去取 U 分量,plane[2] 中以索引0,2,4间隔方式去取 V 分量,这样就取到了最准确的 UV分量;

  2. 我们其实可以看到,在 plane[1] 中其实就包含了 UV 分量了,只不过丢掉了最后一个 V ,对于人眼来说,少点一个 V,是完全没有影响的。因此其实可以直接拿 plane[1] 的数据,就拿到 UVplane[2] 同理其实也是有 VU ,那么这样的话,其实就可以 plane[0] + plane[1] 可得 NV12格式;或者 plane[0] + plane[2] 可得 NV21格式。Semi-Planar 格式在大多数手机中会经常出现,经过上面的分析也能理解为什么 U 和 VrowStride 会和 Y 一样,而不是一半。以及为什么 UV 的数量最后会少一个分量的原因。

(3)特殊情况:

rowStride 除了有 P 和 SP 格式而导致不同之外,它其实还有一个重要的作用,就是在一些特殊的摄像头sensor 采集的时候,因为芯片处理器要字节对齐取数据等原因而导致的补齐操作,从而使得每一行所占的空间比实际数据要多。

我们举个例子,比如还是图像是6*4的,但是由于字节补齐操作等原因,相机输出的时候,假如rowStride+2 了,如下:

d76b94221e8e83f183b6387dfe840d8d.jpeg

plane[0]pixelStride 是1,说明没有间隔,Y 是连续的,rowStride 本来应该是6,但是这里是8,后面补了两个空的字节,也就是每行8个,length 数量是32,32/8 = 4,共4行。这里分析的时候可以这样判断,getWidth()getHeight() 获取到是6和4,6*4是24,发现32与24不对应,就可以初步判断是有补齐的情况了。而 YrowStride 是8,比 getWidth() 的6多了2,也就可以推测是每行补齐了2个字节。

这样的话在取数据的时候,就需要每行都去丢弃最后的2个空字节。同理 plane[1]plane[2] 也是类似。因此对于这类特殊的 camera,我们需要根据pixelStriderowStride 与分辨率的关系,去进行一些特殊的处理才行。

不过目前来说,现在的手机厂商会尽可能避免这种情况,因此出现这种特殊情况的可能性也是比较低的。

05

总结

根据上面的总体分析,可以总结如下:

  1. 根据 rowStridepixelStride与分辨率的关系,判断是否有补齐的特殊情况,若有,则需要对数据进行空字节丢弃的处理。一般大部分的手机都不会有这么特殊的情况。若没有,则属于正常情况;

  2. 正常情况下,先判断 pixelStride 的值,可以知道是 P 还是 SP 存储格式;若 pixelStride 为1,是 P 格式,直接可以顺序取 YUV ;若 pixelStride 为2,则可以考虑上述第三点中 Semi-Planar 格式的取数据方式。

06

问题解决

最后回到我一开始遇到的问题,之所以会出问题就是因为我没有判断 pixelStriderowStride 就直接取了,实际上我的手机是以 SP 的方式的,所以 UVpixelStride 是2,所以没法直接赋值。根据这种逻辑,我修改代码如下,判断 pixelStride ,并采用分别取 UV 的方式,暂时不考虑字节补齐的特殊情况。

private byte[] y;
	private byte[] u;
	private byte[] v;
	private ReentrantLock lock = new ReentrantLock();
	@Override
	public void onImageAvailable(ImageReader reader) {
		Image image = reader.acquireNextImage();
		// Y:U:V == 4:2:2
		if (image.getFormat() == ImageFormat.YUV_420_888) {
			Image.Plane[] planes = image.getPlanes();
			// 加锁确保y、u、v来源于同一个Image
			lock.lock();
			// 重复使用同一批byte数组,减少gc频率
			if (y == null || u == null || v == null) {
				y = new byte[planes[0].getBuffer().limit() - planes[0].getBuffer().position()];
				u = new byte[planes[1].getBuffer().limit() - planes[1].getBuffer().position()];
				v = new byte[planes[2].getBuffer().limit() - planes[2].getBuffer().position()];
			}
			if (image.getPlanes()[0].getBuffer().remaining() == y.length) {
				planes[0].getBuffer().get(y);
				planes[1].getBuffer().get(u);
				planes[2].getBuffer().get(v);

				//数据前期处理
				//判断是p还是sp
				if(planes[1].getPixelStride() == 1){   //p
					//无须处理
					encodeDataTopush(y,u,v);
				}else if(planes[1].getPixelStride() == 2){      //sp
//这里需要+1,需要注意,具体原理不再展开阐述
					byte[] temp_u = new byte[(u.length+1)/2];       
					byte[] temp_v = new byte[(v.length+1)/2];
					int index_u = 0;
					int index_v = 0;
//提取U分量
					for(int i=0;i < u.length;i++){
						if(0 == (i%2)){
							temp_u[index_u] = u[i];
							index_u++;
						}
					}
//提取V分量
					for(int j=0;j < v.length;j++){
						if(0 == (j%2)){
							temp_v[index_v] = v[j];
							index_v++;
						}
					}
					encodeData(y,temp_u,temp_v);
				}
			}
			lock.unlock();
		}
		image.close();
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1092972.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软件设计师_面向对象_学习笔记

文章目录 1 面向对象基本概念2 设计模式3 UML4 设计模式4.1 设计模式的基本概念4.2 设计模式的分类4.3 创建型模式 1 面向对象基本概念 2 设计模式 3 UML 4 设计模式 4.1 设计模式的基本概念 模式&#xff1a;通俗的来说就是成功方案的复用。 架构模式从全局看待问题。设计模式…

芯片学习记录TLP291-4

TLP291-4 芯片介绍 东芝TLP291-4由光学耦合到红外LED的光电晶体管组成。TLP291-4光电耦合器安装在非常小而薄的SO16封装中。由于TLP291-4在宽工作温度范围内得到保证&#xff08;Ta-55至110&#xff09;&#xff0c;因此适用于高密度表面贴装应用&#xff0c;例如可编程控制器…

快速排序全面详解

目录 1 基本思想 2 排序步骤 3 代码实现 3.1 区间划分算法&#xff08;hoare初始版本&#xff09;&#xff1a; 3.2 主框架 4 区间划分算法 4.1 hoare法 4.2 挖坑法 4.3 前后指针法 5 快排优化 5.1 取key方面的优化 5.2 递归方面的优化 5.3 区间划分方面的优化 6…

终极Whois查询工具:优雅美观、功能强大、信息全面

1. 引言 这个程序的适用面不是很广&#xff0c;但对于域名爱好者&#xff0c;我想这会是一个不错的工具。 查询一个域名的Whois&#xff0c;这样的工具有很多。但是显示的数据却是有点差强人意&#xff0c;一次偶然的机会发现了 who.cx 这个whois工具&#xff0c;不得不说界面…

Android组件通信——Service(二十七)

1. Service 1.1 知识点 &#xff08;1&#xff09;掌握Service与Activity的区别&#xff1b; &#xff08;2&#xff09;掌握Service的定义及使用&#xff1b; &#xff08;3&#xff09;可以使用ServiceConnection 接口绑定一个Service&#xff1b; &#xff08;4&#x…

java js 经纬度转换 大地坐标(高斯投影坐标)与经纬度互相转换

项目中有大地坐标(高斯投影坐标)与经纬度互相转换的需求 写了个工具类; 有java和js代码 如图 java代码中将坐标系和带宽已作为参数传入方法,使用时只需调用不同方法: js端没有将坐标系和带宽作为参数 如果有需要可以替换注释地方 或者自行修改为传参方式: 接下来贴代码: jav…

C++笔记之如何给 `const char*` 类型变量赋值

C笔记之如何给 const char* 类型变量赋值 code review! 文章目录 C笔记之如何给 const char* 类型变量赋值1.在C中&#xff0c;如果你要给一个 const char* 变量赋值&#xff0c;你通常有几种方法来做这件事&#xff0c;具体取决于你的需求。下面是一些常见的方法&#xff1a;…

使用JAVA发送邮件

这里用java代码编写发送邮件我采用jar包&#xff0c;需要先点击这里下载三个jar包&#xff1a;这三个包分别为&#xff1a;additionnal.jar&#xff1b;activation.jar&#xff1b;mail.jar。这三个包缺一不可&#xff0c;如果少添加或未添加均会报下面这个错误&#xff1a; C…

芯片学习记录SN74HC14DR

SN74HC14DR 芯片介绍 该设备包含六个独立的逆变器使用施密特触发器输入。每个门执行正逻辑中的布尔函数Y/A("/"表示“非”)。 引脚信息 引脚名称I/O电平功能11AI0~vcc输入21YO0~vcc输出7GND-电源14VCC-3.3v电源&#xff08;2~6V&#xff09;Y/A 推荐使用条件 参数…

jwt的使用概念工具类与切入spa项目

jwt的概念 JWT&#xff08;JSON Web Token&#xff09;是一种用于身份验证和授权的开放标准&#xff0c;它是一种轻量级的、安全的、基于JSON的令牌机制。 JWT由三部分组成&#xff1a;头部&#xff08;Header&#xff09;、载荷&#xff08;Payload&#xff09;和签名&#…

C++笔记之std::async的用法

C笔记之std::async的用法 code review! 文章目录 C笔记之std::async的用法1.概念2.C 异步任务的使用示例 - 使用 std::async 和 std::future3. std::launch::async 和 std::launch::deferred 1.概念 std::async 是 C 标准库中的一个函数&#xff0c;用于创建异步任务&#xf…

leetCode 1035.不相交的线 动态规划 + 滚动数组 (最长公共子序列)

1035. 不相交的线 - 力扣&#xff08;LeetCode&#xff09; 在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 现在&#xff0c;可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线&#xff0c;这些直线需要同时满足满足&#xff1a; nums1[i] nums2[j]…

3.4 构造方法

思维导图&#xff1a; 3.4.1 定义构造方法 ### Java中的构造方法 #### **定义与目的** 构造方法&#xff0c;也称为构造器&#xff0c;是一个特殊的成员方法&#xff0c;用于在实例化对象时为对象赋值或执行初始化操作。其主要目的是确保对象在被创建时具有有效和合适的初始状…

学习记忆——数学篇——代数——记忆宫殿——卧室

明确需放置的大件物品 整式、分式 &#xff08;1&#xff09;整式&#xff1a;运算、因式定理 &#xff08;2&#xff09;分式&#xff1a;运算函数、方程与不等式 &#xff08;1&#xff09;函数&#xff1a;一元二次函数、 &#xff08;2&#xff09;方程&#xff1a;一元二…

Mysql高级——事务(2)

MySQL事务日志 事务有4种特性&#xff1a;原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢&#xff1f; 事务的隔离性由锁机制实现。 而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。 REDO LOG 称为重做日志&#xff0c;…

默认关闭idea2020的注释doc的rendered view模式(阅读模式)

idea2020的javadoc有了一个rendered模式,在开发的时候感觉很不习惯… 打开sessings,在编辑器的外观中取消这个选项,默认情况下doc就是编辑模式了,点击左侧的图标就会变为rendered view模式

输入法显示到语言栏_状态栏

设置–时间和语言–语言–最右侧"相关设置"中的"拼写、键入和键盘设置" 最下方的"高级键盘设置"–“使用桌面语言栏(如果可用)” 点击"语言栏选项" 接下来就是不同输入法的设置了 搜狗输入法:右键输入法选择"隐藏状态栏"–…

全新整合热搜榜单热门榜单内容系统聚合源码/带教程安装

源码简介&#xff1a; 在移动互联网时代&#xff0c;我们每天都会接收到大量的信息&#xff0c;但是想要知道哪些是最热门的话题和内容&#xff0c;往往需要花费很多精力去搜索和筛选。因为有这个需要&#xff0c;一个全新整合热搜榜单热门榜单内容系统聚合源码就应运而生了&a…

Zabbix监控系统详解2:基于Proxy分布式实现Web应用监控及Zabbix 高可用集群的搭建

文章目录 1. zabbix-proxy的分布式监控的概述1.1 分布式监控的主要作用1.2 监控数据流向1.3 构成组件1.3.1 zabbix-server1.3.2 Database1.3.3 zabbix-proxy1.3.4 zabbix-agent1.3.5 web 界面 2. 部署zabbix代理服务器2.1 前置准备2.2 配置 zabbix 的下载源&#xff0c;安装 za…

1.1 Windows驱动开发:配置驱动开发环境

在进行驱动开发之前&#xff0c;您需要先安装适当的开发环境和工具。首先&#xff0c;您需要安装Windows驱动开发工具包&#xff08;WDK&#xff09;&#xff0c;这是一组驱动开发所需的工具、库、示例和文档。然后&#xff0c;您需要安装Visual Studio开发环境&#xff0c;以便…