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,把圖片存下來。