OpenCV+AndroidStudioで顔認識アプリ

##やりたいこと
OpenCVを使った顔認識アプリの作成
・目標
　・とりあえずAndroidStudioの導入、使い方を学ぶ(初心者)
　・OpenCVをAndroid上で動かす
　・カメラで撮影した画像、ギャラリーに保存されている画像のどちらも使用可能に

## 参考
OpenCVを使って、簡単顔認識Androidアプリを作ってみた
https://qiita.com/Kuroakira/items/094ecb236da89949d702
インテントでカメラを呼び出す方法の補足（主に、Xperia 2.1問題対応） 
https://gabu.hatenablog.com/entry/20101125/1290681748

## 使ったもの
Android Studio 3.5.3
OpenCV 4.2.0

## 大まかな流れ
実装についてはほぼ上記の先人たちを参考にしているのでざっと書きます。

カメラ、ギャラリーボタンを押下
↓
カメラ、ギャラリーから画像を取得
↓
取得した画像をbitmap→Mat形式に変換
↓
OpenCVへMat形式の画像を渡す
↓
顔認識情報を取得し、画面へ表示


## 結果
それっぽい座標情報が出てきました。

![result1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/585211/1d770dae-10fe-70e5-57cc-030965c7255b.png)

## 今後も使えそうなこと
・カメラ
　カメラで撮影した画像を高解像度で取得したい場合、いったんURLへ書き出し、ＵＲＬから画像を取得する。
　getImg = (Bitmap) data.getExtras().get("data");ではサムネイル用の低解像度画像しか取得できない。
　(画像認識用の画像はgetImg = (Bitmap) data.getExtras().get("data");で取得しているが、人数が増えたり、引きの画だと不都合がでそう)

```java:MainActivity.java
　　　getImg = (Bitmap) data.getExtras().get("data");
```


```java:MainActivity.java
    /**略**/
    imageView.setImageURI(mImageUri);
    /**略**/

    /**
     * 写真撮影用
     * 撮影した写真をいったんURLに書き出して、書き出し先のURLをmImageUriに格納
     */
    protected void takePhoto(){
        String filename = System.currentTimeMillis() + ".jpg";

        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.TITLE,filename);
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
        mImageUri = getContentResolver().insert(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values
        );

        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, mImageUri);
        startActivityForResult(intent, RESULT_CAMERA);
    }
```

## 今後、実現したいこと
・顔認識部分だけクラス分割
　当初、クラス分割しての実装を試みたが、以下の部分がクラス分割した場合の方法がわからず断念。
　(別クラスからでは、MainActivity.javaと同様の記述ではのres/rawへアクセスができない？)

```java:MainActivity.java
        File cascadeDir = getDir("cscade", Context.MODE_PRIVATE);
        File cascadeFile = new File(cascadeDir, "lbpcascade_frontalface.xml");
```

・ImageViewから画像取得
　ImageVIewから画像取得することでカメラ、ギャラリーの場合、それぞれの場合での実装が不要になるためコードを削減できるのではないか

・Kotlinでの実装
　とりあえず慣れてみるためだったので書きなれたJavaで実装を行ったが今後Android向け開発を行う場合はKotlinを使用したい


## ソースコード
```java:MainActivity.java
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import  java.io.InputStream;

import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import org.opencv.android.Utils;
import org.opencv.core.Mat;
import org.opencv.core.MatOfRect;
import org.opencv.core.Rect;
import org.opencv.objdetect.CascadeClassifier;

public class MainActivity extends AppCompatActivity {
    private final static int RESULT_CAMERA = 1001; // カメラ用
    private final static int REQUEST_GALLERY = 1000; //　ギャラリー用
    private Uri mImageUri; // 画像のURLを格納するインスタンス変数
    private Bitmap getImg = null; // カメラ、またはギャラリーから取得する画像
    private ImageView imageView;//イメージビューの宣言文

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        imageView = findViewById(R.id.image_view);//先にImageViewをレイアウトビューのIDと紐づけ

        Button cameraButton = findViewById(R.id.camera_button);
        cameraButton.setOnClickListener(new View.OnClickListener() {//普通のインナークラスを使っての実装
            @Override
            public void onClick(View v) {
                takePhoto();
            }
        });

        Button galleyButton = findViewById(R.id.gallery_button);
        galleyButton.setOnClickListener(new View.OnClickListener() {//普通のインナークラスを使っての実装
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setType("image/*");
                intent.setAction(Intent.ACTION_GET_CONTENT);
                startActivityForResult(intent, REQUEST_GALLERY);
            }
        });
    }

    //これからImageViewにとった写真を張り付け。
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(requestCode == RESULT_CAMERA || requestCode == REQUEST_GALLERY) {
            TextView textView = findViewById(R.id.text1); // テキストビュー(表示するテキストを格納するビュー)
            String outputText = ""; // 表示するテキスト
            MatOfRect faceRects; // 顔認識データを格納する

            // カメラからの画像選択の場合
            if (requestCode == RESULT_CAMERA) {
                // cancelしたケースも含む
                if (data.getExtras() == null) {
                    Log.d("debug", "cancel ?");
                    return;
                } else {
                    imageView.setImageURI(mImageUri);
                    getImg = (Bitmap) data.getExtras().get("data");
                }
            }
            // ギャラリーからの画像選択の場合
            if (requestCode == REQUEST_GALLERY && resultCode == RESULT_OK) {
                try {
                    InputStream in = getContentResolver().openInputStream(data.getData());
                    getImg = BitmapFactory.decodeStream(in);
                    in.close();
                    // 選択した画像を表示する
                    imageView.setImageBitmap(getImg);
                } catch (Exception e) {
                    //　ファイルが無かった時
                    textView.setText("ファイルが見つかりません");
                }
            }
            //　imageViewに張り付けられた画像(カメラ、ギャラリーから取得)から顔認識
            try {
                faceRects = checkFaceExistence(getImg);
                outputText = makeOutputText(faceRects);\
                // 選択した画像を表示する
                imageView.setImageBitmap(getImg);
            } catch (IOException e) {
                outputText = "顔認識エラー";
                e.printStackTrace();
            }

            Toast.makeText(this,outputText,Toast.LENGTH_LONG).show();
            textView.setText(outputText);
        }
    }

    /**
     * 写真撮影用
     * 撮影した写真をいったんURLに書き出して、書き出し先のURLをmImageUriに格納
     */
    protected void takePhoto(){
        String filename = System.currentTimeMillis() + ".jpg";

        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.TITLE,filename);
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
        mImageUri = getContentResolver().insert(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values
        );

        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, mImageUri);
        startActivityForResult(intent, RESULT_CAMERA);
    }

    /**
     * 渡された画像を元に顔認識を行う
     * @param bitmap 顔認識対象の画像
     * @return text 顔認識情報
     * @throws IOException
     */
    protected MatOfRect checkFaceExistence(Bitmap bitmap) throws IOException{
        System.loadLibrary("opencv_java4");

        // 送られた画像データ(bitmap)をMat形式に変換
        Mat matImg = new Mat();
        Utils.bitmapToMat(bitmap, matImg);
        String text =  "";   // 顔認識情報を格納するtext

        // 顔認識を行うカスケード分類器インスタンスの生成(一度ファイルを書き出してファイルパスを取得)
        // 一度raw配下に格納されたxmlファイルを取得
        InputStream inStream = getResources().openRawResource(R.raw.haarcascade_frontalface_alt);
        MatOfRect faceRects = new MatOfRect(); // 顔認識データを格納する

        try {
            // 出力したxmlファイルのパスをCascadeClassfleの引数に
            CascadeClassifier faceDetetcor = outputCascadeFile(inStream);
            // カスケード分類器に画像データを与えて、顔認識
            faceDetetcor.detectMultiScale(matImg, faceRects);
        }
        catch (IOException e){
            Toast.makeText(this,"解析情報ファイルオープンに失敗しました。",Toast.LENGTH_SHORT).show();
        }

        return faceRects;
    }

    /**
     * あらかじめ用意されたopenCV分類器を一度取り込んで、書き出し使用可能にする。
     * @param inStream 分類器の元データ
     * @return faceDetector 分類器データ
     * @throws IOException
     */
    protected CascadeClassifier outputCascadeFile(InputStream inStream) throws IOException {
        File cascadeDir = getDir("cscade", Context.MODE_PRIVATE);
        File cascadeFile = new File(cascadeDir, "lbpcascade_frontalface.xml");

        // 取得したxmlファイルを特定ディレクトリに出力
        FileOutputStream outputStream = new FileOutputStream(cascadeFile);

        byte[] buf = new byte[2048];
        int rdBytes;

        while ((rdBytes = inStream.read(buf)) != -1) {
            try {
                outputStream.write(buf, 0, rdBytes);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // 出力したxmlファイルのパスをCascadeClassfleの引数に
        CascadeClassifier faceDetetcor = new CascadeClassifier((cascadeFile.getAbsolutePath()));

        outputStream.close();

        return faceDetetcor;
    }

    /**
     * 出力用テキスト作成
     * 顔認識情報を元に座標情報をtextにする
     * @param faceRects
     * @return
     */
    protected String makeOutputText(MatOfRect faceRects){
        String text = "";

        // 顔認識結果をStringでreturn
        if(faceRects.toArray().length > 0){
            for(Rect face: faceRects.toArray()) {
                text = "顔の縦幅:" + face.height + "\n" +
                        "顔の横幅" + face.width + "\n" +
                        "顔の位置(Y座標)" + face.y + "\n" +
                        "顔の位置(X座標)" + face.x;
            }
        }
        else{
            text = "顔が検出されませんでした。";
        }

        return text;
    }
```