使用Camera2 API和ImageReader技术

9
我正在尝试在Galaxy S4上使用Camera2 API捕获图像数据。ImageReader用作表面提供者。已尝试使用ImageFormat.YV12和ImageFormat.YUV_420_888两种图像格式,结果相同。
设置似乎没有问题,并且我使用ImageReader从Image获取了图像。该图像有3个平面。缓冲区的大小符合预期,即Y平面为Width*Height,另外两个平面为(Width*Height)/4。
问题在于我无法正确地获取数据。第一个问题是Y平面数据是镜像的。虽然可以处理,但这很奇怪,因此我想知道是否应该这样做。
更糟糕的问题是其他两个平面似乎根本没有正确传递数据。例如,对于640x480的图像大小,导致U和V缓冲区大小为76800字节,只有缓冲区的前320个字节是非零值。这个数字会变化,并且似乎不遵循不同图像大小之间的固定比率,但对于每个大小的图像,它似乎是一致的。
我想知道是否有什么我在使用此API时忽略的东西。以下是代码。
public class OnboardCamera {
  private final String TAG = "OnboardCamera";

  int mWidth = 1280;
  int mHeight = 720;
  int mYSize = mWidth*mHeight;
  int mUVSize = mYSize/4;
  int mFrameSize = mYSize+(mUVSize*2); 

  //handler for the camera
  private HandlerThread mCameraHandlerThread;
  private Handler mCameraHandler;

  //the size of the ImageReader determines the output from the camera.
  private ImageReader mImageReader = ImageReader.newInstance(mWidth, mHeight, ImageFormat.YV12, 30);

  private Surface mCameraRecieverSurface = mImageReader.getSurface();
  {
      mImageReader.setOnImageAvailableListener(mImageAvailListener, mCameraHandler);
  }

  private byte[] tempYbuffer = new byte[mYSize];
  private byte[] tempUbuffer = new byte[mUVSize];
  private byte[] tempVbuffer = new byte[mUVSize];

  ImageReader.OnImageAvailableListener mImageAvailListener = new ImageReader.OnImageAvailableListener() {
      @Override
      public void onImageAvailable(ImageReader reader) {
          //when a buffer is available from the camera
          //get the image
          Image image = reader.acquireNextImage();
          Image.Plane[] planes = image.getPlanes();

          //copy it into a byte[]
          byte[] outFrame = new byte[mFrameSize];
          int outFrameNextIndex = 0;


          ByteBuffer sourceBuffer = planes[0].getBuffer();
          sourceBuffer.get(tempYbuffer, 0, tempYbuffer.length);

          ByteBuffer vByteBuf = planes[1].getBuffer();
          vByteBuf.get(tempVbuffer);

          ByteBuffer yByteBuf = planes[2].getBuffer();
          yByteBuf.get(tempUbuffer);

          //free the Image
          image.close();
      }
  };


  OnboardCamera() {
      mCameraHandlerThread = new HandlerThread("mCameraHandlerThread");
      mCameraHandlerThread.start();
      mCameraHandler = new Handler(mCameraHandlerThread.getLooper());

  }




  @Override
  public boolean startProducing() {
      CameraManager cm = (CameraManager) Ten8Application.getAppContext().getSystemService(Context.CAMERA_SERVICE);
      try {
          String[] cameraList = cm.getCameraIdList();
          for (String cd: cameraList) {
              //get camera characteristics
              CameraCharacteristics mCameraCharacteristics = cm.getCameraCharacteristics(cd);

              //check if the camera is in the back - if not, continue to next
              if (mCameraCharacteristics.get(CameraCharacteristics.LENS_FACING) != CameraCharacteristics.LENS_FACING_BACK) {
                  continue;
              }

              //get StreamConfigurationMap - supported image formats
              StreamConfigurationMap scm = mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

              android.util.Size[] sizes =  scm.getOutputSizes(ImageFormat.YV12);

              cm.openCamera(cd, mDeviceStateCallback, mCameraHandler);
          }

      } catch (CameraAccessException e) {
          e.printStackTrace();
          Log.e(TAG, "CameraAccessException detected", e);
      }
      return false;
  }

  private final CameraDevice.StateCallback mDeviceStateCallback = new CameraDevice.StateCallback() {
      @Override
      public void onOpened(CameraDevice camera) {
          //make list of surfaces to give to camera
          List<Surface> surfaceList = new ArrayList<>();
          surfaceList.add(mCameraRecieverSurface);

          try {
              camera.createCaptureSession(surfaceList, mCaptureSessionStateCallback, mCameraHandler); 
          } catch (CameraAccessException e) {
              Log.e(TAG, "createCaptureSession threw CameraAccessException.", e);
          }
      }

      @Override
      public void onDisconnected(CameraDevice camera) {

      }

      @Override
      public void onError(CameraDevice camera, int error) {

      }
  };

  private final CameraCaptureSession.StateCallback mCaptureSessionStateCallback = new CameraCaptureSession.StateCallback() {
      @Override
      public void onConfigured(CameraCaptureSession session) {
          try {
              CaptureRequest.Builder requestBuilder = session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
              requestBuilder.addTarget(mCameraRecieverSurface);
              //set to null - image data will be produced but will not receive metadata
              session.setRepeatingRequest(requestBuilder.build(), null, mCameraHandler); 

          } catch (CameraAccessException e) {
              Log.e(TAG, "createCaptureSession threw CameraAccessException.", e);
          }


      }

      @Override
      public void onConfigureFailed(CameraCaptureSession session) {

      }
  };
}
2个回答

3

我遇到了同样的问题,我相信问题出在Android API 21上。我升级到API 23后,相同的代码能够正常工作。同时,在API 22上也测试过,也没有问题。


谢谢回复!我可能不会很快再次涉及这个问题,但知道我不是疯了还是很好的。 - Nicholas Spencer

2
你是否关注了Image.Plane的行和像素步幅参数?
由于硬件内存映射限制,行步幅通常大于图像宽度,并且对于给定平面中图像的第y行,其在字节数组中的起始位置为(yrowStride),而不是(ywidth)。
如果是这种情况,那么在640x480图像的前320个字节之后(一个子采样色度数据的一行),U或V平面将在一段时间内为0——应该有(rowStride-width)字节的零或垃圾,然后下一行像素数据将开始。
请注意,如果pixelStride不为1,则还必须跳过像素值之间的字节;这通常在底层YCbCr缓冲区实际上是半平面而不是平面时使用。

在简化的代码中没有考虑到,但我进行了检查。对于某些大小的图像,行步幅稍微增加以填充行宽至16的倍数。我测试时每个大小的像素步幅始终为1。实际上,我将数据写入文件并在十六进制编辑器中打开以确认我的怀疑,因为图像呈现出亮绿色调(这暗示我获取到了0值)。在初始值之后,缓冲区的长度都包含了0值。 - Nicholas Spencer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接