Android+OpenCVで特徴点マッチングをやってみた

# はじめに
Android Application(Java)上で画像認識をやってみたいなと思い、OpenCVを使ってて特徴点マッチングをやってみました。

Windows 10 64-bit
Android Studio バージョン:Dolphin 2021.3.1 Patch 1
Android APIレベル:33
OpenCV SDKのバージョン:4.7.0
開発言語:Java

# OpenCVの取り込み
Android StudioへのOpenCVの取り込みはすでに分かりやすくい記載されている方がいるので以下を参照

https://qiita.com/tifa2chan/items/a2ec4f5d90eeb9cf1802

※GradleのSync errorが発生する場合は以下を参照

https://qiita.com/yoshiyuki-N/items/b6dba2c7167681dba7b2

# 特徴点マッチングについて
画像認識の手法の一つ(という理解)です。
画像中に含まれている特徴量を計算して特徴点を抽出します。
異なる画像間で抽出した特徴量を比較し、近しいものあれば類似した特徴点として扱います。
この類似した特徴点が基準(閾値)以上の数検出されれば、画像が似ていると判定することができるというものです。

# 大まかな処理の流れ
AKAZEと呼ばれる特徴点検出アルゴリズムを用いて対象画像から特徴点を抽出します。
画像ごとに抽出した特徴点の特徴量を総当たり的に見ていき類似度の計算をします。
この時点で画像間の特徴点の対応が決まります(マッチング完了)。
ただし類似度が低いマッチングが残っているので閾値を設けて類似度の低いマッチングを除去します。
除去したマッチング結果をもとに、類似度が高いマッチング結果だけを入力画像に重畳表示した画像を作成して表示します。

# 実装
activity_main.xml
```XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/output"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
```

MainActivity.java
```Java
import androidx.appcompat.app.AppCompatActivity;

import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.widget.ImageView;

import org.opencv.android.OpenCVLoader;
import org.opencv.android.Utils;
import org.opencv.core.DMatch;
import org.opencv.core.Mat;
import org.opencv.core.MatOfDMatch;
import org.opencv.core.MatOfKeyPoint;
import org.opencv.features2d.AKAZE;
import org.opencv.features2d.BFMatcher;
import org.opencv.features2d.DescriptorMatcher;
import org.opencv.features2d.Features2d;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    Bitmap bitmap01;
    Bitmap bitmap02;
    Bitmap outputBMP;

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

        ImageView output = findViewById(R.id.output);
        AssetManager assets = getResources().getAssets();

        //opencvの初期化
        OpenCVLoader.initDebug();

        // AKAZEで特徴点検出器を作成
        AKAZE akaze = AKAZE.create();

        // Brute Force Matching(総当たりマッチング)インスタンスの生成
        BFMatcher matcher = new BFMatcher(DescriptorMatcher.BRUTEFORCE);

        //画像のオープン
        try (InputStream image01 = assets.open("image01.jpg")) {
            try (InputStream image02 = assets.open("image02.jpg")) {
                //Bitmapに変換
                bitmap01 = BitmapFactory.decodeStream(image01);
                bitmap02 = BitmapFactory.decodeStream(image02);

                Mat mat01 = new Mat();
                Mat mat02 = new Mat();

                //Matに変換
                Utils.bitmapToMat(bitmap01, mat01);
                Utils.bitmapToMat(bitmap02, mat02);

                //キーポイント、特徴量記述子を作成
                MatOfKeyPoint keypoints1 = new MatOfKeyPoint();
                MatOfKeyPoint keypoints2 = new MatOfKeyPoint();
                Mat descriptors1 = new Mat();
                Mat descriptors2 = new Mat();

                //AKAZEによる特徴点抽出の実行
                akaze.detect(mat01,keypoints1);
                akaze.compute(mat01,keypoints1, descriptors1);
                akaze.detect(mat02,keypoints2);
                akaze.compute(mat02,keypoints2, descriptors2);

                // 特徴点マッチング実行
                List<MatOfDMatch> matchVector = new ArrayList<MatOfDMatch>();
                matcher.knnMatch(descriptors1, descriptors2, matchVector, 2);

                //特徴点マッチング後処理
                //レシオテスト
                float threshold = 0.5f;
                List<MatOfDMatch> good_matches = new ArrayList<MatOfDMatch>();
                for (int i = 0; i < matchVector.size(); i++) {
                    DMatch[] vector = matchVector.get(i).toArray();
                    if (vector[0].distance <  threshold * vector[1].distance) {
                        good_matches.add(matchVector.get(i));
                    } else {
                        MatOfDMatch tmp = new MatOfDMatch();
                        good_matches.add(tmp);
                    }
                }

                //マッチング結果を重畳表示した画像を生成・表示
                Mat matchedImage = new Mat();
                Features2d.drawMatchesKnn(mat01,keypoints1,mat02,keypoints2,good_matches,matchedImage);
                outputBMP = Bitmap.createBitmap(matchedImage.cols(), matchedImage.rows(),Bitmap.Config.ARGB_8888);
                Utils.matToBitmap(matchedImage,outputBMP);
                output.setImageBitmap(outputBMP);

            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
```

# 結果
入力画像1
![image01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3117573/4fd9ac41-9139-4784-a68b-a8d9316d63c4.jpeg) 
入力画像2
![image02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3117573/54319393-bc3f-4c0d-14be-4f6d8371e7ca.jpeg)

出力画像
![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3117573/1373d2ae-4905-cd14-0a97-f8051207577c.png)

大きさが違っても奥側の新幹線付近の特徴点にしっかりマッチングしているのがわかります。

# おわりに
Android(Java)で特徴点マッチングをやってみました。
これで入力画像中に特定画像が含まれているかの判定ができるようになりました。
OpenCVといえばPythonやC++のイメージがありますが、Android(Java)環境でできることでスマホで撮影した画像にその場で画像認識(特徴点マッチング)ができていろんなことに使えるかなと思って試してみました。
特徴点マッチングの大まかな流れについても学べたので、いい勉強になりました。

# 余談
OpenCVはPythonやC++でのサンプルは多いものの、JavaやAndroid環境でのサンプルは少ないので言語による違いを埋めるのに少し苦労しました。
また数少ないJavaのサンプルを見つけても使用しているOprenCVのバージョンが異なるためかそのまま使えないケースもあったりしました。(特徴点の対応付けした画像の表示部分)

# 参考
・OpenCVで特徴量マッチング(AKAZE, KNN) サンプルコード付
　https://miyashinblog.com/opencvakaze-knn/#toc3
・OpenCV – 特徴点マッチングを行う方法について
　https://pystyle.info/opencv-feature-matching/
・JavaCV(OpenCV)で特徴点マッチング
　https://developers-trash.com/archives/77
・JavaでOpenCVつついてSIFTとSURFしてみる
　https://ramencozo.hatenadiary.org/entry/20130626/1372255948