Android Camera2中怎么获取预览YUV数据

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,不过官方文档并没有指明这一点,说明实际情况还是要具体平台具体对待的。

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

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论