首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android Camera2中如何获取预览YUV数据

Android Camera2中如何获取预览YUV数据

作者头像
雪月清
发布2020-06-23 16:16:57
6.1K0
发布2020-06-23 16:16:57
举报
文章被收录于专栏:雪月清的随笔雪月清的随笔

Camera1中我们可以通过onPreviewFrame接口直接获取到默认为NV21格式的预览数据, 如下图注释所示,还可以通过调用setPreviewFormat方法要求Camera返回YV12格式的预览数据

那么在Camera2的架构中,我们要如何获取NV21或者YV12格式的预览数据呢?

在之前的文章Android Camera2详解中描述到,要获取每一帧的预览数据,我们需要ImageReader这个类的帮助:

val imageReader = ImageReader(width, height, format, maxImages)

可以看到有一个format的参数可以指定,遗憾的是NV21格式是不支持的,进入ImageReader的构造方法中可以看到:

if (format == ImageFormat.NV21) {
    throw new IllegalArgumentException( "NV21 format is not supported");
}

NV21

查看文档关于NV21的描述,发现在Camera2中官方建议使用YUV_420_888

YUV_420_888是一种Y:U:V按4:1:1的比例进行采样的格式,也就是说其中每一个UV都被四个Y共享, 888表示每一个分量都是8bits

NV21和YV12都是这种YUV_420的采样格式,只是其中U,V分量的排列不一样.

NV21:先排Y, 再VU交替排列,  码流:YYYY YYYY VU VU
YV12:先排Y, 再排V, 最后排列U,  码流:YYYY YYYY VV UU

虽然YUV存在444,422,420等不同的采样方式,但是深入ImageReader源码后,发现只能使用YUV_420_888,另外两种会抛出UnsupportedOperationException异常

在ImageReader的实例中,参数format设置为YUV_420_888,并注册数据回调后,对于每一帧预览,我们将拿到一个包装对象Image

ImageReader.OnImageAvailableListener {
    val image = it.acquireLatestImage
    //...
}

如何从这个Image对象中获取具体的YUV byte[]数据呢?

在YUV_420_888这种格式下拿到的Image对象,存在以下几点规则:

  • Y,U,V的数据是分别存储在3个plane中的;
  • plane#0为Y分量,plane#1为U分量,plane#2为V分量;
  • Y-plane的pixelStride一定为1,而U-plane和V-plane的pixelStride则不固定(所谓pixelStride是指连续的码流中有效位的偏移,1表示数据是紧凑的,连续有效,中间不存在无效数据)
  • 每一个plane都有一个rowStride,表示一行数据的长度,rowStride >= imageWidth,这就是有些图片存在绿边的原因;
  • U-plane的rowStride一定等于V-plane的rowStride

知道了相关的规则,就可以开始撸代码了

首先将Y,U,V分量的数据依次提取出来:

/**
 * take YUV data from image, output data format-> YYYYYYYYUUVV
 */
private fun readYuvDataToBuffer(image: Image, data: ByteArray): Boolean {
    if (image.format != ImageFormat.YUV_420_888) {
        throw IllegalArgumentException("only support ImageFormat.YUV_420_888 for mow")
    }

    val imageWidth = image.width
    val imageHeight = image.height
    val planes = image.planes
    var offset = 0
    for (plane in planes.indices) {
         val buffer = planes[plane].buffer ?: return false
         val rowStride = planes[plane].rowStride
         val pixelStride = planes[plane].pixelStride
         val planeWidth = if (plane == 0) imageWidth else imageWidth / 2 
         val planeHeight = if (plane == 0) imageHeight else imageHeight / 2
         if (pixelStride == 1 && rowStride == planeWidth) {
             buffer.get(data, offset, planeWidth * planeHeight)
             offset += planeWidth * planeHeight
          } else {
             // Copy pixels one by one respecting pixelStride and rowStride
             val rowData = ByteArray(rowStride)
             var colOffset: Int
             for (row in 0 until planeHeight - 1) {
                  colOffset = 0
                  buffer.get(rowData, 0, rowStride)
                  for (col in 0 until planeWidth) {
                       data[offset++] = rowData[colOffset]
                       colOffset += pixelStride
                  }
              }
              // Last row is special in some devices:
              // may not contain the full |rowStride| bytes of data
              colOffset = 0
              buffer.get(rowData, 0, min(rowStride, buffer.remaining()))
              for (col in 0 until planeWidth) {
                   data[offset++] = rowData[colOffset]
                   colOffset += pixelStride
              }
          }
      }
   
   return true
}

然后按照所需要的格式要求进行调整Y,U,V分量的排列

/**
 * take YUV image from image.
 */
 fun readYuvDataToBuffer(image: Image, format:Int, data: ByteArray): Boolean {
     require(!(format != ImageFormat.NV21 && format != ImageFormat.YV12)) {
        "output only support ImageFormat.NV21 and ImageFormat.YV12 for now"
     }

     val result = readYuvDataToBuffer(image, data)
     if (!result) {
         return false
     }
     // data(YU12): YYYY YYYY UU VV
     if (format == ImageFormat.NV21) {
         // convert to: YYYY YYYY VU VU
         val size = data.size
         val uv = ByteArray(size / 3)
         var uOffset = size / 6 * 4
         var vOffset = size / 6 * 5
         for (i in 0 until uv.size - 1 step 2) {
              uv[i] = data[vOffset++]
              uv[i + 1] = data[uOffset++]
          }
          
          val uvOffset = size / 3 * 2
          for (i in uvOffset until size) {
              data[i] = uv[i - uvOffset]
          }
       } else if (format == ImageFormat.YV12) {
          // convert to: YYYY YYYY VV UU
          val size = data.size
          val tmp = ByteArray(size / 6)
          val uOffset = size / 6 * 4
          val vOffset = size / 6 * 5
          System.arraycopy(data, uOffset, tmp, 0, tmp.size)
          System.arraycopy(data, vOffset, data, uOffset, tmp.size)
          System.arraycopy(tmp, 0, data, vOffset, tmp.size)
       }
    return true
}

YV12

ImageReader的format是可以直接指定为YV12的,这种格式下又是如何从Image对象中提取YUV byte[]数据的呢?

通过阅读源码我们得知,其实在应用层设置YV12或者YUV_420_888,最后被映射到framework层的都是同一个东西。所以我们还是要通过3个plane来分别获取Y,U,V分量的数据。

只不过从目前我测试的一些设备上来看,Y,U,V3个分量的pixelStride总是为1,不过官方文档并没有指明这一点,说明实际情况还是要具体平台具体对待的。

PS:YUV的采样方式有很多种,同一种采样方式还存在不同的排列方式,导致YUV格式五花八门的, 比如:NV21, NV12, YV12, YU12, YUY2...

关于这方面的内容推荐阅读头条大佬的文章一文读懂 YUV 的采样与格式,图文并茂,通俗易懂

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 雪月清的随笔 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档