Android CameraX OpenCV图像处理

时间:2020-02-23 14:28:48  来源:igfitidea点击:

在本教程中,我们将把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类型。