HMS ML Kitの手のひら検出機能とコサイン類似度を利用し、じゃんけん判別アプリを作ってみた

# 手のひら検出機能について
手のひら検出機能はHMS ML Kitが提供している一つの機械学習機能です。手のひら検出機能を使えば、手の以下の特徴点が検出できます。

* 手首
* 親指の一つ目の関節
* 親指の二つ目の関節
* 親指の三つ目の関節
* 親指の指先
* 人差し指の一つ目の関節
* 人差し指の二つ目の関節
* 人差し指の三つ目の関節
* 人差し指の指先
* 中指の一つ目の関節
* 中指の二つ目の関節
* 中指の三つ目の関節
* 中指の指先
* 薬指の一つ目の関節
* 薬指の二つ目の関節
* 薬指の三つ目の関節
* 薬指の指先
* 小指の一つ目の関節
* 小指の二つ目の関節
* 小指の三つ目の関節
* 小指の指先

たとえば、このような写真があるとします。

<img width="600" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704548/58c33e83-c727-6113-1d15-cc6e0a8bb8ff.jpeg">

手のひら検出機能を使えば、このように手の特徴点を抽出できます。

<img width="600" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704548/c3a213fa-713c-7abe-b615-ffcb226cd2e4.jpeg">

# じゃんけん判別アプリの作成
手の特徴点だけ抽出しても面白くないので、じゃんけん判別アプリを作ることにしました。
基本的な流れは次になります。

1. カメラで手を撮影します。
2. 撮影した画像を手のひら検出機能にかけて、手の特徴点を取得します。<b>（HMS ML Kitを使う）</b>
3. 特徴点から比較用のデータを作ります。
4. データをじゃんけん辞書（グー・チョキ・パーのデータ）と比較します。<b>（コサイン類似度）</b>
5. 特徴点と判定結果を描画します。

# 実装について
AppGallery Connectの作業、カメラの実装、プレビューサイズの調整、オーバーレイの実装は、“[HMS ML Kitの顔検出機能の実装入門とFirebase ML Kitの顔検出機能との比較](https://qiita.com/Rei_2020/items/8dc4543bdfef3f149829)”と“[HMS ML Kitの骨格検出機能の実装入門](https://qiita.com/Rei_2020/items/b14293bdd36f77f0155f)”ですでに紹介したので、そちらも合わせてご参考いただければと思います。

### ライブラリ
手のひら検出のライブラリはこちらの二つです。

```gradle:build.gradle
implementation 'com.huawei.hms:ml-computer-vision-handkeypoint:2.0.4.300'
implementation 'com.huawei.hms:ml-computer-vision-handkeypoint-model:2.0.4.300'
```

### AndroidManifest.xml
HMS ML Kitの手のひら検出機能の定義をAndroidManifest.xmlに追加します。

```xml:AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="xxx">
    ...
    <meta-data
        android:name="com.huawei.hms.ml.DEPENDENCY"
        android:value= "handkeypoint"/>
    ...
</manifest>
```


### 手のひら検出
手のひら検出オブジェクトの生成手順は顔検出オブジェクトの生成手順と骨格検出オブジェクトの生成手順と同じです。

```kotlin:MainFragment.kt
private fun generateHandKeypointDetectionLensEngineCreator() : LensEngine.Creator {
	// 検出内容の設定
	val setting = MLHandKeypointAnalyzerSetting.Factory()
		// MLHandKeypointAnalyzerSetting.TYPE_ALL = すべての結果を返す
		// MLHandKeypointAnalyzerSetting.TYPE_KEYPOINT_ONLY = 手の特徴点のみ返す
		// MLHandKeypointAnalyzerSetting.TYPE_RECT_ONLY = 手の領域のみ返す
		.setSceneType(MLHandKeypointAnalyzerSetting.TYPE_ALL) 
		// 最大検出可能な手の個数
		.setMaxHandResults(1)
		.create()

	// 手のひら検出オブジェクトの生成
	analyzer = MLHandKeypointAnalyzerFactory.getInstance().getHandKeypointAnalyzer(setting).apply {
		// 検索結果のコールバックの設定
		setTransactor(object : MLAnalyzer.MLTransactor<MLHandKeypoints> {
			override fun transactResult(results: MLAnalyzer.Result<MLHandKeypoints>?) {
				// 検索結果をオーバーレイに渡す
				binding.overlayView.setResults(results)
			}

			override fun destroy() {
			}
		})
	}

	return LensEngine.Creator(context, analyzer)
}
```

## データの判別方法
手順は次の通りです。

### 1. 比較用の配列を作成
ML Kitから返されるデータ特徴点を

* X座標の配列
* Y座標の配列

に変換します。

また、特徴点の座標は画像からの相対座標なので、それを手のひらの領域からの相対座標に変換する必要があります。

```kotlin
fun MLHandKeypoints.getHandKeypointPair(): Pair<Array<Float>, Array<Float>> {
    val wrist = this.getHandKeypoint(MLHandKeypoint.TYPE_WRIST)
    val thumb1 = this.getHandKeypoint(MLHandKeypoint.TYPE_THUMB_FIRST)
    val thumb2 = this.getHandKeypoint(MLHandKeypoint.TYPE_THUMB_SECOND)
    val thumb3 = this.getHandKeypoint(MLHandKeypoint.TYPE_THUMB_THIRD)
    val thumb4 = this.getHandKeypoint(MLHandKeypoint.TYPE_THUMB_FOURTH)
    val forefinger1 = this.getHandKeypoint(MLHandKeypoint.TYPE_FOREFINGER_FIRST)
    val forefinger2 = this.getHandKeypoint(MLHandKeypoint.TYPE_FOREFINGER_SECOND)
    val forefinger3 = this.getHandKeypoint(MLHandKeypoint.TYPE_FOREFINGER_THIRD)
    val forefinger4 = this.getHandKeypoint(MLHandKeypoint.TYPE_FOREFINGER_FOURTH)
    val middleFinger1 = this.getHandKeypoint(MLHandKeypoint.TYPE_MIDDLE_FINGER_FIRST)
    val middleFinger2 = this.getHandKeypoint(MLHandKeypoint.TYPE_MIDDLE_FINGER_SECOND)
    val middleFinger3 = this.getHandKeypoint(MLHandKeypoint.TYPE_MIDDLE_FINGER_THIRD)
    val middleFinger4 = this.getHandKeypoint(MLHandKeypoint.TYPE_MIDDLE_FINGER_FOURTH)
    val ringFinger1 = this.getHandKeypoint(MLHandKeypoint.TYPE_RING_FINGER_FIRST)
    val ringFinger2 = this.getHandKeypoint(MLHandKeypoint.TYPE_RING_FINGER_SECOND)
    val ringFinger3 = this.getHandKeypoint(MLHandKeypoint.TYPE_RING_FINGER_THIRD)
    val ringFinger4 = this.getHandKeypoint(MLHandKeypoint.TYPE_RING_FINGER_FOURTH)
    val littleFinger1 = this.getHandKeypoint(MLHandKeypoint.TYPE_LITTLE_FINGER_FIRST)
    val littleFinger2 = this.getHandKeypoint(MLHandKeypoint.TYPE_LITTLE_FINGER_SECOND)
    val littleFinger3 = this.getHandKeypoint(MLHandKeypoint.TYPE_LITTLE_FINGER_THIRD)
    val littleFinger4 = this.getHandKeypoint(MLHandKeypoint.TYPE_LITTLE_FINGER_FOURTH)

    return Pair(
            // X座標の配列
            arrayOf(
                    wrist.pointX - rect.left,
                    thumb1.pointX - rect.left, thumb2.pointX - rect.left, thumb3.pointX - rect.left, thumb4.pointX - rect.left,
                    forefinger1.pointX - rect.left, forefinger2.pointX - rect.left, forefinger3.pointX - rect.left, forefinger4.pointX - rect.left,
                    middleFinger1.pointX - rect.left, middleFinger2.pointX - rect.left, middleFinger3.pointX - rect.left, middleFinger4.pointX - rect.left,
                    ringFinger1.pointX - rect.left, ringFinger2.pointX - rect.left, ringFinger3.pointX - rect.left, ringFinger4.pointX - rect.left,
                    littleFinger1.pointX - rect.left, littleFinger2.pointX - rect.left, littleFinger3.pointX - rect.left, littleFinger4.pointX - rect.left
            ),
            // Y座標の配列
            arrayOf(
                    wrist.pointY - rect.top,
                    thumb1.pointY - rect.top, thumb2.pointY - rect.top, thumb3.pointY - rect.top, thumb4.pointY - rect.top,
                    forefinger1.pointY - rect.top, forefinger2.pointY - rect.top, forefinger3.pointY - rect.top, forefinger4.pointY - rect.top,
                    middleFinger1.pointY - rect.top, middleFinger2.pointY - rect.top, middleFinger3.pointY - rect.top, middleFinger4.pointY - rect.top,
                    ringFinger1.pointY - rect.top, ringFinger2.pointY - rect.top, ringFinger3.pointY - rect.top, ringFinger4.pointY - rect.top,
                    littleFinger1.pointY - rect.top, littleFinger2.pointY - rect.top, littleFinger3.pointY - rect.top, littleFinger4.pointY - rect.top
            )
    )
}

```

### 2. 二つの配列を比較し、コサイン類似度を算出
こちらはコサイン類似度の算式です。
![tech_mining_img94.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704548/475143b2-4c21-3519-9407-2bd788d9c133.jpeg)

ぱっと見てわからないかもしれないので、例を使って説明します。

たとえば、次のような配列があるとします。

|配列x|配列y|
|:--:|:--:|
|2|3|
|0|3|
|0|0|
|0|3|
|2|3|

二つの配列のコサイン類似度は次のように計算できます。
![スクリーンショット 2020-11-04 195716.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704548/ee9aa260-70ba-db4c-2d6c-91c60ed465e9.png)

コサイン類似度の値は０～１です。１に近ければ近いほど、類似度が高いというわけです。

また、高い汎用性を想定し、BigDecimalを使います。

```kotlin:SimilarityUtil.kt
import java.math.BigDecimal
import java.math.RoundingMode
import kotlin.math.min

private val SQRT_DIG = BigDecimal(150)
private val SQRT_PRE = BigDecimal(10).pow(SQRT_DIG.toInt())
private const val BIGDECIMAL_SCALE = 10
private const val BIGDECIMAL_ROUNDINGMODE = BigDecimal.ROUND_DOWN

object SimilarityUtil {
    // 二つの配列を比較し、コサイン類似度を算出するメソッド
    fun compare(array1: Array<Float>, array2: Array<Float>): BigDecimal {
        val size = min(array1.size, array2.size) - 1

        var numerator = BigDecimal.ZERO
        for (i in 0..size) {
            numerator = numerator.add(BigDecimal.valueOf(array1[i].toDouble() * array2[i].toDouble()))
        }

        var denominator1 = BigDecimal.ZERO
        array1.forEach { value ->
            val bigValue = BigDecimal.valueOf(value.toDouble())
            denominator1 = denominator1.add(bigValue.multiply(bigValue))
        }
        denominator1 = denominator1.sqrt().setScale(BIGDECIMAL_SCALE, BIGDECIMAL_ROUNDINGMODE)

        var denominator2 = BigDecimal.ZERO
        array2.forEach { value ->
            val bigValue = BigDecimal.valueOf(value.toDouble())
            denominator2 = denominator2.add(bigValue.multiply(bigValue))
        }
        denominator2 = denominator2.sqrt().setScale(BIGDECIMAL_SCALE, BIGDECIMAL_ROUNDINGMODE)

        return numerator.divide(denominator1, BIGDECIMAL_SCALE, BIGDECIMAL_ROUNDINGMODE).divide(denominator2, BIGDECIMAL_SCALE, BIGDECIMAL_ROUNDINGMODE)
    }
}

private fun sqrtNewtonRaphson(c: BigDecimal, xn: BigDecimal, precision: BigDecimal): BigDecimal {
    val fx = xn.pow(2).add(c.negate())
    val fpx = xn.multiply(BigDecimal(2))
    var xn1: BigDecimal = fx.divide(fpx, 2 * SQRT_DIG.toInt(), RoundingMode.HALF_DOWN)
    xn1 = xn.add(xn1.negate())
    val currentSquare = xn1.pow(2)
    var currentPrecision = currentSquare.subtract(c)
    currentPrecision = currentPrecision.abs()
    return if (currentPrecision.compareTo(precision) <= -1) {
        xn1
    } else sqrtNewtonRaphson(c, xn1, precision)
}

fun BigDecimal.sqrt(): BigDecimal {
    return sqrtNewtonRaphson(this, BigDecimal(1), BigDecimal(1).divide(SQRT_PRE))
}
```

### 3. パー・チョキ・グーのパターンと比較
こちらのパー・チョキ・グーのパターンはサンプルです。

```kotlin:HandKeyPointData.kt
class HandKeyPointData {

    companion object {
        val rock = Pair(
                arrayOf(41.276f, 214.91125f, 324.97888f, 314.76807f, 453.758f, 80.19635f, 427.61835f, 429.67285f, 233.00378f, 84.378265f, 373.0124f, 357.93494f, 344.8894f, 107.818665f, 408.64258f, 364.50995f, 286.06537f, 157.31186f, 397.87488f, 358.1792f, 287.08337f),
                arrayOf(263.6897f, 103.11963f, 43.16443f, 54.834717f, 271.7013f, 166.1062f, 111.1814f, 192.62744f, 111.94751f, 265.54077f, 235.55518f, 264.83472f, 257.89197f, 389.995f, 358.11462f, 372.53796f, 399.69824f, 471.76013f, 427.6277f, 408.4226f, 431.7068f)
        )

        val scissors = Pair(
                arrayOf(169.43738f, 238.76599f, 388.1537f, 347.25568f, 236.83105f, 218.57349f, 234.98047f, 250.53857f, 255.20374f, 193.07843f, 107.296844f, 77.06616f, 35.43927f, 53.722107f, 39.475098f, 208.49728f, 236.9411f, 36.82541f, 58.9068f, 148.95392f, 208.33884f),
                arrayOf(709.02124f, 638.40015f, 468.63232f, 521.01855f, 572.69214f, 373.50488f, 253.7351f, 164.92944f, 64.48749f, 414.94763f, 342.4065f, 273.62012f, 195.82544f, 440.02002f, 435.06042f, 528.77563f, 570.60876f, 548.62f, 544.7168f, 589.99f, 614.97144f)
        )

        val paper = Pair(
                arrayOf(158.61594f, 257.23584f, 321.78967f, 322.68817f, 257.6515f, 281.95667f, 236.70715f, 223.77979f, 210.91858f, 256.8501f, 163.98566f, 82.79358f, 29.706177f, 238.10547f, 198.91608f, 106.33908f, 84.14255f, 159.59784f, 158.92807f, 173.5293f, 135.32196f),
                arrayOf(802.3806f, 793.6758f, 605.0879f, 633.37305f, 732.2988f, 372.59717f, 246.68213f, 142.75195f, 73.309204f, 457.32495f, 295.39587f, 237.64575f, 172.12012f, 472.11267f, 408.26953f, 364.9807f, 324.3921f, 579.5957f, 456.87488f, 534.67554f, 493.6067f)
        )
    }
}
```

オーバーレイで配列を比較し、比較結果を描画します。

```kotlin:OverlayView.kt
private fun drawFaceResult(canvas: Canvas, results: MLAnalyzer.Result<MLHandKeypoints>?) {
    val mlList: SparseArray<MLHandKeypoints> = results?.analyseList ?: return

    mlList.forEach { key, value ->
        value.rect?.let { rect ->
            canvas.drawRect(
                    Rect(translateX(canvas, rect.left.toFloat()).toInt(),
                            translateY(canvas, rect.top.toFloat()).toInt(),
                            translateX(canvas, rect.right.toFloat()).toInt(),
                            translateY(canvas, rect.bottom.toFloat()).toInt()),
                    rectPaint)

            val x = translateX(canvas, if (lensType == LensEngine.FRONT_LENS) rect.right.toFloat() else rect.left.toFloat())
            var y = translateY(canvas, rect.top.toFloat()) - 5.0f

            val handKeypointPair = value.getHandKeypointPair()
            val rockSimilarity = SimilarityUtil.compare(handKeypointPair.first, HandKeyPointData.rock.first).multiply(SimilarityUtil.compare(handKeypointPair.second, HandKeyPointData.rock.second))
            val scissorsSimilarity = SimilarityUtil.compare(handKeypointPair.first, HandKeyPointData.scissors.first).multiply(SimilarityUtil.compare(handKeypointPair.second, HandKeyPointData.scissors.second))
            val paperSimilarity = SimilarityUtil.compare(handKeypointPair.first, HandKeyPointData.paper.first).multiply(SimilarityUtil.compare(handKeypointPair.second, HandKeyPointData.paper.second))

            val map: MutableMap<String, Point> = mutableMapOf()
            map[String.format("グー:%.3f", rockSimilarity)] = Point(x.toInt(), y.apply { y -= 50.0f }.toInt())
            map[String.format("チョキ:%.3f", scissorsSimilarity)] = Point(x.toInt(), y.apply { y -= 50.0f }.toInt())
            map[String.format("パー:%.3f", paperSimilarity)] = Point(x.toInt(), y.apply { y -= 50.0f }.toInt())

            map.forEach { (string, point) ->
                canvas.drawText(string,
                        point.x.toFloat(),
                        point.y.toFloat(),
                        comparePaint)
            }
        }
    }
}

```

実装は以上になります。

### 実装結果サンプル
<img width="600" alt="image1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704548/120dcb40-8abc-c989-7f98-7ca498519cc8.jpeg">
<img width="600" alt="image2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704548/5d0a4898-21e5-d0f7-ba11-d3d430d49c9f.jpeg">
<img width="600" alt="image3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/704548/5e1fdae8-1dcc-d3ae-ec0b-58c0d9ffb855.jpeg">




# GitHub
* [https://github.com/Rei2020GitHub/MyPublicProject/tree/master/ml/HandKeypointDetectionDemo](https://github.com/Rei2020GitHub/MyPublicProject/tree/master/ml/HandKeypointDetectionDemo)

# APK
* [https://github.com/Rei2020GitHub/MyPublicProject/blob/master/APK/HandKeypointDetectionDemo.apk](https://github.com/Rei2020GitHub/MyPublicProject/blob/master/APK/HandKeypointDetectionDemo.apk)

# 参考
* HMS：[https://developer.huawei.com/consumer/jp/](https://developer.huawei.com/consumer/jp/)
* HMS ML Kitの紹介：[https://developer.huawei.com/consumer/jp/hms/huawei-mlkit](https://developer.huawei.com/consumer/jp/hms/huawei-mlkit)
* HMS ML Kitのドキュメント：[https://developer.huawei.com/consumer/jp/doc/development/HMSCore-Guides/service-introduction-0000001050040017](https://developer.huawei.com/consumer/jp/doc/development/HMSCore-Guides/service-introduction-0000001050040017)
* HMS ML Kitの手のひら検出の概要：[https://developer.huawei.com/consumer/jp/doc/development/HMSCore-Guides-V5/handkeypointdetection-0000001051615541-V5](https://developer.huawei.com/consumer/jp/doc/development/HMSCore-Guides-V5/handkeypointdetection-0000001051615541-V5)
* Huawei Developers：[https://forums.developer.huawei.com/forumPortal/en/home](https://forums.developer.huawei.com/forumPortal/en/home)
* Facebook Huawei Developersグループ：[https://www.facebook.com/Huaweidevs/](https://www.facebook.com/Huaweidevs/)

# ※素材について
[写真AC](https://www.photo-ac.com/)が提供している商用利用可能なフリー素材を利用させていただきました。写真のもとの場所はこちらです。

* [https://www.photo-ac.com/main/detail/3438430?title=%E6%8B%B3%E3%80%80%E5%A5%B3%E6%80%A7&searchId=141194830](https://www.photo-ac.com/main/detail/3438430?title=%E6%8B%B3%E3%80%80%E5%A5%B3%E6%80%A7&searchId=141194830)
* [https://www.photo-ac.com/main/detail/2120205?title=%E3%83%81%E3%83%A7%E3%82%AD&searchId=141198820](https://www.photo-ac.com/main/detail/2120205?title=%E3%83%81%E3%83%A7%E3%82%AD&searchId=141198820)
* [https://www.photo-ac.com/main/detail/874031?title=%E8%8B%A5%E3%81%84%E5%A5%B3%E6%80%A7%E3%81%AE%E6%89%8B%E3%80%80%E3%83%91%E3%83%BC&searchId=141200801](https://www.photo-ac.com/main/detail/874031?title=%E8%8B%A5%E3%81%84%E5%A5%B3%E6%80%A7%E3%81%AE%E6%89%8B%E3%80%80%E3%83%91%E3%83%BC&searchId=141200801)
* [https://www.photo-ac.com/main/detail/2852083?title=%E6%89%8B%E3%81%A0%E3%81%91%E3%81%AE%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA%2003&searchId=141200801](https://www.photo-ac.com/main/detail/2852083?title=%E6%89%8B%E3%81%A0%E3%81%91%E3%81%AE%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA%2003&searchId=141200801)