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

45

我正在开发一款安卓应用程序,它能够处理来自相机的输入图像并将其显示给用户。这很简单,我在相机对象上注册了一个 PreviewCallback 并使用 setPreviewCallbackWithBuffer。这在旧的相机API中非常容易且运行流畅。

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

我正在尝试将我的应用程序移植为使用新的Camera2 API,并不确定应该如何操作。 我遵循了L Preview中的Camera2Video示例,允许录制视频。 但是,示例中没有直接的图像数据传输,因此我不知道从哪里获取图像像素数据以及如何处理它。

有人能帮助我或建议一种在Android L中获得PreviewCallback功能的方法,或者如何在显示到屏幕之前处理来自相机的预览数据吗?(相机对象上没有预览回调)

谢谢!


你解决了这个问题吗? - user1154390
是的,我做了。请查看VP的回复以及来自Android示例的Camera2Basic和Camera2Video。您需要创建一个ImageReader并使用setOnImageAvailableListener在捕获图像时获取新图像。为了绘制图像,我创建了一个OpenGL表面,它呈现纹理和一个着色器,将YUV_420_888转换为RGB - bubo
谢谢,我已经检查了两个存储库和VP响应。当我设置addTarget(mImageReader.getSurface())时,它只在可用的图像上提供三个帧,之后它会冻结预览。 - user1154390
当您没有从ImageReader中读取/关闭图像时,可能会发生这种情况(或类似情况)。确保在监听器onImageAvailable(...)中读取并关闭图像。即使您不使用图像,监听器也不能是空的,您需要读取它(例如使用reader.acquireNextImage())。 - bubo
感谢@bubo。我花了一些时间才弄清楚这个行为,因为它在文档中没有提到。 - user1154390
5个回答

34

为了更易于理解,将几个答案结合起来。因为@VP的答案虽然技术上很清晰,但如果是您第一次从Camera转移到Camera2,可能难以理解:

使用https://github.com/googlesamples/android-Camera2Basic作为起点,修改以下内容:

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

Surface mImageSurface = mImageReader.getSurface();
将新的表面作为输出目标添加到您的 CaptureRequest.Builder 变量中。使用Camera2Basic示例,该变量将是mPreviewRequestBuilder
mPreviewRequestBuilder.addTarget(mImageSurface);

这是带有新换行符的代码片段(请查看我的@AngeloS评论):

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。(另外,我建议您降低预览大小以获得更流畅的操作 - 这是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();
    }

我使用Google的CameraView而不是Camera2Basic,但仍然遇到了java.lang.IllegalArgumentException: submitRequestList:216: Request targets Surface that is not part of current capture session错误。 - Sira Lam
3
在上面的代码片段中,可以在 onImageAvailable 函数内处理/修改图像。然而,这样做不会在预览中显示图像,对吗?如何在处理完成后在预览(TextureView)上显示图像? - vyi
我已经尝试了你的方法,但是预览被暂停了,只收到了一些onImageAvailable的回调。 - Bipin Vayalu
定义的表面需要添加到发送到createCaptureSession的列表中 - 就像已经存在的预览表面一样 - 如果您有正在显示的预览。 - slott

30

由于相机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)。


1
@Ruban 我根据VP的答案添加了下面的代码响应,以澄清这个实现:https://dev59.com/QV8e5IYBdhLWcg3wssLG#43564630 - AngeloS
1
即使使用YUV_420_888,与Camera1的PreviewCallback相比,摄像头的预览仍然有显著的延迟,并且在Moto G3上最多只能产生10 FPS(而在相同分辨率下,Camera1可以产生超过30 FPS)。这是一个已知的问题吗? - Dmitry Zaytsev
ImageReader获取JPEG和ImageReader的格式为YUV_420_888不合理。 - user25
@VP,我已经按照您的方法尝试了,但仍然没有成功。我也尝试过TEMPLATE_RECORD和TEMPLATE_PREVIEW。 - Bipin Vayalu
我正在设置两个表面,一个是预览,另一个是ImageReader(使用YUV_420_888格式)。当我开始预览时,预览会暂停,并在onImageAvailable上收到5-7个回调。 - Bipin Vayalu
显示剩余4条评论

16

在ImageReader.OnImageAvailableListener类中,按照下面的方式读取后关闭图像(这将释放缓冲区以供下一次捕获使用)。您需要处理关闭时的异常。

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

这是一个非常有用的代码片段。 - twerdster
你如何在每一帧上实现这个? - Florian Mac Langlade
只有在使用JPEG格式创建ImageReader时,此内容才是正确的,否则图像中将会有多个平面。 - Charlesjean

9

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

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应用程序(https://github.com/googlesamples/android-Camera2Basic)在第一次图像捕获后会卡住。你解决了这个问题吗? - user2924714
此外,图像保存应在 UI 线程之外完成。 - user2924714
是的,我将这一行代码 mCaptureSession.capture(captureBuilder.build(), CaptureCallback, null); 改为了 mCaptureSession.capture(captureBuilder.build(), mCaptureCallback, mBackgroundHandler);。 - panonski
savePreviewShot实际上是保存可见的预览帧,还是捕获下一个可用的帧?createCaptureRequest暗示了未来吗? - RoundSparrow hilltx
一个预览帧只能短暂地显示出来。 :) 帧一直在流动(在我最快的设备上大约每秒30帧)。然而,设备只能保存其中的几个(例如5-6),因此可以说只有6分之1的帧被保存。 - panonski
显示剩余2条评论

2
最好使用最大图像缓冲为2来初始化ImageReader,然后在onImageAvailable()中使用reader.acquireLatestImage()。因为acquireLatestImage()将从ImageReader的队列中获取最新的图像,并删除旧的图像。对于大多数用例,建议使用此函数而不是acquireNextImage(),因为它更适合实时处理。请注意,最大图像缓冲区应至少为2。并且在处理完图像后记得close()您的图像。

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