使用 Android L 和 Camera2 API 处理相机预览图像数据

Posted

技术标签:

【中文标题】使用 Android L 和 Camera2 API 处理相机预览图像数据【英文标题】:Camera preview image data processing with Android L and Camera2 API 【发布时间】:2014-10-17 05:00:21 【问题描述】:

我正在开发一个 android 应用程序,该应用程序正在处理来自相机的输入图像并将其显示给用户。这很简单,我在相机对象上注册了一个PreviewCallbacksetPreviewCallbackWithBuffer。 这很简单,并且可以与旧的相机 API 顺利配合

public void onPreviewFrame(byte[] data, Camera cam) 
    // custom image data processing

我正在尝试移植我的应用程序以利用新的 Camera2 API,但我不确定我应该怎么做。我遵循了 L Preview 示例中的 Camera2Video,它允许录制视频。但是,样本中没有直接的图像数据传输,所以我不明白我到底应该从哪里获取图像像素数据以及如何处理它。

任何人都可以帮助我或建议如何在android L中获得PreviewCallback的功能,或者如何在将其显示到屏幕之前处理来自相机的预览数据? (相机对象上没有预览回调)

谢谢!

【问题讨论】:

你解决了这个问题。 是的,我做到了。检查 VP 的响应以及来自 android 示例的 Camera2Basic 和 Camera2Video。您需要创建一个 ImageReader 并在捕获时使用setOnImageAvailableListener 获取新图像。为了绘制图像,我创建了渲染纹理的 OpenGL 表面和将 YUV_420_888 转换为 RGB 的着色器。 谢谢,我已经检查了 repos 和 VP 的回复。当我设置 addTarget(mImageReader.getSurface());它在冻结预览之后只提供三帧 onImageAvailable。 当您不从 ImageReader 读取/关闭图像时,可能会发生这种情况(或类似情况)。确保在侦听器onImageAvailable(...) 中您确实阅读并关闭了图像。 Listener 不能为空,即使您不使用需要读取的图像(例如使用 reader.acquireNextImage())。 谢谢@bubo。我花了一段时间才弄清楚这种行为,因为文档中没有提到它。 【参考方案1】:

将几个答案组合成一个更容易理解的答案,因为@VP 的答案虽然在技术上很清楚,但如果您是第一次从 Camera 转移到 Camera2,就很难理解:

以https://github.com/googlesamples/android-Camera2Basic为起点,修改如下:

createCameraPreviewSession() 中从mImageReader 初始化一个新的Surface

Surface mImageSurface = mImageReader.getSurface();

将该新表面添加为CaptureRequest.Builder 变量的输出目标。使用 Camera2Basic 示例,变量将为 mPreviewRequestBuilder

mPreviewRequestBuilder.addTarget(mImageSurface);

这是带有新行的 sn-p(请参阅我的 @AngeloS cmets):

private void createCameraPreviewSession() 

    try 

        SurfaceTexture texture = mTextureView.getSurfaceTexture();
        assert texture != null;

        // We configure the size of default buffer to be the size of camera preview we want.
        texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());

        // This is the output Surface we need to start preview.
        Surface surface = new Surface(texture);

        //@AngeloS - Our new output surface for preview frame data
        Surface mImageSurface = mImageReader.getSurface();

        // We set up a CaptureRequest.Builder with the output Surface.
        mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

        //@AngeloS - Add the new target to our CaptureRequest.Builder
        mPreviewRequestBuilder.addTarget(mImageSurface);

        mPreviewRequestBuilder.addTarget(surface);

        ...

接下来,在setUpCameraOutputs() 中,在初始化ImageReader 时将格式从ImageFormat.JPEG 更改为ImageFormat.YUV_420_888。 (PS,我还建议您降低预览尺寸以获得更流畅的操作 - Camera2 的一项不错的功能)

mImageReader = ImageReader.newInstance(largest.getWidth() / 16, largest.getHeight() / 16, ImageFormat.YUV_420_888, 2);

最后,在ImageReader.OnImageAvailableListeneronImageAvailable() 方法中,一定要使用@Kamala 的建议,因为如果不关闭预览会在几帧后停止

    @Override
    public void onImageAvailable(ImageReader reader) 

        Log.d(TAG, "I'm an image frame!");

        Image image =  reader.acquireNextImage();

        ...

        if (image != null)
            image.close();
    

【讨论】:

我得到了 java.lang.IllegalArgumentException: submitRequestList:216: Request targets Surface that is not part of current capture session,但使用的是 Google 的 CameraView 而不是 Camera2Basic。 在上面的代码sn-p中,可以在onImageAvailable函数中处理/修改图像。但是,这不会在预览中显示图像。正确的?处理完成后如何在预览中显示图片(TextureView)? 我已经尝试过你的方法,但是预览被暂停并且只接收到几个 onImageAvailable 的回调。 定义的表面需要添加到发送到 createCaptureSession 的列表中 - 就像已经存在的预览表面一样 - 如果您有预览显示。【参考方案2】:

由于Camera2 API 与当前的Camera API 非常不同,因此阅读文档可能会有所帮助。

一个很好的起点是camera2basic 示例。它演示了如何使用Camera2 API 并配置ImageReader 以获取JPEG 图像并注册ImageReader.OnImageAvailableListener 以接收这些图像

要接收预览帧,您需要将ImageReader 的表面添加到setRepeatingRequestCaptureRequest.Builder

此外,您应该将ImageReader 的格式设置为YUV_420_888,这将在 8MP 时为您提供 30fps(文档保证 Nexus 5 在 8MP 时为 30fps)。

【讨论】:

你好,@VP。您能否告诉我如何将我在 onImageAvailable 侦听器中收到的 YUV_420_888 图像转换为位图? 给你:最大尺寸 = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.YUV_420_888)), new CompareSizesByArea());而不是 .JPEG - 这是来自 camera2Basic repo 库。 @Ruban 我根据 VP 的回答添加了一个带有以下代码的响应,以澄清此实现:***.com/a/43564630/630996 即使在使用 YUV_420_888 时,相机的预览也明显滞后(与 Ca​​mera1 的 PreviewCallback 相比)并且在 Moto G3 上产生不超过 10 FPS(而在相同的情况下产生超过 30 FPS Camera1 上的分辨率)。这是一个已知问题吗? 如果您的 Camera2 帧速率较低,请查看此答案***.com/a/51083567/2606068 祝您好运。@DmitryZaytsev【参考方案3】:

在 ImageReader.OnImageAvailableListener 类中,读取后关闭图像,如下图(这将释放缓冲区以供下次捕获)。您必须在关闭时处理异常

      Image image =  imageReader.acquireNextImage();
      ByteBuffer buffer = image.getPlanes()[0].getBuffer();
      byte[] bytes = new byte[buffer.remaining()];
      buffer.get(bytes);
      image.close();

【讨论】:

这是一个非常有用的sn-p 如何在每一帧上做到这一点? 这仅在您使用JPEG格式创建ImageReader时正确,否则图像中会有多个平面【参考方案4】:

我需要同样的东西,所以我使用了他们的示例,并在相机处于预览状态时添加了对新函数的调用。

private CameraCaptureSession.CaptureCallback mCaptureCallback
            = new CameraCaptureSession.CaptureCallback()
    private void process(CaptureResult result) 
        switch (mState) 
            case STATE_PREVIEW: 
                    if (buttonPressed)
                        savePreviewShot();
                    
                break;
            

savePreviewShot() 只是原始captureStillPicture() 的回收版本,适用于预览模板。

   private void savePreviewShot()
        try 
            final Activity activity = getActivity();
            if (null == activity || null == mCameraDevice) 
                return;
            
            // This is the CaptureRequest.Builder that we use to take a picture.
            final CaptureRequest.Builder captureBuilder =
                    mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            captureBuilder.addTarget(mImageReader.getSurface());

            // Orientation
            int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
            captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));

            CameraCaptureSession.CaptureCallback CaptureCallback
                    = new CameraCaptureSession.CaptureCallback() 

                @Override
                public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request,
                                               TotalCaptureResult result) 
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss:SSS");
                    Date resultdate = new Date(System.currentTimeMillis());
                    String mFileName = sdf.format(resultdate);
                    mFile = new File(getActivity().getExternalFilesDir(null), "pic "+mFileName+" preview.jpg");

                    Log.i("Saved file", ""+mFile.toString());
                    unlockFocus();
                
            ;

            mCaptureSession.stopRepeating();
            mCaptureSession.capture(captureBuilder.build(), CaptureCallback, null);
         catch (Exception e) 
            e.printStackTrace();
        
    ;

【讨论】:

我正在使用您的解决方案。问题是 camera2basic 应用程序 (github.com/googlesamples/android-Camera2Basic) 在第一次图像捕获后卡住了。你解决了吗? 另外,图片保存应该在Ui线程之外进行 是的,我更改了行 mCaptureSession.capture(captureBuilder.build(), CaptureCallback, null);到 mCaptureSession.capture(captureBuilder.build(), mCaptureCallback, mBackgroundHandler); 是 savePreviewShot 实际上保存可见的预览帧,还是捕获下一个可用帧? createCaptureRequest 暗示未来? 单个预览帧花费很少的时间可见。 :) 帧一直在流动(在我最快的设备上大约为 30 帧/秒)。但是,设备只能保存其中的几个(例如 5-6 个),因此您可以说 6 帧中有 1 个被保存。【参考方案5】:

最好初始化ImageReader,最大图像缓冲区为2,然后在onImageAvailable()中使用reader.acquireLatestImage()

因为acquireLatestImage() 将从 ImageReader 的队列中获取最新的图像,丢弃旧的图像。对于大多数用例,建议在 acquireNextImage() 上使用此函数,因为它更适合 实时 处理。请注意,最大图像缓冲区至少应为2

并记得在处理后close()您的图像。

【讨论】:

以上是关于使用 Android L 和 Camera2 API 处理相机预览图像数据的主要内容,如果未能解决你的问题,请参考以下文章

Android:如何检查设备是不是实现了 Camera2 api 功能?

Android使用camera2复制内置视频录制质量和帧率

Android开发Camera2相关

使用 Android Camera2 API 进行人脸检测和画圆

支持 Android Camera Api 和 Camera2 Api 的问题

Android Camera2 拍照——使用TextureView