初识YUV,实战NV21格式数据转换为Bitmap

  • 一、初识YUV
  • 二、采样方式
    • 2.1、YUV 4:4:4
    • 2.2、YUV 4:2:2
    • 2.2、YUV 4:2:0【重点】
  • 三、存储方式
    • 3.1、planar(平面方式)
    • 3.2、packed(打包方式)
  • 四、NV21数据转Bitmap
  • 五、总结
  • 参考

一、初识YUV

说到YUV我们就必须先从RGB说起了,如下图所示,请各位把每个色块当成一个像素点,也就是说请各位把下图当成一个宽度4像素,高度2像素的图片。通常一个像素点由RGB(Alpha我们先不考虑)三个分量混合而成。

如下图中第一个像素点,其RGB值分别为(236,102,93),而这三个值混合就组成了我们看到的 红色 。

然而上图中除了RGB值外,我还标出了每个像素点的YUV的值,YUV的值是怎么来的呢?直接给公式 :
请不要使用其他博客中的浮点数类型的公式,会严重影响精度

y = (( 66 * r + 129 * g + 25  * b + 128) >> 8) + 16  ;
u = ((-38 * r - 74  * g + 112 * b + 128) >> 8) + 128 ;
v = ((112 * r - 94  * g - 18  * b + 128) >> 8) + 128 ;

这时大家可能就有疑问了,我们明明已经有了RGB可以表示这个像素点了,为什么还需要再使用YUV来进行表示。

说白一点就是RGB三个分量一个都不可少才能表示出一个像素点。而YUV可以通过不同的采样方式来减少一些U、V分量,从而减小所需的存储空间。而恢复为RGB的时候可以几个Y分量共用U、V分量来恢复为RGB(具体也是跟人眼对颜色的识别有关联的,请参考YUV相关理论知识)。

上图中我们YUV分量下都加了下划线,表示需要都需要采样。这样全采样的YUV其实跟RGB所需存储空间一样了,而这种采样方式就是 YUV 4:4:4

二、采样方式

由于采样都是 4 : x : x,所以我们以每行四个像素为例,请记住每个色块代表一个像素!!!

采样的意思就是我们需要采集到的这些YUV分量的值,然后接下来我们需要存储下来这个值。所以根据采样、存储方式的不同,YUV才有那么多的格式。

2.1、YUV 4:4:4

这种方式也就是上文所说的,YUV分量全部进行采样,请看第一张图片即可;

2.2、YUV 4:2:2

在所有像素上,Y分量全部采样。
在同行的像素上, UV 分量分别 交替 进行采样;
其他行同理;

采样方式如下图U、V的下划线所示:

2.2、YUV 4:2:0【重点】

在所有像素上,Y分量全部采样。
在第0行(偶数行), U 分量 间隔 进行采样,而不采样V分量。
在第1行(奇数行), V 分量 间隔 进行采样,而不采样U分量。
以此类推。

采样方式如下图U、V的下划线所示:


以上采样的概念我们已经理清了,在采样时,我们可以临时使用 三个byte[]数组分别存储 采集到的Y、U、V的值。

以上面的8个像素为例,那么我们采集到的数组长度则分别为:

  • YUV 4:4:4
    8 + 8 + 8 长度为24
  • YUV 4:2:2
    8 + 4 + 4 长度为16,是第一种的 三分之二
  • YUV 4:2:0
    8 + 2 + 2 长度为12,是第一种的 二分之一

所以使用420的采样方式,所需的存储空间会大大减小。

三、存储方式

上面的YUV分量分别使用了一个数组来进行存储,但是这里的存储我们要说的是将上述三个数组合并为一个数组

三个数组合并为一个数组,这就有的说了,排列组合嘛,但是呢,也没那么随意,一起来学习下,我们把上面使用 YUV420 方式采样好的y、u、v分量数组分别命名为 arrayY,arrayU,arrayV 。因为我们示例的图片是宽度4像素,高度2像素,一个像素一个Y值,所以Y的最大采样数就是8个值,arrayY数组的长度就是8,其他数组长度就是它的 1/4。

byte[] arrayY = new byte[8];
byte[] arrayU = new byte[8/4];
byte[] arrayV = new byte[8/4];

那么采样完毕后,数组中的数据分别为:

Y数组: [-119, -126, -113, -69, -69, -113, -126, -119]
U数组: [104, -76]
V数组: [-93, 69]

嗯???

不对劲,为什么上面图中展示的都是正值,而这里的数组中却是负值呢?首先请注意下,我们开始的时候使用int类型来存储所有的rgb,yuv的值的,因为我们从Color中获取到RGB的值是int类型的,或者下文示例的时候我们从bitmap中获取到的像素点数据中的RGB值也是int类型的,所以上文就是用int值来示例了,也符合我们的认知习惯。

而前辈们在存储的时候采用的是byte数组,为什么呢,因为RGB的取值范围也就是0 - 255,而一个byte就可以存储从 [-128,127] 的值,也就是可以存储256个值,所以这其中的利害关系大家能体会的到吧。

看第一个像素Y的int值是137,存储中的byte值是 -119 ,你尝试用 256-119 计算下,结果是多少呢?(所以在做恢复为RGB值的时候也是要先处理为正值的,只不过恢复为RGB的过程目前我们先不用处理)

OK,上面是一个小插曲,接下来进入正题了,我们先将数组的值都抹去,这样示例更清晰些,如下示例:

Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
U数组: [U, U]
V数组: [V, V]

一般YUV的打包模式也就分为如下两种:

3.1、planar(平面方式)

这种方式就是将数组arrayY、arrayU、arrayV中的数据 按顺序 组合起来。那么这种顺序可以是 先存Y,再存U,最后存V。也可以是先存Y,再存V,再存U。我们这里把前者称为 YU的存储方式,把后者称为 YV的存储方式,所以:


  • 420采样方式 + YU存储方式 = YU12(又叫 I420
Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
U数组: [U, U]
V数组: [V, V]//为了更清晰的展示数据格式,做了改动
新数组:[YYYYYYYY   UU   VV]
  • 420采样方式 + YV存储方式 = YV12
Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
U数组: [U, U]
V数组: [V, V]//为了更清晰的展示数据格式,做了改动
新数组:[YYYYYYYY   VV   UU]

这两种存储格式呢,又统称为 YUV420P 格式。


上面两种是清晰易懂的平面方式,接下来的这种有点特殊,Y分量同样是先全部存储进去,然后需要将U、V分量先按照 UV 或者 VU 这样的方式先组合起来,然后再拼接到Y分量之后,如下数组示例:

Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
U数组: [U, U]
V数组: [V, V]//先将UV混合,为了让大家理解这还是 平面模式,之前是Y、U、V做平面,现在是Y、UV来做平面
Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
UV数组: [U, V, U, V]//为了更清晰的展示数据格式,做了改动
新数组:[YYYYYYYY   UVUV]

这下大家应该明了了,所以呢,除了这种 UV交替存储的,还有VU交替存储的,那么我们就把前者称为UV存储,把后者称为VU存储,那么总结来了:

  • 420采样方式 + UV存储方式 = NV12
  • 420采样方式 + VU存储方式 = NV21

上面这两种特殊的平面方式呢,又叫 Semi-Splanar ,所以以上两种格式又称为 YUV420SP 格式。


好了,到这里其实我们已经搞定了 420采样方式 的 4种 YUV的格式了,有YU12,YV12,NV12,NV21。我们还可以发现一个小小的规律,Y开头的表示正常平面存储方式,N开头的表示特殊平面存储方式。12或者21则表示前面两个分量的排列顺序。

比如YU12:平面存储方式,Y第一个存,U第二个存,那么当然的V就是第三个存了。
那么NV12怎么理解呢?特殊平面存储方式,其中UV是交错存储的,所以NV12理解为UV12,这就通了。同理NV21理解为UV21。至于怎么方便理解和记忆大家自行斟酌。

3.2、packed(打包方式)

一般我们使用422采样方式的时候会采用这种存储方式,这种方式就不像上面那种那么直白了,先用数组表示吧,注意是422采样模式,所以U、V数组长度也变化了,如下:

Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
U数组: [U, U, U, U]
V数组: [V, V, V, V]//为了更清晰的展示数据格式,做了改动
新数组:[YUYV    YUYV    YUYV    YUYV]

如上所示,因为YUV的比例是2:1:1 ,所以取两个Y元素就需要分别取一个U和V元素,后面同理。所以根据上面这种格式:

  • 422采样方式 + YUYV打包存储方式 = YUYV
  • 422采样方式 + UYVY打包存储方式 = UYVY

422采样的主流存储方式就是以上几种了,当然,还有各种衍生版本,这里也不多做介绍了。


然而422采样也可以使用平面存储的方式,如下:

Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
U数组: [U, U, U, U]
V数组: [V, V, V, V]//为了更清晰的展示数据格式,做了改动
新数组:[YYYYYYYY   UUUU    VVVV]
  • 422采样方式 + 平面存储方式 = YUV422P(属于YUV422)

至此主流的存储方式我们已经了解完毕了,各种衍生的类似存储方式大家则需学习好了,接下来进行实战吧!

四、NV21数据转Bitmap

为什么使用NV21数据来做示例呢,因为我们Android有现成的类可以直接处理nv21的数据。

先说下整体流程吧,准备一张800*400像素的图片,如下,然后我将其放到了drawable-xxhdpi目录下,因为这个目录下我的设备读取出来后是1:1的,不用做过多的转换,大家在这里一定要处理好了(宽、高需要为偶数哦)。


然后按照如下步骤:

  • 1、BitmapFactory读取到这张图片;
  • 2、bitmap.getPixels()获取图片所有像素点数据,可以得到rgb数据的数组;
  • 3、根据rgb数组采样分别获取y,u,v数组;
  • 4、将分别得到的y、u、v数组存储为nv21格式的数组;
  • 5、nv21数据转换为Bitmap;
    这里可以分两种方式:使用 YuvImage 或者 ScriptIntrinsicYuvToRGB

按照如上步骤,我们先读取drawable目录下的图片并获取像素点,代码如下:

//1、获取bitmap图片
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.demo800_400);int width = bitmap.getWidth();
int height = bitmap.getHeight();//2、获取像素点数据
int length = width * height;
int[] pixels = new int[length];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);

然后我们需要根据420的采样方式,分别获取到y,u,v的数组:

//3、采样分别获取y、u、v
byte[] arrayY = new byte[length];
byte[] arrayU = new byte[length / 4];
byte[] arrayV = new byte[length / 4];int indexU = 0;
int indexV = 0;
for (int i = 0; i < length; i++) {int pixel = pixels[i];int r = (pixel >> 16) & 0xff;int g = (pixel >> 8) & 0xff;int b = pixel & 0xff;arrayY[i] = (byte) (((66 * r + 129 * g + 25 * b + 128) >> 8) + 16);int row = i / width;         //第n行,从0开始int column = i % width;      //第n列,从0开始//偶数行取U,基数行取Vif (row % 2 == 0) {//偶数行取Uif (column % 2 == 0) {arrayU[indexU] = (byte) (((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128);indexU++;}} else {if (column % 2 == 0) {arrayV[indexV] = (byte) (((112 * r - 94 * g - 18 * b + 128) >> 8) + 128);indexV++;}}
}

接下来是重头戏了,重新存储为NV21格式的数组,千万别存储错了哦:

//4、交叉存储VU数据
int lengthY = arrayY.length;
int lengthU = arrayU.length;
int lengthV = arrayV.length;int newLength = lengthY + lengthU + lengthV;
byte[] arrayNV21 = new byte[newLength];//先将所有的Y数据存储进去
System.arraycopy(arrayY, 0, arrayNV21, 0, lengthY);//然后交替存储VU数据(注意U,V数据的长度应该是相等的,记住顺序是VU VU)
for (int i = 0; i < lengthV; i++) {int index = lengthY + i * 2;arrayNV21[index] = arrayV[i];
}for (int i = 0; i < lengthU; i++) {int index = lengthY + i * 2 + 1;arrayNV21[index] = arrayU[i];
}

NV21格式的数据封装好后我们可以先使用YuvImage+BitmapFactory来得到Bitmap图片,然后直接显示在ImageView中查看效果,如下:

YuvImage image = new YuvImage(arrayNV21, ImageFormat.NV21, width, height, null);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
image.compressToJpeg(new Rect(0, 0, width, height), 80, stream);
Bitmap newBitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
stream.close();ImageView imageView = findViewById(R.id.img);
imageView.setImageBitmap(newBitmap);

OK,不出意外的话,你应该可以看到上面的示例图片了。然而使用上述代码的话,你肉眼能观察的到:图片亮度会降低,而且画质也不清楚。

还有一种转换方法,如下:

RenderScript rs = RenderScript.create(this);
ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs));
Type.Builder yuvType = (new Type.Builder(rs, Element.U8(rs))).setX(newLength);
Allocation in = Allocation.createTyped(rs, yuvType.create(), 1);
Type.Builder rgbaType = (new Type.Builder(rs, Element.RGBA_8888(rs))).setX(width).setY(height);
Allocation out = Allocation.createTyped(rs, rgbaType.create(), 1);
in.copyFrom(arrayNV21);
yuvToRgbIntrinsic.setInput(in);
yuvToRgbIntrinsic.forEach(out);
Bitmap newBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
out.copyTo(newBitmap);ImageView imageView = findViewById(R.id.img);
imageView.setImageBitmap(newBitmap);

使用该方法的话显示出来的图像和源图像就没有肉眼上的区别了,而且效率也更高,推荐使用!!!

如果你显示出来异常图像的话,建议查看YVU的数据是否存储错误了:

  • VU数据存储相反的话,使用该示例图片,你会看到蓝色调调的图片。
  • 没有存储U数据的话,使用该示例图片,你会看到黄色调调的图片。
  • 没有存储V数据的话,使用该示例图片,你会看到绿色调调的图片。
  • U、V数据都没有存储的话,使用该示例图片,你会看到更绿色的图片。

好了,真的没有那么多如果,赶快检查代码去吧。

五、总结

至此我们学习到了YUV在420采样格式下的存储及使用,使用NV21讲解主要是因为Android上采集的数据一般是NV21格式的,而且Android也有现成的工具来处理相应的数据,方便演示。

至于其他采样、存储方式,以及存储格式之间的转换,我们都可以触类旁通了,实在记不住可以临时面向网络编程嘛。

以上代码只是作为最基础、最直观的示例展示,性能实在不敢恭维,所以继续学习的话我们可以参考Google libyuv 的仓库,C++代码,从native层加速。

其实从去年就开始打算学习这一块儿了,然而由于种种原因偷懒,也因为网上看到的很多讲解YUV的文章都千篇一律,都从理论上讲了怎么采样,怎么交替存储,可是实在没有找到一篇能清楚的表示在代码中,存储在数组中是什么样子的。也是我理解能力太差了,所以还没入门就放弃了。这次继续拾起来是因为集成声网最新工程版SDK时遇到了问题,以前SDK每帧给出来的数据都是NV21格式的,所以我们使用项目中封装的工具就可以直接处理了。然而这次给出来的是y、u、v分量的三个数组数据,所以这就要求我们自己组装成NV21格式的数据了。然后硬着头皮认真钻研了下YUV相关的基础知识,总算是迈出了一只脚了吧。

参考

感谢这些作者,我从中借鉴并学习到了很多。

  • https://blog.csdn.net/leixiaohua1020/article/details/50534150
  • https://www.cnblogs.com/azraelly/archive/2013/01/01/2841269.html
  • https://blog.csdn.net/iva_brother/article/details/84036877
  • https://www.cnblogs.com/sddai/p/10302979.html
    该作者的示例图让我真正明白了采样是如何进行的
  • https://www.cnblogs.com/zebra-bin/p/12882117.html
  • https://blog.csdn.net/qq1137830424/article/details/81980673
  • https://blog.csdn.net/bluegodisplay/article/details/53431798
    nv21数据转bitmap更高效的方式
  • https://blog.csdn.net/zhying719/article/details/96421405
    该作者的文章纠正了我对存储模式的错误理解

初识YUV,实战NV21格式数据转换为Bitmap相关推荐

  1. Python将JSON格式数据转换为SQL语句以便导入MySQL数据库

    前文中我们把网络爬虫爬取的数据保存为JSON格式,但为了能够更方便地处理数据.我们希望把这些数据导入到MySQL数据库中.phpMyadmin能够把MySQL数据库中的数据导出为JSON格式文件,但却 ...

  2. Numpy中使用astype函数将字符串格式数据转换为数值数据类型

    Numpy中使用astype函数将字符串格式数据转换为数值数据类型 目录 Numpy中使用astype函数将字符串格式数据转换为数值数据类型 numpy是什么?numpy和list有哪些区别? Num ...

  3. BSQ格式数据转换为RSD缺省的BIP格式数据

    李国春 RSD内部统一以BIP格式排列数据,并且文件格式(非TFS)数据倒放(North Down).早期是为了和设备无关位图(DIB)一致节省一点处理时间.现在设备处理能力增强了这点时间已经无关紧要 ...

  4. Gdiplus byte *数据转换为Bitmap类型图片

    最近在mfc上显示缩略图那样显示采集到的图片,这个用CimageList和CListctrl就可以了,网上有很多这里不细说,但是别忘了初始化Gdiplus: 但是我的相机采集到的就是byte类型的数据 ...

  5. python将base64格式数据转换为图片

    文章首发于:http://80sdianying.xyz/?id=10 直接附上代码,其中的value是base64格式的数据,pic.jpg则是我们保存的图片名. img = base64.b64d ...

  6. 向量数据库入坑指南:初识 Faiss,如何将数据转换为向量(一)

    我们日常使用的各种 APP 中的许多功能,都离不开相似度检索技术.比如一个接一个的新闻和视频推荐.各种常见的对话机器人.保护我们日常账号安全的风控系统.能够用哼唱来找到歌曲的听歌识曲,甚至就连外卖配送 ...

  7. 将JSON格式数据转换为javascript对象 JSON.parse()

    <html> <body> <h2>通过 JSON 字符串来创建对象</h3> <p> First Name: <span id=&q ...

  8. 【Android RTMP】x264 图像数据编码 ( NV21 格式中的 YUV 数据排列 | Y 灰度数据拷贝 | U 色彩值数据拷贝 | V 饱和度数据拷贝 | 图像编码操作 )

    文章目录 安卓直播推流专栏博客总结 一. NV21 图像数据中的 YUV 数据简介 二.向 x264 编码图片 三. 提取 NV21 数据中的灰度数据 Y 四. 提取 NV21 数据中的饱和度数据 U ...

  9. 简介Bitmap、YUV,NV21与Bitmap互转

    1. Bitmap 1.1Bitmap简介 关于Bitmap,它和Drawable差不多就是一种图片,Bitmap相关的使用主要有两种: 给ImageView设置背景 当做画布来使用 分别对应下面两个 ...

最新文章

  1. C++ 中 map 的用法
  2. js 点击改变内容与vue 点击改变内容
  3. 享乐不尽 聚 VR一体机艳冠群雄
  4. 选择开源项目的时候,哪些因素是最需要考量的?
  5. Leetcode 148. 排序链表 解题思路及C++实现
  6. 【机器学习】情侣、基友、渣男和狗-基于时空关联规则的影子账户挖掘
  7. java文件端点续传效果图_Java单依赖性Dockerized HTTP端点
  8. 计算机一级查询记录,技巧查看电脑中使用过的记录痕迹的详细教程
  9. java 预览office_java在线预览office
  10. ubuntu下谷歌开源的TensorFlow Object Detection API的安装教程
  11. PowerDesigner--comment和name互相复制
  12. Android 宽高比控件
  13. 淘口令api权限申请,赚取佣金第一步
  14. H5架设新手小白搭建教程(是用于新手)
  15. Bootstrap Validate 下拉框验证
  16. 计算机高级培训教师感言,教师感言,句句经典
  17. bmc linux 默认密码_系统下重置BMC密码方法
  18. 迎接虎虎生威的2010暨2009年51CTO年终总结大会
  19. GBase 8c 全局死锁解除
  20. 联想计算机如何设置用户名和密码忘了,联想电脑怎么设置密码

热门文章

  1. 将dwg文件转为shp文件
  2. 作为校招loser,我如何在一年半后的社招中咸鱼翻身
  3. 微信图片防盗链笔记(转:破解微信图片防盗链)
  4. keras之数据预处理
  5. Android使用CameraX打开相机拍照简单使用
  6. 定时任务与网页去重、代理的使用
  7. MySQL的TIMESTAMP数据类型
  8. Java岗大厂面试百日冲刺【Day50】— 秒杀系统2 (日积月累,每日三题)
  9. Git提交代码到新仓库(--mirror)
  10. c语言编程余弦,C语言实例编程绘制余弦曲线