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

前言

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

  下图是截止 2019/05/07 官方统计结果。(吐个槽,6 年前的系统居然还有这么高占用率,也是醉了。再看看 iOS,哎,还是不比了,怕心脏不行。)

Android Version Distribution

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

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

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

  Android 5 之前的版本,Camera Preview 支持的格式是包括 NV21, YV12,NV16,默认图像格式是 NV21,官方强烈建议使用 NV21 或 YV12。而对于 Andriod 5 及之后版本,Google 推出了全新设计的新版 Camera2,并且提出了全新的格式 YUV_420_888,该格式也是 Android 建议的格式。而 MediaCodec 要求的格式是 I420(也就是 YU12)。此处就有坑了,大家上网查关于摄像头数据旋转算法时,移植到自己的代码里会发现有的好用,有的不好用。其实那些看起来不好用的代码,很可能是没有问题的,有问题的是网上的文章并没有准确的告诉你该旋转算法是在什么具体数据格式下才能使用。因此导致了由于数据格式不匹配,出现无法旋转或旋转后出现画屏,颜色不对等奇怪的现象。

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

关于 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 基础知识”,对于不太了解的人来说,可能一下就看蒙圈了。当然这么多格式对于 开发而言是十分不得的,因此 Image 类就这样横空出世了。

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 这个格式集合,那怎么才能知道具体的格式呢?请您继续往下看。

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.

  最重要的是,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 分量的值。

  对于 U/V plane 来说,Image 能够保证 U 和 V 具体相同的 rowStridepixelStride。从代码角度来说就是 uPlane.getRowStride() == vPlane.getRowStride()uPlane.getPixelStride() == vPlane.getPixelStride()Image 可以通过 ImageReader.acquireLatestImage() 获取。

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

  接下来看看 U 和 V 分量,我们考虑其中的三类格式:Planar,SemiPlanar 和 PackedSemiPlanar。

SemiPlanar

  按道理应该先讲 Planar,但是在实际使用中,我还没有遇到 Camera 返回 Planar 种类的情况,仅遇到了 SemiPlanar 种类,因此我们先来看下 SemiPlanar。该格式下 U 和 V 分量是交叉存储的,例如:YYYYYYYYUVUV。需要注意的是 U 和 V 分量并没有被分离出来。下面以 1280x720 图像分辨率为例,让我们先看一下 YUV420SemiPlanar 格式的 Image 真实解析记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
image.format: 35
image.planes: 3 planes
plane #0:
  pixelStride 1
  rowStride 1280
  buffer size 921600
Finished reading data from plane 0
plane #1:
  pixelStride 2
  rowStride 1280
  buffer size 460799
Finished reading data from plane 1
plane #2:
  pixelStride 2
  rowStride 1280
  buffer size 460799
Finished reading data from plane 2

  其中 image.format: 35 代表 ImageFormat.YUV_420_888。一共包含了 3 个 planes。图像分辨率为 1280x720 = 921600 像素。由此可见,Y 分量包含全部的像素点,U 和 V 分量长度是 Y 分量长度的 1/2,按理说如果 U 分量只包含 U 分量数据的话,其长度应该是 Y 分量长度的 1/4 才对。为什么多出来了 1/4 呢?我们接着分析。

  U 分量的 rowStride 是 1280,即每行有 1280 个数据,但是 pixelStride 是 2。代表相邻像素样本之间的距离 2,也就是说行内索引值为 0,2,4,6... 这样的偶数索引值才有 U 分量。这就表明行内每两个像素点共用一个 U 值,行间每两个像素点共用一个 U 值,即 YUV420。

  V 分量数据含义和 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

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

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

Planar

  目前我在实际使用中,还没有遇到返回的 Image 对象符合 PlanarPackedSemiPlanar 的情况。因此无法给出真实解析记录。只能引用下我找到的相对靠谱的文章,仅大家参考。不过大家都已经看到这了,有了前面的知识介绍,即使不给出示例,相信大家也一定能自己总结出规律。

  以下内容来自互联网,正确性大家可以自行验证(毕竟文章都看到这了,大家应该已经掌握了足够的知识,来验证下文内容的正确性)

Image 真实解析记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
image.format: 35
image.planes: 3 planes
plane #0:
pixelStride 1
rowStride 1920
width 1920
height 1080
buffer size 2088960
Finished reading data from plane 0
plane #1:
pixelStride 1
rowStride 960
width 1920
height 1080
buffer size 522240
Finished reading data from plane 1
plane #2:
pixelStride 1
rowStride 960
width 1920
height 1080
buffer size 522240
Finished reading data from plane 2

注意:1920 x 1080 = 2073600,但是上例中是 2088960(暂认为是原作者在当时实际获取数据时,获取到的就是 2088960 吧,毕竟通过查资料,发现有人遇到过 rowStride 和真实图像 width 不一样的情况,虽然我还没遇到过。因此暂认为原作者不是笔误。) 2088960 / 4 = 522240 这个是没问题的。

  具体分析过程:
   image.format: 35 代表 ImageFormat.YUV_420_888。一共包含了 3 个 planes。图像大小为 1920 x 1280。
  Y 分量包含全部像素点,rowStride 等于 1920,说明一行有 1920 个值,那 Y 有多少行呢?我们计算下:2088960 / 1920 = 1088(该值用于对比 U/V 分量的对应值)。
  
  U 分量 pixelStride1,说明数据是连续存储的。U 分量 rowStride 等于 960,说明一行有 960 个值,其长度刚好为 Y 分量的 1/2,即行内每两个像素共用一个 U 值。那么 U 分量共有多少行吗?那篇文章)里原作者的原文是“根据其buffer size得出共有540行”(但是我们实际计算下 522240 / 960 = 544 才对)。540 行的话,刚好是实际图像高的 1/2(之前计算得到 Y 分量是 1088 行。1088 / 544 = 2),即行间每两个像素共用一个 U 值。U 分量的实际长度刚好是 Y 分量的 1 / 4(2088960 / 52240 = 4)。这就是 YUV420 采样了。

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

PackedSemiPlanar

  同样,我也没有遇到这种情况。因此无法给出解析实例。另外我也参考了网上的其它文章,该文章)的作者同样也没有遇到该情况,他给出的解释是:

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

  总结一下:我们只需要根据 pixelStride 就能知道是 YUV420P 还是 YUV420SP 了,而不必关心其具体的格式。再根据 rowStrideImage 中的 widthheight 就可以获取到对应 plane 中的相应分量了。

关于 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.

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

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

Camera2

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

ImageReader Source

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

  通过 Camera2 得到的图像被保存在 ImageReader 中。我们再通过 ImageReader#acquireLatestImage() 就可以得到所最终 Image 对象。此处就又有问题了,我们拿到的仅仅是原始图像数据,通常需要对图像进行旋转之后再使用。又如通过 MediaCodec 进行实时 H264 编码的话,我们需要将原始数据转换成 I420 才可以。

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

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

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

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

Camera2 使用方法

  之前说了那么多,现在终于进入正题了。是时候讲讲 Camera2 的使用方法了。需要提醒大家,Camera2 的使用方法和以前的 Camera1 是完全不同的。  

Camera2 Capture Image

图像转换性能分析

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

图像分辨率: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

参考文献

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