java - Android Camera X ImageAnalysis 图像平面缓冲区大小(限制)与图像大小不匹配
问题描述
问题
这是关于 camera-x 的 ImageAnalysis 用例的一般性问题,但我将使用此 codelab 的略微修改版本作为示例来说明我看到的问题。我发现图像尺寸 (image.height * image.width) 和相关的 ByteBuffer 大小不匹配,由其限制和/或容量来衡量。我希望它们是相同的,并将图像的一个像素映射到 ByteBuffer 中的单个值。情况似乎并非如此。希望有人能澄清这是否是一个错误,如果不是,如何解释这种不匹配。
细节
在 codelab 的第 6 步(图像分析)中,他们为其亮度分析器提供了一个子类:
package jp.oist.cameraxcodelab
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import android.util.Size
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.util.concurrent.Executors
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
typealias LumaListener = (luma: Double) -> Unit
class MainActivity : AppCompatActivity() {
private var imageCapture: ImageCapture? = null
private lateinit var outputDirectory: File
private lateinit var cameraExecutor: ExecutorService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Request camera permissions
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
// Set up the listener for take photo button
camera_capture_button.setOnClickListener { takePhoto() }
outputDirectory = getOutputDirectory()
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun takePhoto() {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return
// Create time-stamped output file to hold the image
val photoFile = File(
outputDirectory,
SimpleDateFormat(FILENAME_FORMAT, Locale.US
).format(System.currentTimeMillis()) + ".jpg")
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
// Set up image capture listener, which is triggered after photo has
// been taken
imageCapture.takePicture(
outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
val msg = "Photo capture succeeded: $savedUri"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
})
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.createSurfaceProvider())
}
imageCapture = ImageCapture.Builder()
.build()
val imageAnalyzer = ImageAnalysis.Builder()
.setTargetResolution(Size(480, 640)) // I added this line
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
// Log.d(TAG, "Average luminosity: $luma")
})
}
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
private fun getOutputDirectory(): File {
val mediaDir = externalMediaDirs.firstOrNull()?.let {
File(it, resources.getString(R.string.app_name)).apply { mkdirs() } }
return if (mediaDir != null && mediaDir.exists())
mediaDir else filesDir
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
companion object {
private const val TAG = "CameraXBasic"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray) {
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
}
}
private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {
private fun ByteBuffer.toByteArray(): ByteArray {
rewind() // Rewind the buffer to zero
val data = ByteArray(remaining())
get(data) // Copy the buffer into a byte array
return data // Return the byte array
}
override fun analyze(image: ImageProxy) {
val buffer = image.planes[0].buffer
for ((index,plane) in image.planes.withIndex()){
Log.i("analyzer", "Plane: $index" + " H: " + image.height + " W: " +
image.width + " HxW: " + image.height * image.width + " buffer.limit: " +
buffer.limit() + " buffer.cap: " + buffer.capacity() + buffer.get())
}
val data = buffer.toByteArray()
val pixels = data.map { it.toInt() and 0xFF }
val luma = pixels.average()
listener(luma)
image.close()
}
}
}
我正在尝试从外部从平面传递数组数据,所以我data
对LuminosityAnalyzer.analyze()
.
期待
我想检查数据数组的维度,因此在 val 缓冲区的 init 之后添加了您看到的日志语句。对于默认分辨率,我在日志中得到了这个:
平面:0 H:480 W:640 HxW:307200 buffer.limit:307200 buffer.cap:3072005
平面:1 H:480 W:640 HxW:307200 buffer.limit:307200 buffer.cap:3072003
平面:2 H:480 W:640 HxW:307200 buffer.limit:307200 buffer.cap:3072003
这是我所期望的。H*W = buffer.limit。缓冲区表示特定平面中图像的像素。
意外的结果
如果我通过使用 setTargetResolution() 方法设置 imageAnalyzer 来更改分辨率,我会得到奇怪的结果。例如,如果我将其设置为setTargetResolution(144, 176)
我得到以下日志:
平面:0 H:144 W:176 HxW:25344 buffer.limit:27632 buffer.cap:276324
平面:1 H:144 W:176 HxW:25344 buffer.limit:27632 buffer.cap:276324
平面:2 H:144 W:176 HxW:25344 buffer.limit:27632 buffer.cap:276322
请注意图像的大小与缓冲区限制和容量的不同。
平面 0 的其他一些示例(为简洁起见):
平面:0 H:288 W:352 HxW:101376 buffer.limit:110560 buffer.cap:1105604
平面:0 H:600 W:800 HxW:480000 buffer.limit:499168 buffer.cap:4991685
平面:0 H:960 W:1280 HxW:1228800 buffer.limit:1228800 buffer.cap:12288004
这是否与传感器尺寸与标准图像尺寸不匹配有关?我应该期望缓冲区中的剩余条目为零还是无意义?
我最初不是在 Kotlin 中运行它,而是在 Java 中运行它,并且在那里得到了更奇怪的结果。如果您记录三个平面中每个平面的图像大小和缓冲区限制,您会得到比不同层的图像大小更大和更小的限制:
平面:0 宽度:176 高度:144 WxH:25344 缓冲区。限制:27632
平面:1 宽度:176 高度:144 WxH:25344 buffer.limit:13807
平面:2 宽度:176 高度:144 WxH:25344 buffer.limit:13807
无论出于何种原因,在 Kotlin 中,平面之间的限制保持不变。
我该如何解释这个?图像是否在平面 0 中填充并在平面 1 和 2 中裁剪?或者这是一个错误?
作为参考,清单、布局文件和 build.gradle 文件复制如下(应该与 CodeLab 相同) 最后,我还包含了 Java 版本的 MainActivity,它会导致平面之间的缓冲区限制不匹配。如果你能告诉我为什么 ByteBuffer.array() 挂起以及为什么我必须使用 ByteBuffer.get() 来代替:
安卓清单:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.oist.cameraxcodelab">
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CameraXCodeLab">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/camera_capture_button"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginBottom="50dp"
android:scaleType="fitCenter"
android:text="Take Photo"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:elevation="2dp" />
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
build.gradle(:app)
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "jp.oist.cameraxcodelab"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
def camerax_version = "1.0.0-beta07"
// CameraX core library using camera2 implementation
implementation "androidx.camera:camera-camera2:$camerax_version"
// CameraX Lifecycle Library
implementation "androidx.camera:camera-lifecycle:$camerax_version"
// CameraX View class
implementation "androidx.camera:camera-view:1.0.0-alpha14"
}
Java 版本的 MainActivity
package jp.oist.abcvlib.camera;
import android.Manifest;
import android.content.pm.PackageManager;
import android.media.Image;
import android.os.Bundle;
import android.util.Log;
import android.util.Size;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import com.google.common.util.concurrent.ListenableFuture;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
public class MainActivity extends AppCompatActivity implements LifecycleOwner {
private static final int REQUEST_CODE_PERMISSIONS = 10;
private static final String[] REQUIRED_PERMISSIONS = { Manifest.permission.CAMERA };
private ListenableFuture<ProcessCameraProvider> mCameraProviderFuture;
private PreviewView mPreviewView;
private ExecutorService analysisExecutor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPreviewView = findViewById(R.id.preview_view);
// Request camera permissions
if (allPermissionsGranted()) {
startCamera();
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
}
int threadPoolSize = 8;
analysisExecutor = new ScheduledThreadPoolExecutor(threadPoolSize);
}
private void bindAll(@NonNull ProcessCameraProvider cameraProvider) {
Preview preview = new Preview.Builder().build();
CameraSelector cameraSelector = new CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_FRONT)
.build();
ImageAnalysis imageAnalysis =
new ImageAnalysis.Builder()
.setTargetResolution(new Size(10, 10))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
imageAnalysis.setAnalyzer(analysisExecutor, new ImageAnalysis.Analyzer() {
@Override
@androidx.camera.core.ExperimentalGetImage
public void analyze(@NonNull ImageProxy imageProxy) {
Image image = imageProxy.getImage();
if (image != null) {
int width = image.getWidth();
int height = image.getHeight();
byte[] frame = new byte[width * height];
Image.Plane[] planes = image.getPlanes();
int idx = 0;
for (Image.Plane plane : planes){
ByteBuffer frameBuffer = plane.getBuffer();
int n = frameBuffer.capacity();
Log.i("analyzer", "Plane: " + idx + " width: " + width + " height: " + height + " WxH: " + width*height + " buffer.limit: " + n);
frameBuffer.rewind();
frame = new byte[n];
frameBuffer.get(frame);
idx++;
}
}
imageProxy.close();
}
});
Camera camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis);
preview.setSurfaceProvider(mPreviewView.getSurfaceProvider());
}
private void startCamera() {
mPreviewView.post(() -> {
mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
mCameraProviderFuture.addListener(() -> {
try {
ProcessCameraProvider cameraProvider = mCameraProviderFuture.get();
bindAll(cameraProvider);
} catch (ExecutionException | InterruptedException e) {
// No errors need to be handled for this Future.
// This should never be reached.
}
}, ContextCompat.getMainExecutor(this));
});
}
/**
* Process result from permission request dialog box, has the request
* been granted? If yes, start Camera. Otherwise display a toast
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
// super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera();
} else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show();
finish();
}
}
}
/**
* Check if all permission specified in the manifest have been granted
*/
private boolean allPermissionsGranted() {
for (String permission : REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(getBaseContext(), permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
}
解决方案
请查看ImageProxy.PlaneProxy类的详细信息;他们的飞机不仅仅是打包的图像数据。它们可能同时具有行和像素跨度。
Row stride是相邻两行图像数据之间的填充。 像素步幅是两个相邻像素之间的填充。
此外,YUV_420_888 图像中的平面 1 和 2 的像素数是平面 0 的一半;您获得相同大小的原因可能是因为像素步幅为 2。
对于某些分辨率,步幅可能等于宽度(通常处理硬件有一些约束,例如行步幅必须是 16 或 32 字节的倍数),但可能并非全部。
推荐阅读
- javascript - 如何使用 jmeter 处理 AES 256 加密?
- mongodb - 可以使用 mongodb 查询更新文档,但在 mongoose 中执行时不起作用
- java - 如何在 java 或 spring 中读取 /src 之外的文件内容?
- reactjs - 如何将它从 React Js 中的组件重定向到同一个组件
- javascript - 创建数组的副本,但仅包含特定字段
- angular - 如何在故事书中设置 Angular 组件的 ng-content?
- java - 如何使用 java KMS API 设置密钥环的保护级别?
- elasticsearch - 如何找出在elasticsearch 6.4中加入或离开的节点
- javascript - chartjs将轴的原点与图形的角对齐
- html - 如何在html中设置自定义tabindex