[源码解析] PyTorch分布式优化器 |
849 2023-04-03 03:46:34
CameraX使用ImageAnalysis分析器,可以访问缓冲区中的图像,获取视频帧数据。
准备工作包括gradle,layout,动态申请相机权限,外部存储权限等等,大部分设置与CameraX 打开摄像头预览相同。
一些关键配置
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" }
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>
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
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
代码如下
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上运行,截取到的图片(效果示意图)
mImageAnalysis.clearAnalyzer();
通过上面的示例,我们掌握了ImageAnalysis简单的用法。
setAnalyzer
我们使用的是java.util.concurrent.Executors
。上面的例子传入了一个定长的线程池。
处理图片的方法会运行在线程池的线程里。当然这里换其他类型线程池也可以。也可以用主线程ContextCompat.getMainExecutor(getApplicationContext());
。
封装了android.media.Image
用来创建ImageAnalysis
默认输出图片格式是OUTPUT_IMAGE_FORMAT_YUV_420_888
,本文示例中我们使用的是默认格式。
示例中setTargetResolution(new Size(720, 1280))
。我们用的是竖屏,设置成了宽度小于高度。
可以把传入的叫做“目标尺寸”。最终图片会找一个最接近的尺寸。具体由摄像头来决定。
比如把示例里的设置改成setTargetResolution(new Size(1280, 720))
,最终输出的图片大小可能是720x720
setTargetResolution
和setTargetAspectRatio
只能二选一
setOutputImageRotationEnabled(boolean)
是否启用输出图片的旋转功能。注意这是ImageAnalysis.Builder的方法。
此功能默认关闭
输出的图片可以用ImageInfo.getRotationDegrees()
获得旋转的角度。
启用后,分析器会旋转每一张图片。相对而言会多耗费性能。
对于640x480图片来说,中等性能的设备大约会多耗费10-15ms。
setOutputImageRotationEnabled(true)
启用旋转后,可以设置输出图片的旋转角度。
setTargetRotation(int)
接受的参数是Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270
上面的示例用的是Surface.ROTATION_0
当图片产生的速度大于图片分析的速度时,分析器会采用的应对策略。Android称之为背压策略。
可选值如下
STRATEGY_KEEP_ONLY_LATEST
(默认)使用最新的图片
STRATEGY_BLOCK_PRODUCER
阻止产生新的图片。
当产生的图片超过队列深度时,生产者(producer)会停止生产图片。
如果上一张图片没有调用ImageProxy.close()
,生产出来的图片会去排队(queued),而不是交给分析器。
如果停止生产图片(image),其他地方也会停止,比如实时预览。
在上面的示例中,可以试试注释掉
imageProxy.close();
,修改setBackpressureStrategy(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)
这个策略配合ImageAnalysis.Builder.setImageQueueDepth(int)
使用。设置队列的长度。
例子中把YUV数据转换成nv21。
然后利用android.graphics.YuvImage,把图片存下来。