Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

手のひら検出機能について

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

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

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

image.png

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

image.png

じゃんけん判別アプリの作成

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

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

実装について

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

ライブラリ

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

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に追加します。

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>

手のひら検出

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

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座標の配列

に変換します。

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

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

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

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

配列x 配列y
2 3
0 3
0 0
0 3
2 3

二つの配列のコサイン類似度は次のように計算できます。
スクリーンショット 2020-11-04 195716.png

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

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

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. パー・チョキ・グーのパターンと比較

こちらのパー・チョキ・グーのパターンはサンプルです。

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)
        )
    }
}

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

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)
            }
        }
    }
}

実装は以上になります。

実装結果サンプル

image1.png
image2.png
image3.png

GitHub

APK

参考

※素材について

写真ACが提供している商用利用可能なフリー素材を利用させていただきました。写真のもとの場所はこちらです。

huaweijapan
2005年に設立されたファーウェイ・ジャパン(華為技術日本株式会社)は、2020年6月現在950人以上の従業員を擁し、そのうち78%以上が現地採用となっています。通信事業者向けネットワーク事業、法人向けICTソリューション事業、コンシューマー向け端末事業の3つの事業分野を柱とし、日本市場のお客様のニーズに応える幅広い製品やサービスを提供しています。
https://www.huawei.com/jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away