Android CameraX OpenCV图像处理
在本教程中,我们将把OpenCV集成到我们的Android应用程序中。
我们已经在这里讨论了CameraX的基础知识。
因此,今天,我们将了解如何在实时摄像头供稿上运行OpenCV图像处理。
Android CameraX
Android Jetpack随附的CameraX都是关于用例的。
基本上,可以使用三个核心用例来构建摄像机:预览,分析和捕获。
之前已经完成了预览和捕获,今天我们的主要重点是分析。
因此,我们将使用OpenCV分析帧并进行实时处理。
什么是OpenCV?
OpenCV是一个计算机视觉库,其中包含2000多种与图像处理有关的算法。
它是用C ++编写的。
幸运的是,OpenCV中的许多高级内容都可以用Java完成。
此外,我们总是可以使用JNI接口在Java和C ++之间进行通信。
我们可以从其官方GitHub存储库下载并导入OpenCV SDK。
这是一个很大的模块。
由于大小和项目空间的限制,我们将使用如下所示的Gradle依赖库:
implementation 'com.quickbirdstudios:opencv:3.4.1'
你知道吗?
使用OpenCV模块的Android应用程序具有较大的APK大小。
在以下使用CameraX和OpenCV的部分中,我们将转换色彩空间以使摄像机具有完全不同的外观。
Android OpenCV项目结构
Android Camerax Opencv项目结构
Android OpenCV代码
下面给出了activity_main.xml布局的代码:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android" xmlns:app="https://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <TextureView android:id="@+id/textureView" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="visible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" <ImageView android:id="@+id/ivBitmap" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/btnCapture" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="24dp" app:backgroundTint="@android:color/black" android:src="@drawable/ic_camera" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="24dp" android:orientation="horizontal" android:weightSum="2" android:visibility="gone" android:id="@+id/llBottom" android:gravity="center" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/btnReject" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.5" app:backgroundTint="@color/rejectedRed" android:src="@drawable/ic_close" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/btnAccept" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="0.5" android:layout_marginStart="20dp" android:src="@drawable/ic_check" app:backgroundTint="@color/acceptedGreen" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
下面给出了MainActivity.java的代码:
package com.theitroad.androidcameraxopencv; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.camera.core.CameraX; import androidx.camera.core.ImageAnalysis; import androidx.camera.core.ImageAnalysisConfig; import androidx.camera.core.ImageCapture; import androidx.camera.core.ImageCaptureConfig; import androidx.camera.core.ImageProxy; import androidx.camera.core.Preview; import androidx.camera.core.PreviewConfig; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Matrix; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; import android.util.Rational; import android.util.Size; import android.view.Menu; import android.view.MenuItem; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; import org.opencv.android.OpenCVLoader; import org.opencv.android.Utils; import org.opencv.core.Mat; import org.opencv.imgproc.Imgproc; import java.io.File; public class MainActivity extends AppCompatActivity implements View.OnClickListener { private int REQUEST_CODE_PERMISSIONS = 101; private final String[] REQUIRED_PERMISSIONS = new String[]{"android.permission.CAMERA", "android.permission.WRITE_EXTERNAL_STORAGE"}; TextureView textureView; ImageView ivBitmap; LinearLayout llBottom; int currentImageType = Imgproc.COLOR_RGB2GRAY; ImageCapture imageCapture; ImageAnalysis imageAnalysis; Preview preview; FloatingActionButton btnCapture, btnOk, btnCancel; static { if (!OpenCVLoader.initDebug()) Log.d("ERROR", "Unable to load OpenCV"); else Log.d("SUCCESS", "OpenCV loaded"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnCapture = findViewById(R.id.btnCapture); btnOk = findViewById(R.id.btnAccept); btnCancel = findViewById(R.id.btnReject); btnOk.setOnClickListener(this); btnCancel.setOnClickListener(this); llBottom = findViewById(R.id.llBottom); textureView = findViewById(R.id.textureView); ivBitmap = findViewById(R.id.ivBitmap); if (allPermissionsGranted()) { startCamera(); } else { ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS); } } private void startCamera() { CameraX.unbindAll(); preview = setPreview(); imageCapture = setImageCapture(); imageAnalysis = setImageAnalysis(); //bind to lifecycle: CameraX.bindToLifecycle(this, preview, imageCapture, imageAnalysis); } private Preview setPreview() { Rational aspectRatio = new Rational(textureView.getWidth(), textureView.getHeight()); Size screen = new Size(textureView.getWidth(), textureView.getHeight()); //size of the screen PreviewConfig pConfig = new PreviewConfig.Builder().setTargetAspectRatio(aspectRatio).setTargetResolution(screen).build(); Preview preview = new Preview(pConfig); preview.setOnPreviewOutputUpdateListener( new Preview.OnPreviewOutputUpdateListener() { @Override public void onUpdated(Preview.PreviewOutput output) { ViewGroup parent = (ViewGroup) textureView.getParent(); parent.removeView(textureView); parent.addView(textureView, 0); textureView.setSurfaceTexture(output.getSurfaceTexture()); updateTransform(); } }); return preview; } private ImageCapture setImageCapture() { ImageCaptureConfig imageCaptureConfig = new ImageCaptureConfig.Builder().setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY) .setTargetRotation(getWindowManager().getDefaultDisplay().getRotation()).build(); final ImageCapture imgCapture = new ImageCapture(imageCaptureConfig); btnCapture.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { imgCapture.takePicture(new ImageCapture.OnImageCapturedListener() { @Override public void onCaptureSuccess(ImageProxy image, int rotationDegrees) { Bitmap bitmap = textureView.getBitmap(); showAcceptedRejectedButton(true); ivBitmap.setImageBitmap(bitmap); } @Override public void onError(ImageCapture.UseCaseError useCaseError, String message, @Nullable Throwable cause) { super.onError(useCaseError, message, cause); } }); /*File file = new File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "" + System.currentTimeMillis() + "_JDCameraX.jpg"); imgCapture.takePicture(file, new ImageCapture.OnImageSavedListener() { @Override public void onImageSaved(@NonNull File file) { Bitmap bitmap = textureView.getBitmap(); showAcceptedRejectedButton(true); ivBitmap.setImageBitmap(bitmap); } @Override public void onError(@NonNull ImageCapture.UseCaseError useCaseError, @NonNull String message, @Nullable Throwable cause) { } });*/ } }); return imgCapture; } private ImageAnalysis setImageAnalysis() { //Setup image analysis pipeline that computes average pixel luminance HandlerThread analyzerThread = new HandlerThread("OpenCVAnalysis"); analyzerThread.start(); ImageAnalysisConfig imageAnalysisConfig = new ImageAnalysisConfig.Builder() .setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE) .setCallbackHandler(new Handler(analyzerThread.getLooper())) .setImageQueueDepth(1).build(); ImageAnalysis imageAnalysis = new ImageAnalysis(imageAnalysisConfig); imageAnalysis.setAnalyzer( new ImageAnalysis.Analyzer() { @Override public void analyze(ImageProxy image, int rotationDegrees) { //Analyzing live camera feed begins. final Bitmap bitmap = textureView.getBitmap(); if(bitmap==null) return; Mat mat = new Mat(); Utils.bitmapToMat(bitmap, mat); Imgproc.cvtColor(mat, mat, currentImageType); Utils.matToBitmap(mat, bitmap); runOnUiThread(new Runnable() { @Override public void run() { ivBitmap.setImageBitmap(bitmap); } }); } }); return imageAnalysis; } private void showAcceptedRejectedButton(boolean acceptedRejected) { if (acceptedRejected) { CameraX.unbind(preview, imageAnalysis); llBottom.setVisibility(View.VISIBLE); btnCapture.hide(); textureView.setVisibility(View.GONE); } else { btnCapture.show(); llBottom.setVisibility(View.GONE); textureView.setVisibility(View.VISIBLE); textureView.post(new Runnable() { @Override public void run() { startCamera(); } }); } } private void updateTransform() { Matrix mx = new Matrix(); float w = textureView.getMeasuredWidth(); float h = textureView.getMeasuredHeight(); float cX = w/2f; float cY = h/2f; int rotationDgr; int rotation = (int) textureView.getRotation(); switch (rotation) { case Surface.ROTATION_0: rotationDgr = 0; break; case Surface.ROTATION_90: rotationDgr = 90; break; case Surface.ROTATION_180: rotationDgr = 180; break; case Surface.ROTATION_270: rotationDgr = 270; break; default: return; } mx.postRotate((float) rotationDgr, cX, cY); textureView.setTransform(mx); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQUEST_CODE_PERMISSIONS) { if (allPermissionsGranted()) { startCamera(); } else { Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show(); finish(); } } } private boolean allPermissionsGranted() { for (String permission: REQUIRED_PERMISSIONS) { if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { return false; } } return true; } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.black_white: currentImageType = Imgproc.COLOR_RGB2GRAY; startCamera(); return true; case R.id.hsv: currentImageType = Imgproc.COLOR_RGB2HSV; startCamera(); return true; case R.id.lab: currentImageType = Imgproc.COLOR_RGB2Lab; startCamera(); return true; } return super.onOptionsItemSelected(item); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btnReject: showAcceptedRejectedButton(false); break; case R.id.btnAccept: File file = new File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "" + System.currentTimeMillis() + "_JDCameraX.jpg"); imageCapture.takePicture(file, new ImageCapture.OnImageSavedListener() { @Override public void onImageSaved(@NonNull File file) { showAcceptedRejectedButton(false); Toast.makeText(getApplicationContext(), "Image saved successfully in Pictures Folder", Toast.LENGTH_LONG).show(); } @Override public void onError(@NonNull ImageCapture.UseCaseError useCaseError, @NonNull String message, @Nullable Throwable cause) { } }); break; } } }
在上面的代码中,在"图像分析"用例内部,我们从TextureView中检索位图。
Utils.bitmapToMat用于将位图转换为Mat对象。
此方法是OpenCV android的一部分。
Mat类基本上用于保存图像。
它由矩阵标题和指向包含像素值的矩阵的指针组成。
在图像分析中,我们使用" ImgProc.cvtColor"将垫子颜色空间从一种类型转换为另一种类型。
将垫子转换为其他颜色空间后,我们再将其转换为位图,并在ImageView中显示在屏幕上。
默认情况下,图像为RGB类型。
使用菜单选项,我们可以将图像转换为GREY,LAB,HSV类型。