如何使用Android 5.0中的camera2 API实时获取每帧数据?

7
我现在正在使用camera2Basic工作,试图获取每帧数据进行图像处理。我在Android 5.0中使用相机2 API,在仅进行相机预览时一切正常且流畅。但是当我使用ImageReader.OnImageAvailableListener回调来获取每帧数据时,预览会卡顿,这会导致糟糕的用户体验。 以下是我的相关代码:

这是相机和ImageReader的设置,我将图像格式设置为YUV_420_888

public<T> Size setUpCameraOutputs(CameraManager cameraManager,Class<T> kClass, int width, int height) {
    boolean flagSuccess = true;
    try {
        for (String cameraId : cameraManager.getCameraIdList()) {
            CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
            // choose the front or back camera
            if (FLAG_CAMERA.BACK_CAMERA == mChosenCamera &&        
                    CameraCharacteristics.LENS_FACING_BACK != characteristics.get(CameraCharacteristics.LENS_FACING)) {
                continue;
            }
            if (FLAG_CAMERA.FRONT_CAMERA == mChosenCamera &&  
                    CameraCharacteristics.LENS_FACING_FRONT != characteristics.get(CameraCharacteristics.LENS_FACING)) {
                continue;
            }
            StreamConfigurationMap map = characteristics.get(
                    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

            Size largestSize = Collections.max(
                    Arrays.asList(map.getOutputSizes(ImageFormat.YUV_420_888)),
                    new CompareSizesByArea());

            mImageReader = ImageReader.newInstance(largestSize.getWidth(), largestSize.getHeight(),
                    ImageFormat.YUV_420_888, 3);

            mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler);
            ...
            mCameraId = cameraId;
       }
    } catch (CameraAccessException e) {
        e.printStackTrace();
    } catch (NullPointerException e) {

    }
    ......
}

当相机成功打开后,我会创建一个CameraCaptureSession用于相机预览。

private void createCameraPreviewSession() {
    if (null == mTexture) {
        return;
    }

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

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

    // We set up a CaptureRequest.Builder with the output Surface.
    try {
        mPreviewRequestBuilder =
                mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        mPreviewRequestBuilder.addTarget(mImageReader.getSurface());
        mPreviewRequestBuilder.addTarget(surface);

        // We create a CameraCaptureSession for camera preview
        mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
                new CameraCaptureSession.StateCallback() {

                    @Override
                    public void onConfigured(CameraCaptureSession session) {
                        if (null == mCameraDevice) {
                            return;
                        }

                        // when the session is ready, we start displaying the preview
                        mCaptureSession = session;

                        // Finally, we start displaying the camera preview
                        mPreviewRequest = mPreviewRequestBuilder.build();
                        try {
                            mCaptureSession.setRepeatingRequest(mPreviewRequest,
                                    mCaptureCallback, mBackgroundHandler);
                        } catch (CameraAccessException e) {
                            e.printStackTrace();
                        }
                    }

                    @Override
                    public void onConfigureFailed(CameraCaptureSession session) {

                    }
                }, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

最后一个是 ImageReader.OnImageAvailableListener 回调函数。
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {
                Log.d(TAG, "The onImageAvailable thread id: " + Thread.currentThread().getId());
                Image readImage = reader.acquireLatestImage();
                readImage.close();
            }
        };

也许我设置有误,但我尝试了几次,它没有起作用。也许除了ImageReader之外还有其他获取帧数据的方法,但我不知道。 有人知道如何实时获取每个帧的数据吗?


你想做什么样的处理?如果您想保存图像,我建议使用ImageReader. 但是,如果您想进行高效的实时处理,最好将数据发送到与Allocation相关联的Surface缓冲区。 - rcsumner
@Sumner,我想直接从相机获取每一帧并进行人脸检测,而不是保存图像。你提供的解决方案似乎很好,能否提供更多细节? - CJZ
哈哈,不是真的,抱歉。我通常只是为了我的目的保存图像。我知道它涉及使用RenderScript。请查看Allocation类文档。 - rcsumner
然而,也要注意到许多设备提供面部检测功能,但并非所有设备都有。如果您针对特定设备进行开发,请查看其是否通过camera2 API内置了此功能。 - rcsumner
@Sumner,我有同样的问题,但我尝试实时检查QR码,我应该使用MediaCodec吗?作为Surface缓冲区? - Gutyn
显示剩余2条评论
2个回答

1
我不相信陈是正确的。在我测试过的设备上,图像格式几乎没有对速度产生影响。相反,问题似乎出现在图像大小上。在使用图像格式YUV_420_888的Xperia Z3 Compact上,我可以在StreamConfigurationMapgetOutputSizes方法中获得许多不同的选项:
[1600x1200, 1280x720, 960x720, 720x480, 640x480, 480x320, 320x240, 176x144]

对于这些特定的尺寸,当将mImageReader.getSurface()设置为mPreviewRequestBuilder的目标时,我可以获得的最大帧率为:
[13, 18, 25, 28, 30, 30, 30, 30 ]

所以一个解决方案是使用更低的分辨率来达到您想要的速度。对于好奇的人...请注意,这些时间似乎不受行影响。
    mPreviewRequestBuilder.addTarget(surface);
...
    mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),

我担心在屏幕上添加表面可能会增加开销,但如果我删除第一行并将第二行更改为


    mCameraDevice.createCaptureSession(Arrays.asList(mImageReader.getSurface()),

然后我看到帧率变化不到1 fps,所以似乎无论你是否在屏幕上显示图像都没有关系。 我认为camera2 API或ImageReader框架中有一些开销,使得无法获得TextureView明显获得的完整速率。 最令人失望的事情之一是,如果您切换回已弃用的相机API,可以通过使用Camera.setPreviewCallbackWithBuffer方法设置PreviewCallback轻松获得30 fps。使用该方法,我可以获得30fps,而不管分辨率如何。具体而言,尽管它不直接提供1600x1200,但它确实提供1920x1080,即使这也是30fps。

这可能会受到ImageReader回调中处理量和所选择的缓冲区大小(在ImageReader.newInstance(width, height, format, <buffer size>)中)的影响。如果您设置了较低的缓冲区大小(例如仅1帧),相机必须等待您的处理完成,因此您可能会获得更少的FPS。如果您使用较大的缓冲区(例如3或5帧),则可以有更多时间处理单个帧而不影响预览的FPS。在onImageAvailable()中,您还应该使用image.acquireLatestImage()而不是image.acquireNextImage()以不降低FPS。 - Robyer
你是从理论上还是从经验上说的?也许你所说的只适用于较新的设备。当我进行时间测试时,我确保使用的回调函数基本为空。我的观察结果如上所述。然而,这个答案已经有2年了,我的答案可能不适用于更新的设备。 - bremen_matt
我从经验中说话,但是对于具有适当的Camera2支持(不是LEGACY级别)的新设备,现在我也意识到我所说的情况与您所说的不同(我在谈论通过ImageReader表面处理帧时预览表面中较低的FPS)。我不知道您的设备的Camera2支持情况如何,但我可以想象,如果它是LEGACY(可能基本上只是Camera1 API的包装器),那么它可能会带来一些性能问题。在当前设备上,我使用ImageReader和YUV_420_888以任何分辨率获得最大FPS。 - Robyer
我测试了5个遗留设备,明显没有达到最大帧率。当时真的很烦人,因为Android开发人员一直说这是不可能的,因为Camera2 api是一个“薄包装器”(他们的话不是我的)。但我最终得到了一些非遗留设备来确认问题确实只存在于遗留设备中。这是当时的一个问题:https://dev59.com/vJ7ha4cB1Zd3GeqPnK80 - bremen_matt
有趣。我刚刚意识到,当你阅读这个链接 https://developer.android.com/reference/android/graphics/ImageFormat#NV21 时,他们说“这是相机预览图像的默认格式(NV21)(...) 对于android.hardware.camera2 API,建议使用YUV_420_888格式代替YUV输出。”那么如果他们在旧设备上在幕后执行NV21-> YUV_420_888转换呢?这可能会解释低帧率的问题,解决方案可能只是请求NV21。 - Robyer
这已经在我的记忆边缘了,但我确实记得测试了设备支持的每种格式。我不再从事Android编程,但我认为Camera2 API允许您查询支持的格式,并尝试循环遍历所有这些格式。没有任何区别。但是,如果您真的想彻底解决这个问题,您将不得不找到一个可用的旧设备。就像我说的,我已经一年多没有做Android了。 - bremen_matt

0

我正在尝试同样的事情,我认为你可以像这样更改格式

mImageReader = ImageReader.newInstance(largestSize.getWidth(),
                                       largestSize.getHeight(),
                                       ImageFormat.FLEX_RGB_888, 3);

因为使用YUV可能会导致CPU压缩数据,这可能需要一些时间。RGB可以直接显示在设备上。从图像中检测人脸应该放在其他线程中,你必须知道这一点。


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