• Android CameraX ImageAnalysis 获取视频帧


    CameraX使用ImageAnalysis分析器,可以访问缓冲区中的图像,获取视频帧数据。

    准备工作

    准备工作包括gradle,layout,动态申请相机权限,外部存储权限等等,大部分设置与CameraX 打开摄像头预览相同。

    gradle

    一些关键配置

        apply plugin: 'com.android.application'
        apply plugin: 'kotlin-android'
        apply plugin: 'kotlin-android-extensions'
        apply plugin: 'kotlin-kapt'
    
        android {
            compileSdkVersion 31
            buildToolsVersion "31.0.0"
            defaultConfig {
                applicationId "com.rustfisher.tutorial2020"
                minSdkVersion 21
                targetSdkVersion 31
            }
            buildFeatures {
                compose true
                dataBinding true
                viewBinding true
            }
    
            dataBinding {
                enabled = true
            }
    
            kotlinOptions {
                jvmTarget = "1.8"
            }
    
            compileOptions {
                sourceCompatibility JavaVersion.VERSION_1_8
                targetCompatibility JavaVersion.VERSION_1_8
            }
            composeOptions {
                kotlinCompilerExtensionVersion '1.0.1'
            }
        }
    
        dependencies {
            kapt "com.android.databinding:compiler:3.0.1"
            // 其他依赖...
    
            implementation "androidx.camera:camera-core:1.1.0-alpha11"
            implementation "androidx.camera:camera-camera2:1.1.0-alpha11"
            implementation "androidx.camera:camera-lifecycle:1.1.0-alpha11"
            implementation "androidx.camera:camera-view:1.0.0-alpha31"
            implementation "androidx.camera:camera-extensions:1.0.0-alpha31"
        }
    

    layout

    act_simple_preivew_x.xml

       <?xml version="1.0" encoding="utf-8"?>
       <layout>
    
           <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:layout_width="match_parent"
               android:layout_height="match_parent">
    
               <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
                   android:id="@+id/container"
                   android:layout_width="match_parent"
                   android:layout_height="match_parent">
    
                   <androidx.camera.view.PreviewView
                       android:id="@+id/previewView"
                       android:layout_width="match_parent"
                       android:layout_height="match_parent" />
               </FrameLayout>
    
               <LinearLayout
                   android:layout_width="match_parent"
                   android:layout_height="wrap_content"
                   android:layout_alignParentBottom="true"
                   android:gravity="center"
                   android:orientation="vertical"
                   android:padding="4dp">
    
                   <LinearLayout
                       android:layout_width="match_parent"
                       android:layout_height="wrap_content"
                       android:orientation="horizontal">
    
                       <Button
                           android:id="@+id/start"
                           style="@style/NormalBtn"
                           android:layout_width="wrap_content"
                           android:layout_height="wrap_content"
                           android:text="打开摄像头" />
    
                       <Button
                           android:id="@+id/end"
                           style="@style/NormalBtn"
                           android:layout_width="wrap_content"
                           android:layout_height="wrap_content"
                           android:layout_marginStart="4dp"
                           android:text="停止摄像头" />
    
                   </LinearLayout>
    
                   <LinearLayout
                       android:layout_width="match_parent"
                       android:layout_height="wrap_content"
                       android:layout_marginTop="4dp"
                       android:orientation="horizontal">
    
                       <Button
                           android:id="@+id/enable_ana"
                           style="@style/NormalBtn"
                           android:layout_width="wrap_content"
                           android:layout_height="wrap_content"
                           android:text="setAnalyzer" />
    
                       <Button
                           android:id="@+id/clr_ana"
                           style="@style/NormalBtn"
                           android:layout_width="wrap_content"
                           android:layout_height="wrap_content"
                           android:layout_marginStart="4dp"
                           android:text="clearAnalyzer" />
    
    
                       <Button
                           android:id="@+id/take_one_analyse"
                           style="@style/NormalBtn"
                           android:layout_width="wrap_content"
                           android:layout_height="wrap_content"
                           android:layout_marginStart="4dp"
                           android:text="截取" />
    
                   </LinearLayout>
               </LinearLayout>
    
           </RelativeLayout>
       </layout>
    

    ImageAnalysis获取视频帧并保存到本地

    androidx.camera.core.ImageAnalysis

    设置分析器

    先看简单的示例,在SimplePreviewXAct.java中使用ImageAnalysis

    private boolean mTakeOneYuv = false; // 获取一帧 实际工程中不要这么做
    
    private final ImageAnalysis mImageAnalysis =
            new ImageAnalysis.Builder()
                    //.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
                    .setTargetResolution(new Size(720, 1280)) // 图片的建议尺寸
                    .setOutputImageRotationEnabled(true) // 是否旋转分析器中得到的图片
                    .setTargetRotation(Surface.ROTATION_0) // 允许旋转后 得到图片的旋转设置
                    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                    .build();
    

    注意这里的setOutputImageRotationEnabled(true),启用了旋转后,分析器会多花费一些时间(毫秒级)。
    启用选择,setTargetRotation才有意义。

    onCreate方法里设置setAnalyzer

    // SimplePreviewXAct onCreate
    final ExecutorService executorService = Executors.newFixedThreadPool(2);
    mBinding.enableAna.setOnClickListener(v -> {
        Toast.makeText(getApplicationContext(), "启用分析器", Toast.LENGTH_SHORT).show();
        mImageAnalysis.setAnalyzer(executorService, imageProxy -> {
            // 下面处理数据
            if (mTakeOneYuv) {
                mTakeOneYuv = false;
                Log.d(TAG, "旋转角度: " + imageProxy.getImageInfo().getRotationDegrees());
                ImgHelper.useYuvImgSaveFile(imageProxy,  true); // 存储这一帧为文件
                runOnUiThread(() -> Toast.makeText(getApplicationContext(), "截取一帧", Toast.LENGTH_SHORT).show());
            }
            imageProxy.close(); // 最后要关闭这个
        });
    });
    

    为了更直观的看到分析器中的图片,我们想办法把图片数据保存了下来。

    绑定生命周期(启动相机)的时候,把mImageAnalysis传进去

    cameraProvider.bindToLifecycle(this, cameraSelector, preview, mImageAnalysis);
    

    相机运行起来,分析器中可以得到帧数据。ImgHelper代码和SimplePreviewXAct如下文。

    ImgHelper.java

    新建一个工具类来处理图片格式问题。

    ImgHelper.java

        import android.graphics.ImageFormat;
        import android.graphics.Rect;
        import android.graphics.YuvImage;
        import android.os.Environment;
        import android.util.Log;
    
        import androidx.camera.core.ImageProxy;
    
        import java.io.File;
        import java.io.FileOutputStream;
        import java.nio.ByteBuffer;
    
        public class ImgHelper {
            public static String TAG = "rfDevImg";
    
            // 获取到YuvImage对象 然后存文件
            public static void useYuvImgSaveFile(ImageProxy imageProxy, boolean outputYOnly) {
                final int wid = imageProxy.getWidth();
                final int height = imageProxy.getHeight();
                Log.d(TAG, "宽高: " + wid + ", " + height);
    
                YuvImage yuvImage = ImgHelper.toYuvImage(imageProxy);
                File file = new File(Environment.getExternalStorageDirectory(), "z_" + System.currentTimeMillis() + ".png");
                saveYuvToFile(file, wid, height, yuvImage);
                Log.d(TAG, "rustfisher.com 存储了" + file);
    
                if (outputYOnly) { // 仅仅作为功能演示
                    YuvImage yImg = ImgHelper.toYOnlyYuvImage(imageProxy);
                    File yFile = new File(Environment.getExternalStorageDirectory(), "y_" + System.currentTimeMillis() + ".png");
                    saveYuvToFile(yFile, wid, height, yImg);
                    Log.d(TAG, "rustfisher.com 存储了" + yFile);
                }
            }
    
            // 仅作为示例使用
            public static YuvImage toYOnlyYuvImage(ImageProxy imageProxy) {
                if (imageProxy.getFormat() != ImageFormat.YUV_420_888) {
                    throw new IllegalArgumentException("Invalid image format");
                }
                int width = imageProxy.getWidth();
                int height = imageProxy.getHeight();
                ByteBuffer yBuffer = imageProxy.getPlanes()[0].getBuffer();
                int numPixels = (int) (width * height * 1.5f);
                byte[] nv21 = new byte[numPixels];
                int index = 0;
                int yRowStride = imageProxy.getPlanes()[0].getRowStride();
                int yPixelStride = imageProxy.getPlanes()[0].getPixelStride();
                for (int y = 0; y < height; ++y) {
                    for (int x = 0; x < width; ++x) {
                        nv21[index++] = yBuffer.get(y * yRowStride + x * yPixelStride);
                    }
                }
                return new YuvImage(nv21, ImageFormat.NV21, width, height, null);
            }
    
            public static YuvImage toYuvImage(ImageProxy image) {
                if (image.getFormat() != ImageFormat.YUV_420_888) {
                    throw new IllegalArgumentException("Invalid image format");
                }
                int width = image.getWidth();
                int height = image.getHeight();
    
                // 拿到YUV数据
                ByteBuffer yBuffer = image.getPlanes()[0].getBuffer();
                ByteBuffer uBuffer = image.getPlanes()[1].getBuffer();
                ByteBuffer vBuffer = image.getPlanes()[2].getBuffer();
    
                int numPixels = (int) (width * height * 1.5f);
                byte[] nv21 = new byte[numPixels]; // 转换后的数据
                int index = 0;
    
                // 复制Y的数据
                int yRowStride = image.getPlanes()[0].getRowStride();
                int yPixelStride = image.getPlanes()[0].getPixelStride();
                for (int y = 0; y < height; ++y) {
                    for (int x = 0; x < width; ++x) {
                        nv21[index++] = yBuffer.get(y * yRowStride + x * yPixelStride);
                    }
                }
    
                // 复制U/V数据
                int uvRowStride = image.getPlanes()[1].getRowStride();
                int uvPixelStride = image.getPlanes()[1].getPixelStride();
                int uvWidth = width / 2;
                int uvHeight = height / 2;
    
                for (int y = 0; y < uvHeight; ++y) {
                    for (int x = 0; x < uvWidth; ++x) {
                        int bufferIndex = (y * uvRowStride) + (x * uvPixelStride);
                        nv21[index++] = vBuffer.get(bufferIndex);
                        nv21[index++] = uBuffer.get(bufferIndex);
                    }
                }
                return new YuvImage(nv21, ImageFormat.NV21, width, height, null);
            }
    
            public static void saveYuvToFile(File file, int wid, int height, YuvImage yuvImage) {
                try {
                    boolean c = file.createNewFile();
                    Log.d(TAG, file + " created: " + c);
                    FileOutputStream fos = new FileOutputStream(file);
                    yuvImage.compressToJpeg(new Rect(0, 0, wid, height), 100, fos);
                    fos.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    

    SimplePreviewXAct.java

    完整的SimplePreviewXAct.java代码如下

        import android.os.Bundle;
        import android.util.Log;
        import android.util.Size;
        import android.view.Surface;
        import android.widget.Toast;
    
        import androidx.annotation.Nullable;
        import androidx.appcompat.app.AppCompatActivity;
        import androidx.camera.core.CameraSelector;
        import androidx.camera.core.ImageAnalysis;
        import androidx.camera.core.Preview;
        import androidx.camera.lifecycle.ProcessCameraProvider;
        import androidx.core.content.ContextCompat;
        import androidx.databinding.DataBindingUtil;
    
        import com.google.common.util.concurrent.ListenableFuture;
        import com.rustfisher.tutorial2020.R;
        import com.rustfisher.tutorial2020.databinding.ActSimplePreivewXBinding;
    
        import java.util.concurrent.ExecutionException;
        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;
    
    
        /**
        * @author an.rustfisher.com
        * @date 2021-12-09 19:53
        */
        public class SimplePreviewXAct extends AppCompatActivity {
            private static final String TAG = "rfDevX";
            private ActSimplePreivewXBinding mBinding;
            private ListenableFuture<ProcessCameraProvider> mCameraProviderFuture;
            private ProcessCameraProvider mCameraProvider;
            private boolean mRunning = false;
    
            private boolean mTakeOneYuv = false; // 获取一帧 实际工程中不要这么做
    
            private final ImageAnalysis mImageAnalysis =
                    new ImageAnalysis.Builder()
                            //.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
                            .setTargetResolution(new Size(720, 1280)) // 图片的建议尺寸
                            .setOutputImageRotationEnabled(true) // 是否旋转分析器中得到的图片
                            .setTargetRotation(Surface.ROTATION_0) // 允许旋转后 得到图片的旋转设置
                            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                            .build();
    
            @Override
            protected void onCreate(@Nullable Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                mBinding = DataBindingUtil.setContentView(this, R.layout.act_simple_preivew_x);
                mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
                mCameraProviderFuture.addListener(() -> {
                    try {
                        mCameraProvider = mCameraProviderFuture.get();
                        Log.d(TAG, "获取到了 cameraProvider");
                        bindPreview(mCameraProvider);
                    } catch (ExecutionException | InterruptedException e) {
                        // 这里不用处理
                    }
                }, ContextCompat.getMainExecutor(this));
                mBinding.start.setOnClickListener(v -> {
                    if (mCameraProvider != null && !mRunning) {
                        bindPreview(mCameraProvider);
                    }
                });
                mBinding.end.setOnClickListener(v -> {
                    mCameraProvider.unbindAll();
                    mRunning = false;
                });
    
                mBinding.takeOneAnalyse.setOnClickListener(v -> {
                    mTakeOneYuv = true;
                    Log.d(TAG, "获取一帧, 输出图片旋转: " + mImageAnalysis.isOutputImageRotationEnabled());
                });
    
                final ExecutorService executorService = Executors.newFixedThreadPool(2);
                mBinding.enableAna.setOnClickListener(v -> {
                    Toast.makeText(getApplicationContext(), "启用分析器", Toast.LENGTH_SHORT).show();
                    mImageAnalysis.setAnalyzer(executorService, imageProxy -> {
                        // 下面处理数据
                        if (mTakeOneYuv) {
                            mTakeOneYuv = false;
                            Log.d(TAG, "旋转角度: " + imageProxy.getImageInfo().getRotationDegrees());
                            ImgHelper.useYuvImgSaveFile(imageProxy,  true); // 存储这一帧为文件
                            runOnUiThread(() -> Toast.makeText(getApplicationContext(), "截取一帧", Toast.LENGTH_SHORT).show());
                        }
                        imageProxy.close(); // 最后要关闭这个
                    });
                });
                mBinding.clrAna.setOnClickListener(v -> {
                    mImageAnalysis.clearAnalyzer();
                    Toast.makeText(getApplicationContext(), "clearAnalyzer", Toast.LENGTH_SHORT).show();
                });
            }
    
            private void bindPreview(ProcessCameraProvider cameraProvider) {
                if (cameraProvider == null) {
                    Toast.makeText(getApplicationContext(), "没获取到相机", Toast.LENGTH_SHORT).show();
                    return;
                }
                Toast.makeText(getApplicationContext(), "相机启动", Toast.LENGTH_SHORT).show();
                Preview preview = new Preview.Builder().build();
    
                CameraSelector cameraSelector = new CameraSelector.Builder()
                        .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                        .build();
    
                preview.setSurfaceProvider(mBinding.previewView.getSurfaceProvider());
    
                cameraProvider.bindToLifecycle(this, cameraSelector, preview, mImageAnalysis);
                mRunning = true;
            }
    
        }
    

    运行结果

    在红米9上运行,截取到的图片(效果示意图)

    正常图片 只有Y平面(仅作为参考)

    取消分析器

    mImageAnalysis.clearAnalyzer();
    

    ImageAnalysis相关

    通过上面的示例,我们掌握了ImageAnalysis简单的用法。

    Executors

    setAnalyzer我们使用的是java.util.concurrent.Executors。上面的例子传入了一个定长的线程池

    处理图片的方法会运行在线程池的线程里。当然这里换其他类型线程池也可以。也可以用主线程ContextCompat.getMainExecutor(getApplicationContext());

    androidx.camera.core.ImageProxy

    封装了android.media.Image

    ImageAnalysis.Builder

    用来创建ImageAnalysis

    默认输出图片格式是OUTPUT_IMAGE_FORMAT_YUV_420_888,本文示例中我们使用的是默认格式。

    setTargetResolution

    示例中setTargetResolution(new Size(720, 1280))。我们用的是竖屏,设置成了宽度小于高度。

    可以把传入的叫做“目标尺寸”。最终图片会找一个最接近的尺寸。具体由摄像头来决定。

    比如把示例里的设置改成setTargetResolution(new Size(1280, 720)),最终输出的图片大小可能是720x720

    setTargetResolutionsetTargetAspectRatio只能二选一

    ImageAnalysis.Builder.setOutputImageRotationEnabled

    setOutputImageRotationEnabled(boolean)是否启用输出图片的旋转功能。注意这是ImageAnalysis.Builder的方法。
    此功能默认关闭

    输出的图片可以用ImageInfo.getRotationDegrees()获得旋转的角度。

    启用后,分析器会旋转每一张图片。相对而言会多耗费性能。

    对于640x480图片来说,中等性能的设备大约会多耗费10-15ms。

    setTargetRotation

    setOutputImageRotationEnabled(true)启用旋转后,可以设置输出图片的旋转角度。

    setTargetRotation(int)接受的参数是Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270

    上面的示例用的是Surface.ROTATION_0

    setBackpressureStrategy

    当图片产生的速度大于图片分析的速度时,分析器会采用的应对策略。Android称之为背压策略

    可选值如下

    STRATEGY_KEEP_ONLY_LATEST (默认)

    使用最新的图片

    STRATEGY_BLOCK_PRODUCER

    阻止产生新的图片。
    当产生的图片超过队列深度时,生产者(producer)会停止生产图片。
    如果上一张图片没有调用ImageProxy.close(),生产出来的图片会去排队(queued),而不是交给分析器。
    如果停止生产图片(image),其他地方也会停止,比如实时预览。

    在上面的示例中,可以试试注释掉imageProxy.close();,修改setBackpressureStrategy(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)

    这个策略配合ImageAnalysis.Builder.setImageQueueDepth(int)使用。设置队列的长度。

    获取nv21数据

    例子中把YUV数据转换成nv21。

    然后利用android.graphics.YuvImage,把图片存下来。

    参考

    一个软件工程师的记录
  • 相关阅读:
    Android中各级目录的作用
    轻量级java开发(一)-Hibernate 安装
    Eclipse 安装插件
    Eclipse超级完美汉化教程
    JAVA中extends 与implements区别
    Java基础语法总结
    C#笔试题面试题锦集(全)总20篇
    Nginx集群
    Redis 集群方案
    MS Sql Server 中主从库的配置和使用介绍
  • 原文地址:https://www.cnblogs.com/rustfisher/p/15700757.html
Copyright © 2020-2023  润新知