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
setTargetResolution和setTargetAspectRatio只能二选一
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
Android CameraX ImageAnalysis 获取视频帧的更多相关文章
- [转]android 获取视频帧
本文转自:http://blog.csdn.net/heart_Moving/article/details/17414067 今天做Android视频文件解码,需求:从一个视频文件获取到一帧一帧的图 ...
- 利用ffmpeg获取视频帧
如果要对视频帧进行处理,可以先把视频帧读取出来. sh文件代码如下: #!/usr/bin/env sh VIDEO=/home/xxx/video/ FRAMES=/home/xxx/frame/ ...
- 在Android中如何获取视频的第一帧图片并显示在一个ImageView中
String path = Environment.getExternalStorageDirectory().getPath(); MediaMetadataRetriever media = n ...
- FFmpeg进行视频帧提取&音频重采样-Process.waitFor()引发的阻塞超时
由于产品需要对视频做一系列的解析操作,利用FFmpeg命令来完成视频的音频提取.第一帧提取作为封面图片.音频重采样.字幕压缩等功能: 前一篇文章已经记录了FFmpeg在JAVA中的使用-音频提取&am ...
- Android -- 获取视频第一帧缩略图
干货 从API 8开始,新增了一个类: android.media.ThumbnailUtils这个类提供了3个静态方法一个用来获取视频第一帧得到的Bitmap,2个对图片进行缩略处理. public ...
- Android之使用MediaMetadataRetriever类获取视频第一帧
一.首先,来介绍一下MediaMetadataRetriever类,此类位于android.media包下,这里,先附上可查看此类的API地址:MediaMetadataRetriever类.大家能够 ...
- 关于video标签移动端开发遇到的问题,获取视频第一帧,全屏,自动播放,自适应等问题
最近一直在处理video标签在IOS和Android端的兼容问题,其中遇到不少坑,绝大多数问题已经解决,下面是处理问题经验的总结: 1.获取视频的第一帧作为背景图: 技术:canvas绘图 windo ...
- Android开发 获取视频中的信息(例如预览图或视频时长) MediaMetadataRetriever媒体元数据检索器
前言 在Android里获取视频的信息主要依靠MediaMetadataRetriever实现 获取最佳视频预览图 所谓的最佳就是MediaMetadataRetriever自己计算的 /** * 获 ...
- android 中获取视频文件的缩略图(非原创)
在android中获取视频文件的缩略图有三种方法: 1.从媒体库中查询 2. android 2.2以后使用ThumbnailUtils类获取 3.调用jni文件,实现MediaMetadataRet ...
随机推荐
- 第09章 MySQL子查询
第09章 MySQL子查询 子查询指一个查询语句嵌套在另一个查询语句内部的查询,这个特性从MySQL 4.1开始引入. SQL 中子查询的使用大大增强了 SELECT 查询的能力,因为很多时候查询需要 ...
- 撸了一个可调试 gRPC 的 GUI 客户端
前言 平时大家写完 gRPC 接口后是如何测试的?往往有以下几个方法: 写单测代码,自己模拟客户端测试. 可以搭一个 gRPC-Gateway 服务,这样就可以在 postman 中进行模拟. 但这两 ...
- 论文翻译:2020_A Recursive Network with Dynamic Attention for Monaural Speech Enhancement
论文地址:基于动态注意的递归网络单耳语音增强 论文代码:https://github.com/Andong-Li-speech/DARCN 引用格式:Li, A., Zheng, C., Fan, C ...
- 使用 SpringBoot 构建一个RESTful API
目录 背景 创建 SpringBoot 项目/模块 SpringBoot pom.xml api pom.xml 创建 RESTful API 应用 @SpringBootApplication @C ...
- Codeforces 1553I - Stairs(分治 NTT+容斥)
Codeforces 题面传送门 & 洛谷题面传送门 u1s1 感觉这道题放到 D1+D2 里作为 5250 分的 I 有点偏简单了吧 首先一件非常显然的事情是,如果我们已知了排列对应的阶梯序 ...
- Run For Beer CF575G
Run for beer CF 575G 如果直接bfs分层贪心可以做,但是很毒瘤,具体可以参考Gavinzheng的提交 考虑魔改dijkstra 首先,每次拿权值最小的来松弛肯定没有问题,只是怎么 ...
- 解决 VS Code 无法使用Ctrl+C等快捷键
背景 VScode 安装 Vim扩展后,无法使用Ctrl+C,Ctrl+X和 Ctrl+V等热键 解决方案 方案一 停用Vim 热键覆盖 # 原因: vim 扩展默认启用Vim ctrl键覆盖常见的V ...
- Perl调用和管理外部文件中的变量(如软件和数据库配置文件)
编写流程时,有一个好的习惯是将流程需要调用的软件.数据库等信息与脚本进行分离,这样可以统一管理流程的软件和数据库等信息,当它们路径改变或者升级的时候管理起来就很方便,而不需要去脚本中一个个寻找再修改. ...
- Docker Nginx-Proxy 容器Nginx Proxy反向代理
Docker Nginx-Proxy 容器Nginx Proxy反向代理 简单介绍 Docker容器的自动Nginx反向代理 dockerhub地址 https://hub.docker.co ...
- 【Python小试】计算目录下所有DNA序列的Kmer并过滤
背景 Kmer是基因组组装算法中经常接触到的概念,简单来说,Kmer就是长度为k的核苷酸序列.一般长短为m的reads可以分成m-k+1个Kmer.Kmer的长度和阈值直接影响到组装的效果. Deno ...

