Android CameraX ImageAnalysis 获取视频帧

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 onCreatefinal 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() + ".webp");            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() + ".webp");                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,把图片存下来。

参考#

  • camerax analyze - android
  • ImageFormat#YUV_420_888 - android
  • 关于YUV视频 - microsoft
  • How to use YUV (YUV_420_888) Image in Android - minhazav.dev
免责声明:本网信息来自于互联网,目的在于传递更多信息,并不代表本网赞同其观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,并请自行核实相关内容。本站不承担此类作品侵权行为的直接责任及连带责任。如若本网有任何内容侵犯您的权益,请及时联系我们,本站将会在24小时内处理完毕。
相关文章
返回顶部