0%

[原创] Android 使用 Camera2 获取实时摄像头数据并实时编码成 H.264

引言

强烈建议大家先阅读下我之前写的文章“YUV 基础知识”一定会帮助到你。(我可是花了很长很长时间来学习并撰写文章的,都是干货!)

结论

本文较长,考虑到大忙人不少,可能没时间看完全文,因此先说下结论:

结论1:使用 Camera2 时,通过 Image 获取的数据,可以将其组织成 I420NV21 这两种格式。

结论2:MediaCodec 支持传入的 YUV420 格式是 I420NV12(大多数情况)这两种格式(至少我目前遇到的是这两种格式。注意:是 NV12 而非 NV21)。

PS:对摄像头数据进行编码时,目前我是根据设备支持的编码器的类型来判断需要向 MediaCodec 传入 I420 数组还是 NV12 数组(如果您有更好的办法,还请大神不吝赐教,可以留言或给我发 Email

若设备包含以下编码器,则需要向 MediaCodec 传入 I420
OMX.IMG.TOPAZ.VIDEO.Encoder
OMX.Exynos.AVC.Encoder
OMX.MTK.VIDEO.ENCODER.AVC
OMX.oppo.h264.encoder

结论3:前后置摄像头的旋转角度应该根据 CameraCharacteristics.SENSOR_ORIENTATION 进行设置。并没有一个固定值。如果您再看到有人说:“后置摄像头图像要顺时针旋转90度。前置摄像头图像要顺时针旋转270度”的话,请拿出您82年的锤子直接砍他!

常见 YUV420 格式说明:

I420[^i420]:属于 YUV420P。也叫做 IYUV,YU12,存储格式:YYYYYYYY UU VV

YV12[^yv12]:属于 YUV420P。存储格式:YYYYYYYY VV UU

NV12[^nv12]:属于 YUV420SP。存储格式:YYYYYYYY UVUV

NV21[^nv21]:属于 YUV420SP。存储格式:YYYYYYYY VUVU

需要源码的朋友,可以从我的 Github上下载。

好了,送走了大忙人之后,还请有心人一直来探讨学习。请您继续看后文。

前言

Android 从 5.0(API 21, 2014年发布)开始,Google 已经将原来的 Camera 类废弃了,改用全新设计的功能更加强大的 Camera2。考虑到 Android 5.0 以下版本市场占用率虽然仍超过 10%,还是挺高的,但是相信随着时间的推移,5.0 以下版本终会退出历史的舞台。因此这里只讲解 Camera2 的使用方法。

下图是截止 2020年4月份 Android 版本分布情况。(吐个槽,2014年之前的系统(Android 5-)居然还有这么高占用率,也是醉了。再看看 iOS,哎,还是不比了,怕心脏不行。)

Android Version Distrution 2020 April

以下图片引自statcounter.com。统计期间为 2019年9月至2020年9月,全球 Android 设备的版本分布情况:

StatCounter-android_version-ww-monthly-201909-202009

支持 Camera2 的设备,均支持全新的 YUV420Flexible 格式,配套 YUV_420_888。老版本的 Camera 支持的是 NV21 和 YV12。推出全新格式的原因是统一 Android 内部混乱的中间图片数据(这里中间图片数据指如各式 YUV 格式数据,在处理过程中产生和销毁)管理。主要体现在:

  1. 新的 Camera2 输出的帧信息采用的是 Image,默认格式为 YUV_420_888
  2. 硬件编解码的 MediaCodec 类加入了对 ImageImage 的封装类 ImageReader 的全面支持,并推荐采用 YUV420Flexible 进行编解码。

YUV420Flexible 并是一种具体的格式,而是一类 YUV 格式,包括 I420 还有旧版 Camera 支持的 NV21 和 YV12。YUV420Flexible 格式的最大优点就是速度快。在实时预览时,该格式至少可以达到 30 FPS。

Android 5 之前的版本,Camera Preview 支持的格式是包括 NV21,YV12,NV16,默认图像格式是 NV21,官方强烈建议使用 NV21 或 YV12。网上很多资料的讲解,包括旋转算法都是基于 Camera1 的,编码用到的颜色格式一般也用的是 COLOR_FormatYUV420SemiPlanarCOLOR_FormatYUV420Planar

而对于 Andriod 5 及之后版本,Google 推出了全新设计的 Camera2,并且提供了全新的格式 YUV_420_888,该格式也是 Android 建议的格式,配套颜色格式为 COLOR_FormatYUV420Flexible。老版本的颜色参数基本也都被废弃了。使用 MediaCodec 进行 H.264 编码时,要求传入数据的格式是 I420 和 NV12 这两种格式,至少我目前遇到的是这两种格式。此处就有坑了,大家上网查关于摄像头数据旋转算法时,移植到自己的代码里会发现有的好用,有的不好用。其实那些看起来不好用的代码,很可能是没有问题的,网上很多算法可能是基于 Camera1 返回的数据,问题出现在网上的文章并没有准确的告诉你该旋转算法传入的参数是在什么具体数据格式下(是 I420 还是传 NV12)才能使用。因此导致了由于数据格式不匹配,出现无法旋转或旋转后出现画屏,颜色不对等奇怪的现象。

这里在强调一下,在我的实际使用中,测试了多款不同的手机,摄像头产生的 YUV420 数据出现过两种情况,而且不同手机的 MediaCodec 支持的 H.264 视频硬编码的颜色格式也大不相同。详见后文“附录”部分。这对于如何处理摄像头产生的数据有着非常大的影响,详见后文说明。

以下内容是对一些相关官方内容的引用,感兴趣的可能看一下:

关于 Camera#setPreviewFormat(int pixel_format) 的官方注释中是这样描述的:

Sets the image format for preview pictures.
If this is never called, the default format will be NV21, which uses the NV21 encoding format.
Use getSupportedPreviewFormats() to get a list of the available preview formats.
It is strongly recommended that either NV21 or YV12 is used, since they are supported by all camera devices.

关于 YUV

关于 YUV 的基础知识大家一定要了解,要不然不会解析 Camera 返回的数据。强烈建议大家阅读下我之前写的文章“YUV 基础知识”一定会帮助到你。(我可是花了很长很长时间来学习并撰写文章的,都是干货!)

Image 类

如果你看了“YUV 基础知识”,那么你应该知道 YUV420 包含了很多具体格式。当然这么多格式对于开发者而言要一一处理的话是十分可怕的,因此 Image 类就这样横空出世了。Image 可以通过 ImageReader.acquireLatestImage() 获取。

Width 和 Height

对于 YUV 来说,图片的宽和高是必不可少的。因为之前提到了 YUV 本身只存储颜色信息,而且数据的存储顺序是十分重要的。因此想要还原出图片,必须知道图片的长和宽。Image 类中保存了图片的宽和高,可以通过 Image#getWidth()Image#getHeight() 获得。

图片格式

每个 Image 都有自己的格式,这个格式由 ImageFormat 确定。对于 YUV420 来说,ImageFormat 在API 21 中新加入了 YUV_420_888 类型,表示 YUV420 格式的集合。888 表示 Y、U、V 分量中每个颜色占8 bit。那么需要怎样处理摄像头返回的数据呢?请您继续往下看。

YUV420 分量

首先我们先看下官方文档中关于 ImageFormat.YUV_420_888 的说明:

Multi-plane Android YUV 420 format
This format is a generic YCbCr format, capable of describing any 4:2:0 chroma-subsampled planar or semiplanar buffer (but not fully interleaved), with 8 bits per color sample.
Images in this format are always represented by three separate buffers of data, one for each color plane. Additional information always accompanies the buffers, describing the row stride and the pixel stride for each plane.
The order of planes in the array returned by Image#getPlanes() is guaranteed such that plane #0 is always Y, plane #1 is always U (Cb), and plane #2 is always V (Cr).
The Y-plane is guaranteed not to be interleaved with the U/V planes (in particular, pixel stride is always 1 in yPlane.getPixelStride()).
The U/V planes are guaranteed to have the same row stride and pixel stride (in particular, uPlane.getRowStride() == vPlane.getRowStride() and uPlane.getPixelStride() == vPlane.getPixelStride(); ).
For example, the Image object can provide data in this format from a CameraDevice through a ImageReader object.

再来看一下官方文档对 COLOR_FormatYUV420Flexible 的描述:

Added in API level 21
int COLOR_FormatYUV420Flexible
Flexible 12 bits per pixel, subsampled YUV color format with 8-bit chroma and luma components.
Chroma planes are subsampled by 2 both horizontally and vertically. Use this format with Image. This format corresponds to YUV_420_888, and can represent the COLOR_FormatYUV411Planar, COLOR_FormatYUV411PackedPlanar, COLOR_FormatYUV420Planar, COLOR_FormatYUV420PackedPlanar, COLOR_FormatYUV420SemiPlanar and COLOR_FormatYUV420PackedSemiPlanar formats.

ImageFormat.YUV_420_888 描述中,最重要的信息是:Y、U 和 V 三个分量的数据分别保存在三个 Plane 类中,可以通过 Image#getPlanes() 得到。查看源码可知,Plane 实际上是对 ByteBuffer 的封装。Image 保证了plane #0 一定是 Y,plane #1 一定是 U(Cb),plane #2 一定是 V(Cr)。

对于 plane #0 来说,Y 分量数据一定是连续存储的,中间不会有 U 或 V 数据穿插进来,也就是说我们一定能够一次性的获取到所有 Y 分量的值。并且 Y 分量的 pixelStride 一定等于 1(因为 Y 分量的数据是连续存储的)。

对于 U/V plane 来说,Image 能够保证 U 和 V 具体相同的 rowStridepixelStride。从代码角度来说就是 uPlane.getRowStride() == vPlane.getRowStride()uPlane.getPixelStride() == vPlane.getPixelStride()。这里需要注意的是,对于 U/V plane 来说,Android 并没有保证 U plane 中的 U 或 V plane 中的 V 也是连续存储的。实际上,Android 返回的 U plane 和 V plane 是以 U/V 交叉储存的,也就是说,U plane 中不只是 U 分量数据,而是 U/V 分量交叉储存的。V plane 也是这样存储的。(虽然官方文档没有明确表明是这样储存的,但是从实际效果来看,Android 确实是这么存储的)后文还会讲到。

这里需要说明下,pixelStride 代表相邻像素样本之间的距离,单位是字节。其值可能是 1, 也可能是 21 表示数据是连续存储的,2 表示相邻像素样本之间的距离 2,也就是说索引值为 0,2,4,6…… 这样的索引值对应的数据才是有效的。对于 Y 分量来说,pixelStride 一定等于 1,对于 U/V 分量来说,该值可能是 1也可能是2

COLOR_FormatYUV420Flexible 描述中,重要的信息是:在该颜色格式下,图像中的每个像素占用 12 bit(即 1.5 个字节),该颜色格式与 YUV_420_888 配套使用。需要格外注意的是,该颜色格式并不是一种具体的格式,可能代表下列颜色格式中的一种或几种:

  • COLOR_FormatYUV411Planar
  • COLOR_FormatYUV411PackedPlanar
  • COLOR_FormatYUV420Planar
  • COLOR_FormatYUV420PackedPlanar
  • COLOR_FormatYUV420SemiPlanar
  • COLOR_FormatYUV420PackedSemiPlanar

Camera2 Image 返回的 YUV420 到底长什么样

当我们在 ImageReader.newInstance 设置成 YUV_420_888 时,通过 ImageReader#setOnImageAvailableListener 回调从 Image 中取得的 YUV420 数据到底长什么。我们需要先好好说明下。

经测试发现,通过 Camera2 获取到的 YUV420 数据,Y 分量和之前讲解的一样,是连续存储的。但是 U/V 分量却是交叉存储的(详见后文)。让我们来举例说明下,假如摄像头返回一个 8×4 的 YUV420Flexible 图像:

假设 Camera2 Image 返回的是 I420 格式

图像示意:

I420-8x4

Image 中的 U/V 分量的 pixelStride 均为 1 的话,三个 Plane 的数据如下:

I420-YUV-Array

与之对应的一维数据应该是这样的:

I420-8x4-Array

Image 中的 U/V 分量的 pixelStride 均为 2 的话,三个 Plane 的数据如下:

I420-YUV-Array-PixelStride2

其中 “-” 代表无效数据。

与之对应的一维数据应该是这样的:

I420-8x4-pixelStride1-Array

其中,“空白”代表无效数据。

注意:上面讲的都是理论上的数据表现。

假设 Camera2 Image 返回的是 NV12 格式

说明:这里仅仅是假设,用 NV12 作为示例。

图像示意:

NV12-8x4

Image 中的 U/V 分量的 pixelStride 均为 1 的话,三个 Plane 的数据如下:

I420-YUV-Array

与之对应的一维数据应该是这样的:

NV12-8x4-Array

Image 中的 U/V 分量的 pixelStride 均为 2 的话,三个 Plane 的数据如下:

I420-YUV-Array-PixelStride2

其中 “-” 代表无效数据。

注意:上面讲的都是理论上的数据表现。

实际 Camera2 Image 返回的数据

上述讲的都是“理论上”的数据返回结果,但是实际上 Camera2 返回的结果与“理论上”却不尽相同。让我们来看看实际 Camera2 返回的数据是什么样的:

Image 中的 U/V 分量的 pixelStride 均为 2 的话,实际上三个 Plane 的数据如下:

actually-yuv-array-pixelStride2

请格外注意灰色背景的数据。灰色背景应该是“无效”数据才对,但是从实际测试结果来看,U 分量中的灰色背景数据刚好就是纯 V 分量数据。V 分量中的灰色背景数据刚好就是纯 U 分量数据。换句话说,如果把 U 分量的数据向左移一位的话,刚才就是 V 分量数据:

actually-yuv-array-pixelStride2-shift-left-1bit

若你仔细看的话,还会发现实际上,U/V 分量的长度貌似也有问题,上例中,U/V 分量的实际长度都是 15 ,并不是 16

为什么这么神奇?让我们继教往下看。

其实三个 Plane 中 Y 分量没有什么好说的,可以直接从 Plane#0 中获取。关于 U 和 V 分量要说的很多,我们考虑其中的三类格式:Planar,SemiPlanar 和 PackedSemiPlanar。

Planar

实际使用中,我手里的荣耀 9 Lite,华为 P9,VIVO Y67,红米 6A等几款手机属于这种情况。当采用 MediaCodec 对视频数据进行 H.264 硬编码时,需要将摄像头返回的 YUV420 数据组织成 I420 格式的数组再传递给 MediaCodec 这样才能编码出正确的视频。否则会出现颜色显示下正常的情况。

这里以“荣耀 9 Lite”为例进行,对 Image 的解析进行下详细说明(图像尺寸 1920×1080):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
image.format: 35
image.planes: 3 planes
image.width 1920
image.height 1080
plane #0:
pixelStride 1
rowStride 1920
buffer size 2073600
plane #1:
pixelStride 2
rowStride 1920
buffer size 1036799
plane #2:
pixelStride 2
rowStride 1920
buffer size 1036799

具体分析过程:
image.format: 35 代表 ImageFormat.YUV_420_888。一共包含了 3 个 planes。图像大小为 1920×1280。
Y 分量rowStride 等于 1920,说明一行有 1920 个值,那 Y 有多少行呢?我们通过 Y 分量的 Buffer Size 计算下:2073600 / 1920 = 1080 行(该值用于对比 U/V 分量的行数),说明 Y 分量包含全面像素点。

U 分量 pixelStride2rowStride 等于 1920。根据 Buffer Size 计算得出共有 1036799÷1920≈540 行。按理说如果 U plane 若只包含 U 分量数据的话,其长度应该是 Y 分量长度的 1/4 才对。但是通过 Buffer Size 可知,U 分量长度是 Y 分量的 1/2。这是怎么回事呢?让我们继续分析。

这里一共有两个问题:

  1. U 分量的长度并不是 Y 分量长度的 4/1,而是 1/2。
  2. U 分量的长度并不正好是 Y 分量长度的 1/2。2073600÷2 应该是1036800,但是实际上我们得到 U 分量的长度却为 1036799,比预计值少了 1。

先说第二个问题。根据实际测试,U 分量实际长度的计算方法应该是:

U 分量长度 = U 分量的 rowStride×图像高÷2−1。

那么按照本例的情况,U 分量长度 = 1920×1080÷2−1=1036799。此外在这篇文章里,作者也提到了 U/V 分量的计算方法。

上面的公式中,为什么要使用 rowStride,而不是直接使用图像的宽呢?这里就需要注意了,通过查这份资料和这份资料,发现有人遇到过 rowStride 和真实图像 width 不一样的情况(虽然我还没遇到过),因此使用 rowStride 才是最准确的 。据我分析,若 YUV420 数据是能过摄像头获取的话,是不应该出现这个问题的。因为摄像头返回的图像宽和高一般都是 4 的倍数,因此不会出来这个问题。只有摄像头返回的图像宽和高不是 4 的倍数时,那么在最终生成的 YUV420 数据时,需要把它们补成 4 的倍数,因此就出现了两者不一致的情况。

可能又有人要问了,为什么必须是 4 的倍数?虽然到现在我也没太弄明白这个问题,不过当我开始写“旋转”算法时,算是明白了一点点,不过由于是个人的愚见,纯属于个人猜测,为了不误导大家,我就不瞎写了。大家可以自己尝试写下旋转算法可能也会明白一二。

回过头来,我们再说说第一个问题,也就是 U 分量长度为什么不是 Y 分量的 1/4,而是 1/2。这里我们先注意下 U 分量的 pixelStride2,这代表相邻像素样本之间的距离 2,也就是说只有行内索引值为 0,2,4,6... 这样的偶数索引值才是真正有效的 U 分量。

我们还是用上面的例子,在实际的 Image 中,从 U 和 V 各自数据中抽取出相同索引值的前 20 个字节,来看看它们的关系:

U 行:82 7F 82 7F 82 7F 82 7F 82 80 83 80 83 80 83 80 83 80 83 80

V 行:7F 82 7F 82 7F 82 7F 82 7F 82 80 83 80 83 80 83 80 83 80 83

细心的你可能已经察觉到了,上述例子中如果把 U 分量向左移动了一位刚好就变成了 V 分量。U/V 数据像是被交叉存储一样,因此你可能会觉得仅通过 U 分量就可以获取到 V 分量了。虽然在实际使用中,这样做可能并不会引起什么大的问题(可能仅有最后一个像素会显示出错。当然出错的这个像素我们也可以自己将它补回来),不过由于 Android 文档中并没有对此进行说明,而且也没有保证这么做的正确性,因此还是老老实实的通过 U/V 分量各自所在的 Plane,参考 pixelStride 值,分别获取真正的 U/V 分量才是正解。

通过上述这个例子我们得知,U/V 分量仅有偶数索引值才是真实有效值,而奇数索引值是无效值。虽然奇数索引值是无效的,但是依然占用了存储空间,因此 U 和 V 分量长度是 Y 分量长度的 1/2,而不是 1/4。

我推测 Android 将 U/V 分量交叉返回的原因可能是因为效率。因为将 U/V 交叉返回的话(刚好就是 YUV420SP),我们可以直接按下面的方法获取到具体的 YUV420SP 数据:

NV12 = plane[0] + plane[1] + plane[2]的第一个字节

NV21 = plane[0] + plane[2] + plane[1]的第一个字节

或者直接粗略的计算:

NV12 = plane[0] + plane[1]

NV21 = plane[0] + plane[2]

通过实际测试得知:使用 MediaCodec 进行 H.264 编码时,对于大多数 Android 设备是要求传入 NV12 格式的数据,少数要求传入 I420 格式的数据。因此 U/V 若是交叉储存的话,我们可以很方便的获取到 NV12 数据。(实际情况虽然是 U/V 交叉存储的,但毕竟未在官方文档的找到对应的说明。所以还是建议大家按照正确的方法,从 U/V 各自的 Plane 中,参考 pixelStride 值获取对应的分量数据才是最安全的)

V 分量分析方法与 U 分量的相同。

SemiPlanar

该格式下 U 和 V 分量是交叉存储的,例如:YYYYYYYYUVUV。当采用 MediaCodec 对视频数据进行 H.264 硬编码时,需要将摄像头返回的 YUV420 数据组织成 NV12 格式的数组再传递给 MediaCodec 这样才能编码出正确的视频。否则会出现颜色显示下正常的情况。大多数 Android 设备都要求传入该格式的数据。

这里以“三星 S7 edge(SM-9350)”为例进行,对 Image 的解析进行下详细说明(图像尺寸 1920×1080):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
image.format: 35
image.planes: 3 planes
plane #0:
pixelStride 1
rowStride 1920
buffer size 2073600
plane #1:
pixelStride 2
rowStride 1920
buffer size 1036799
plane #2:
pixelStride 2
rowStride 1920
buffer size 1036799

其中 image.format: 35 代表 ImageFormat.YUV_420_888。一共包含了 3 个 planes。图像分辨率为 1920×1080 = 2073600 像素。由此可见,Y 分量包含全部的像素点,U 和 V 分量长度是 Y 分量长度的 1/2。这些基本分析过程和之前讲解的 Planar 相同,不再赘述。

需要再一次提醒的是,从 Plane 中 获取的 U/V 分量依然是交叉储存的。

这里需要注意的是,三星 S7 edge(SM-9350)和其它大多数 Android 手机,在使用 MediaCodec 进行 H.264 编码时,需要将 YUV420 数据组织成 NV12 格式的数组再传递给 MediaCodec,否则会出现编码后视频颜色显示不正常的情况。

PackedSemiPlanar

我并没有遇到这种情况。因此无法给出解析实例。大家可以参考该文章,作者给出的解释是:

这个简单点说,不知为何,在我的设备上PackedSemiPlanar和SemiPlanar的表现是一致的,也就是说,可能Android已经帮我们解决了Packed的问题,只有Semi留给我们自己解决。

关于 CropRect

Image 有一个 cropRect 方法,官方文档解释如下:

Get the crop rectangle associated with this frame.
The crop rectangle specifies the region of valid pixels in the image, using coordinates in the largest-resolution plane.

大意是说:通过该方法,可以得到从图片中裁减出的一个矩形区域。只有该矩形区域所包含的像素才是有效的。

因此取分量时需要特别注意该问题,需要获取指定区域内的像素所对应的分量。不过由于目前我使用的场景比较简单,还没有处理过这种情况。待以后遇到了再补充吧。

小结

摄像头返回的数据中 U/V 分量是交叉储存的。我们需要根据设备支持的编码器的类型来判断需要向 MediaCodec 传递 I420 数组还是 NV12 数组(如果您有更好的办法,还请大神不吝赐教,可以留言或给我发 Email)。

大家在学习 MediaCodec 的时候,一定都看过 bigflake 的 Android MediaCodec stuff。这可是关于硬解码不可多得的优秀资源,里面的示例让我们学习了很多东西。

不过 Android 设备太多,各硬件厂商的硬编解码处理方式不尽相同。目前通过 MediaCodec 编解码时需要用的 YUV420 数据格式,Android 没有做统一要求,通过上面的示例及后文中附录中的测试结果就可以验证这一点。因此很难写出一种统一并高效的格式转换方法,这点太坑了!不过自加州理工学院的Alan Heirich博士,统计了截止2015年6月,来自全球约 100,000 部 Android 设备 MediaCodec 所支持的解码格式情况(详见原文)。从统计的表格我们可以看到,绝大多数设置支持 OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar64x32Tile2m8kaCOLOR_QCOM_FormatYUV420SemiPlanar32m。换句话说,就是支持 COLOR_FormatYUV420Planar 的设备还是很少的。

并且所有设备在解码 H.264 时,都要求 16 位对齐。

关于 Camera2 与 MediaCodec

刚才讲解了一大堆,我们知道 Camera2 中建议使用的是 YUV420Flexible 格式,配套 YUV_420_888。那以前的 NV21 还支持吗?答案是:不支持。请看 ImageReader 源码:

ImageReader Source

看到了吧,NV21 是不被支持的。

实际上,通过 Camera2 获取到的 YUV420 数据,需要组织成 I420 或 NV12 格式的数据,之后再传递给 MediaCodec。否则会出现编码后视频颜色显示不正确的问题。

随之又产生了一个新问题:如何从得到的 Image 对象提取出 byte 数组?只有提取出所需要的 byte 数组后,才能进行旋转等其它处理(详见之前讲解的 Camera2返回的YUV420到底长什么样)。相信大家科学上网的话,会搜到很多答案,不过你可能又发现了,很多答案并不生效。因此有些人直接把 Image 对象中的 Plane 拼接成了 byte 数组,而忽略了具体的 YUV420 格式;有的人则会将得到的原始数据转换成 I420 格式的 byte 数组。网上的文章并没有明确说明他们提供的图像旋转算法需要哪种格式的 byte 数组,因此造成了使用时出现各种诡异的问题。

其实,无论是进行图像旋转,还是将数据编码成 H.264 等,都需要从 Image 中提取出期望格式的 byte 数组。例如,期望转换成 I420 或 NV12 格式的数组。

对于 YUV_420_888 而言,通过 Image 对象,我们可以获取到一些重要信息:图像的 width,height,存放 YUV 数据的 Plane 数组。根据 Plane 又可以取得 pixelStriderowStride。通过这些数据,我们就可以准确的提取出 Y/U/V 分量。之后就可以将它们转换成所需的 byte 数组了。(关于 Plane ,已经在前面的“YUV420分量”中讲过了,这里就不太赘述了。)

注意:准确的提取 Y/U/V 分量,是进行数据转换的必要前提。

Camera2 使用方法

之前说了那么多,现在终于进入正题了。是时候讲讲 Camera2 的使用方法了。需要提醒大家,Camera2 的使用方法和以前的 Camera1 是完全不同的。具体示例源代码请从我的 Github 上下载。

Camera2 Capture Image

关于图像旋转

之前提到了,网上几乎所有关于摄像头的旋转算法都没有提到传入的数组参数到底是 YUV420 的哪种格式。因此导致使用时出现了各自奇怪的问题。

本文主要讲解应该如何做旋转。大家如果想了解详细旋转算法实现,可以从我的 Github上下载程序源码。直接上图说明:

I420-Rotation-All

NV12-Rotation-All

上面两张图详细展示了如何对图像进行旋转。大家可以对照源码了解具体的程序实现。

写文档时,忘画“水平翻转”的示意图了(源码里有具体实现),不过相信大家完全可以根据上面的示例,自行脑补。

图像转换性能分析

这里先简单记录下我在测试图像旋转及格式转换时所花费的时间。

图像分辨率:720x1280 Samsung S7 Edge(G9350) HuaWei Honor V20(PCT_AL10)
ImageReader 获取 Byte 数组 3ms+ 1.5ms-
一次性完成旋转+格式转换 16ms+ 6.1ms+
总计 ≈20ms ≈8ms
图像分辨率:1080x1920 Samsung S7 Edge(G9350) HuaWei Honor V20(PCT_AL10)
ImageReader 获取 Byte 数组 9.4ms+ 1.7ms-
一次性完成旋转+格式转换 20.4ms+ 8.2ms+
总计 ≈30ms ≈10ms

附录

Camera2 获取到的数据信息:

安卓版本 MediaCodec 要求 NV12 MediaCodec 要求 I420 设置支持的 AVC 编码器 Profile/Level
华为 P40 Pro
(ELS-TN00)
10 :heavy_check_mark: OMX.hisi.video.encoder.avc
c2.android.avc.encoder
OMX.google.h264.encoder
1/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768
2/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768
8/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768
华为 P40
(ANA-AN00)
10 :heavy_check_mark: OMX.hisi.video.encoder.avc
c2.android.avc.encoder
OMX.google.h264.encoder
1/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768
2/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768
8/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768
华为 Mate 30 Pro
(LIO-AL00)
10 :heavy_check_mark: OMX.hisi.video.encoder.avc
c2.android.avc.encoder
OMX.google.H.264.encoder
1/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768
2/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768
8/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768
华为荣耀 V20
(PCT-AL10)
10 :heavy_check_mark: OMX.hisi.video.encoder.avc
c2.android.avc.encoder
OMX.google.h264.encoder
1/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768
2/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768
8/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768
荣耀 9 Lite
(LLD-AL00)
9 :heavy_check_mark: OMX.IMG.TOPAZ.VIDEO.Encoder
OMX.google.H.264.encoder
1/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536
2/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536
8/1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536
64/1024,2048,4096,8192,16384,32768,65536
华为 P9
(VA-DL00)
8.0 :heavy_check_mark: OMX.IMG.TOPAZ.VIDEO.Encoder
OMX.google.H.264.encoder
Google Pixel 3XL(Pixel 3XL) 12 c2.qti.avc.encoder
OMX.qcom.video.encoder.avc
c2.android.avc.encoder
OMX.google.h264.encoder
1/65536
2/65536
8/65536
65536/65536
Nexus 6P 8.1 :heavy_check_mark: OMX.qcom.video.encoder.avc
OMX.google.H.264.encoder
1/65536
2/65536
8/65536
65536/65536
2130706433/65536
三星 Galaxy S20 5G
(SM-G9810)
10 :heavy_check_mark: OMX.qcom.video.encoder.avc
c2.android.avc.encoder
OMX.google.h264.encoder
1/131072
2/131072
8/131072
65536/131072
524288/131072
三星 Galaxy A51 5G
(SM-A5160)
10 :heavy_check_mark: OMX.Exynos.AVC.Encoder
c2.android.avc.encoder
OMX.google.h264.encoder
1/65536
2/65536
8/65536
65536/65536
524288/65536
三星 Galaxy Note 8
(SM-N9500)
9 :heavy_check_mark: OMX.qcom.video.encoder.avc
OMX.google.H.264.encoder
1/65536
2/65536
8/65536
65536/65536
524288/65536
三星 Galaxy S7 edge
(SM-9350)
8.0 :heavy_check_mark: OMX.qcom.video.encoder.avc
OMX.google.h264.encoder
1/65536
2/65536
8/65536
2130706433/65536
2130706434/65536
OPPO A72 5G
(PDYM20)
10 :heavy_check_mark: OMX.MTK.VIDEO.ENCODER.AVC
OMX.oppo.h264.encoder
c2.android.avc.encoder
OMX.google.h264.encoder
1/512,1024,2048,4096,8192,16384,32768
2/512,1024,2048,4096,8192,16384,32768
8/512,1024,2048,4096,8192,16384,32768
OPPO A32
(PDVM00)
10 OMX.qcom.video.encoder.avc
c2.android.avc.encoder
OMX.google.h264.encoder
1/16384
2/16384
8/16384
65536/16384
524288/16384
OPPO Reno4 5G
(PDPM00)
10 :heavy_check_mark: OMX.qcom.video.encoder.avc
c2.android.avc.encoder
OMX.google.h264.encoder
1/131072
2/131072
8/131072
65536/131072
524288/131072
OPPO R15 梦镜版
(PAAM00)
8.1.0 OMX.qcom.video.encoder.avc
OMX.google.h264.encoder
OMX.oppo.h264.encoder
1/32768
2/32768
8/32768
65536/32768
524288/32768
2130706433/32768
2130706434/32768
vivo X50
(V2001A)
10 :heavy_check_mark: OMX.qcom.video.encoder.avc
c2.android.avc.encoder
OMX.google.h264.encoder
1/131072
2/131072
8/131072
65536/131072
524288/131072
VIVO G1
(V1962BA)
10 :heavy_check_mark: OMX.Exynos.AVC.Encoder
c2.android.avc.encoder
OMX.google.h264.encoder
1/65536
2/65536
8/65536
65536/65536
524288/65536
VIVO U3x
(V1928A)
9 :heavy_check_mark: OMX.qcom.video.encoder.avc
OMX.google.h264.encoder
1/65536
2/65536
8/65536
65536/65536
524288/65536
VIVO Y67
(PD1612)
6.0 :heavy_check_mark: OMX.MTK.VIDEO.ENCODER.AVC
OMX.google.H.264.encoder
红米 6A 9 :heavy_check_mark: OMX.MTK.VIDEO.ENCODER.AVC
OMX.google.H.264.encoder
1/1,2,4,6,16,32,64,128,256,512,1024,2048,4096
8/1,2,4,6,16,32,64,128,256,512,1024,2048,4096
红米 Note8 Pro 9 :heavy_check_mark: OMX.MTK.VIDEO.ENCODER.AVC
OMX.google.h264.encoder
1/1,2,4,8,16,32,64,128,256,512,1024,2048,4096
8/1,2,4,8,16,32,64,128,256,512,1024,2048,4096
红米 8 10 :heavy_check_mark: OMX.qcom.video.encoder.avc
OMX.google.H.264.encoder
1/2048
2/2048
8/2048
65536/2048
524288/2048
红米 10 :heavy_check_mark: OMX.qcom.video.encoder.avc
c2.android.avc.encoder
OMX.google.h264.encoder
1/65536
2/65536
8/65536
65536/65536
524288/65536
一加 8T
(KB2000)
11 :heavy_check_mark: OMX.qcom.video.encoder.avc
c2.android.avc.encoder
OMX.google.h264.encoder
1/131072
2/131072
8/131072
65536/131072
524288/131072
一加 7 Pro
(GM1910)
10 :heavy_check_mark: OMX.qcom.video.encoder.avc
c2.android.avc.encoder
OMX.google.h264.encoder
c2.qti.avc.encoder
1/65536
2/65536
8/65536
65536/65536
524288/65536
AVC 编码器支持的颜色格式
华为 P40 Pro
(ELS-TN00)
15,16,19,21,22,25,26,27,28,2130706433,2130706447,2130708361,2135033992
华为 P40
(ANA-AN00)
15,16,19,21,22,25,26,27,28,2130706433,2130706447,2130708361,2135033992
华为 Mate 30 Pro
(LIO-AL00)
15,16,19,21,22,25,26,27,28,2130706433,2130708361,2130706447,2135033992
华为荣耀 V20
(PCT-AL10)
15,16,19,21,22,25,26,27,28,2130708361,2130706433,2130706447,2135033992
荣耀 9 Lite
(LLD-AL00)
6,19,20,21,22,23,24,25,26,27,28,39,2130706433,2130706434,2130706435,
2130706437,2130706438,2130706439,2130706440,2130706441,2130706442,
2130706443,2130706444,2130706445,2130706446,2130706447,2130706448,
2130706449,2130706450,2130708361,2135033992
华为 P9
(VA-DL00)
6,19,20,21,22,23,24,25,26,27,28,39,2130706433,2130706434,2130706435,
2130706437,2130706438,2130706439,2130706440,2130706441,2130706442,
2130706443,2130706444,2130706449,2130706450,2130706445,2130706446,
2130706447,2130706448,2130708361,2135033992
Google Pixel 3XL(Pixel 3XL) 19,20,21,39,2130708361,2135033992
Nexus 6P 21,2135033992,2141391876,2130708361
三星 Galaxy S20 5G
(SM-G9810)
21,2130708361,2135033992,2141391872,2141391876
三星 Galaxy Note 8
(SM-N9500)
21,2130708361,2135033992,2141391872,2141391876,2141391878,2141391879,
2141391880
三星 Galaxy S7 edge
(SM-9350)
21,2130708361,2135033992,2141391876,2141391878,2141391879,2141391880
OPPO A72 5G
(PDYM20)
6,11,15,16,19,21,2130706944,2130707200,2130708361,2135033992
OPPO A32
(PDVM00)
21,2130708361,2135033992,2141391872,2141391876
OPPO Reno4 5G
(PDPM00)
21,2130708361,2135033992,2141391872,2141391876,2141391878,
2141391879,2141391880,2141391881,2141391882
OPPO R15 梦镜版
(PAAM00)
21,2130708361,2135033992,2141391876
vivo X50
(V2001A)
21,2130708361,2135033992,2141391872,2141391876,2141391878,
2141391879,2141391880,2141391881,2141391882
VIVO G1
(V1962BA)
15,16,19,21,2130706449,2130706450,2130708361,2130747392,2135033992
VIVO U3x
(V1928A)
21,2130708361,2135033992,2141391872,2141391876,2141391878,2141391879,
2141391880,2141391881,2141391882,2141391883
VIVO Y67(PD1612) 6,11,15,16,19,21,2130706944,2130707200,2130708361,2135033992
红米 6A 6,11,15,16,19,21,2130706944,2130707200,2130708361,2135033992
红米 Note8 Pro 6,11,15,16,19,21,2130706944,2130707200,2130708361,2135033992
红米 8 21,2130708361,2135033992,2141391876
红米 K30 21,2130708361,2135033992,2141391872,2141391876,2141391878,2141391879,
2141391880,2141391881,2141391882
一加 8T
(KB2000)
21,2130708361,2135033992,2141391872,2141391876,2141391878,
2141391879,2141391880,2141391881,2141391882
一加 7 Pro
(GM1910)
21,2130708361,2135033992,2141391872,2141391876,2141391878,
2141391879,2141391880,2141391881,2141391882
颜色格式值 含义
6 COLOR_Format16bitRGB565
15 COLOR_Format32bitBGRA8888
16 COLOR_Format32bitARGB8888
19 COLOR_FormatYUV420Planar
20 COLOR_FormatYUV420PackedPlanar
21 COLOR_FormatYUV420SemiPlanar
22 COLOR_FormatYUV422Planar
23 COLOR_FormatYUV422PackedPlanar
24 COLOR_FormatYUV422SemiPlanar
25 COLOR_FormatYCbYCr
26 COLOR_FormatYCrYCb
27 COLOR_FormatCbYCrY
28 COLOR_FormatCrYCbY
39 COLOR_FormatYUV420PackedSemiPlanar
2130708361 COLOR_FormatSurface
2135033992 COLOR_FormatYUV420Flexible

说明:颜色格式值表格里没有列出的值表示 Android MediaCodecInfo.CodecCapabilities 源码中没有找到对应的“含义”。

源代码

需要源码的朋友,可以从我的 Github上下载。

参考文献

*[I420]:属于 YUV420P。也叫做 IYUV,YU12。存储格式:YYYYYYYY UU VV

*[YV12]:属于 YUV420P。存储格式:YYYYYYYY VV UU

*[NV12]:属于 YUV420SP。存储格式:YYYYYYYY UVUV

*[NV21]:属于 YUV420SP。存储格式:YYYYYYYY VUVU

[^i420]: I420 yuv pixel formatV4L2_PIX_FMT_YVU420 (‘YV12’), V4L2_PIX_FMT_YUV420 (‘YU12’)
[^yv12]: YV12 yuv pixel formatV4L2_PIX_FMT_YVU420 (‘YV12’), V4L2_PIX_FMT_YUV420 (‘YU12’)
[^nv12]: NV12 yuv pixel formatV4L2_PIX_FMT_NV12 (‘NV12’), V4L2_PIX_FMT_NV21 (‘NV21’)
[^nv21]: NV21 yuv pixel formatV4L2_PIX_FMT_NV12 (‘NV12’), V4L2_PIX_FMT_NV21 (‘NV21’)

坚持原创及高品质技术分享,您的支持将鼓励我继续创作!