3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

HMS ML Kitの顔検出機能の実装入門とFirebase ML Kitの顔検出機能との比較

Last updated at Posted at 2020-10-22

HMSのML Kitについて

HMS ML Kitはファーウェイが提供しているAndroid用の機械学習キットです。ARMのAndroid端末であれば、ファーウェイの端末じゃなくても使えます。機能についてはすでにHMSとFirebaseの機械学習(ML Kit)の比較で紹介したので、そちらをご参照いただければ幸いです。

HMSの顔検出とFirebaseの顔検出の比較

最近の顔検出SDKは単なる顔を検出し、その領域を特定するだけでなく、目や鼻の位置を特定したり、笑顔を検出したりもします。当然、SDKによって、含まれている機能が異なるので、そこをしっかり把握したうえで、SDKを選定したほうがおすすめです。

HMS ML KitとFirebase ML KitはAndroidで簡単に導入できる顔検出SDKです。それらの機能を次のようにまとめました。

※Googleは2020年6月3日にFirebaseに依存しないGoogle ML Kitをリリースしましたが、機能的にFirebaseのML Kitと同じです。

検出対象

機能 HMS ML Kit Firebase ML Kit
顔検出(静止画)
顔検出(動画)

顔のパーツ

機能 HMS ML Kit Firebase ML Kit
顔の領域(長方形)
顔の輪郭
左目の輪郭
右目の輪郭
左眉の輪郭(上部)
左眉の輪郭(下部)
右眉の輪郭(上部)
右眉の輪郭(下部)
上唇の輪郭(上部)
上唇の輪郭(下部)
下唇の輪郭(上部)
下唇の輪郭(下部)
鼻柱の位置
鼻の下部の輪郭
顔の三次元向き

表情

機能 HMS ML Kit Firebase ML Kit
笑う
怒る ×
嫌がる ×
怖がる ×
悲しむ ×
驚く ×
無表情 ×

顔の特徴

機能 HMS ML Kit Firebase ML Kit
左目の開き具合
右目の開き具合
メガネの有無 ×
帽子の有無 ×
ひげの有無 ×
性別 ×
年齢 ×

HMS ML Kitのほうが機能数が多いです。

HMSの顔検出の実装(ステップ1:カメラ関連の実装)

Androidカメラのプレビュー表示(Camera API + SurfaceView)で紹介した実装の一部を利用します。

カメラのパーミッション

AndroidManifest.xml

カメラを使うのに、カメラのパーミッションが必要です。AndroidManifest.xmlにandroid.permission.CAMERAを追加します。

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="xxx">
    ...
    <uses-permission android:name="android.permission.CAMERA" />
    ...
</manifest>

パーミッション要請

ユーザーにカメラ権限を要請します。

MyActivity.kt
// カメラ権限があるかどうかを確認し、ある場合はカメラを起動し、ない場合はユーザーに要請します。
private fun checkAndAskCameraPermission(context: Context) {
	if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
		// ユーザーにカメラ権限を要請
		requestPermissions(arrayOf(Manifest.permission.CAMERA), PERMISSION_REQUEST_CODE)
	} else {
		// カメラ権限があるので、カメラを起動
		openCamera()
	} 
}

// カメラ権限要請のコールバック
override fun onRequestPermissionsResult(
		requestCode: Int,
		permissions: Array<String>, grantResults: IntArray
) {
	when (requestCode) {
		PERMISSION_REQUEST_CODE -> {
			if (permissions.isNotEmpty() && grantResults.isNotEmpty()) {
				val resultMap: MutableMap<String, Int> = mutableMapOf()
				for (i in 0..min(permissions.size - 1, grantResults.size - 1)) {
					resultMap[permissions[i]] = grantResults[i]
				}

				when {
					resultMap[Manifest.permission.CAMERA] == PackageManager.PERMISSION_GRANTED -> {
						// ユーザーがカメラ権限を許可したので、カメラを起動
						openCamera()
					}
				}
			}
		}
	}
}

[注意点]:Surfaceサイズの調整

表示のアスペクト比を調整し、画面いっぱいに表示します。

MyActivity.kt
private var surfaceWidth = 1
private var surfaceHeight = 1

private fun updateSurfaceSize(view: View, width: Int, height: Int, surfaceWidth: Int, surfaceHeight: Int) {
	val param = view.layoutParams
	var newWidth = width
	var newHeight = height

	// [注意点]:端末が縦の場合、設定する横幅と縦幅は逆になる
	if (activity?.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
		newWidth = height
		newHeight = width
	}

	val scale1 = surfaceWidth / newWidth.toFloat()
	val scale2 = surfaceHeight / newHeight.toFloat()
	val scale = if (newWidth < surfaceWidth || newHeight < surfaceHeight) {
		max(scale1, scale2)
	} else if (newWidth < surfaceWidth) {
		scale1
	} else if (newHeight < surfaceHeight) {
		scale2
	} else {
		1.0f
	}

	// [注意点]:入力された横幅と縦幅を使う(SurfaceViewの幅と高さだと、アスペクト比が合わないことがある)
	param.width = (newWidth.toFloat() * scale).toInt()
	param.height = (newHeight.toFloat() * scale).toInt()
	view.layoutParams = param
}

HMSの顔検出の実装(ステップ2:ML Kit関連の実装)

こちらはML Kit関連の実装になります。

AppGallery Connectの作業

  1. AppGallery Connectに入って、[My projects]を選びます。
  2. リストから対象アプリに切り替えます。
  3. [Project settings] -> [Manage APIs]に入って、ML Kitを有効にします。(デフォルトではすでに有効になっているはずです)

こちらもご参照ください。
https://developer.huawei.com/consumer/jp/doc/development/HMSCore-Guides/config-agc-0000001050990353
https://developer.huawei.com/consumer/jp/doc/development/HMSCore-Guides/enable-service-0000001050038078

HMS SDKの導入

  1. agconnect-services.jsonをプロジェクト内に配置します。
  2. プロジェクトのbuild.gradleにSDKを追加します。
  3. モジュールのbuild.gradleにSDKを追加します。

顔検出関連のライブラリはこちらです。

build.gradle
    implementation 'com.huawei.hms:ml-computer-vision-face:2.0.1.300'
    implementation 'com.huawei.hms:ml-computer-vision-face-shape-point-model:2.0.1.300'
    implementation 'com.huawei.hms:ml-computer-vision-face-emotion-model:2.0.1.300'
    implementation 'com.huawei.hms:ml-computer-vision-face-feature-model:2.0.1.300'

こちらもご参照ください。
https://developer.huawei.com/consumer/jp/doc/development/HMSCore-Guides/add-appgallery-0000001050038080
https://developer.huawei.com/consumer/jp/doc/development/HMSCore-Guides/config-maven-0000001050040031
https://developer.huawei.com/consumer/jp/doc/development/HMSCore-Guides/face-sdk-0000001050038096

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= "face"/>
    ...
</manifest>

レイアウト

顔のパーツなどを描画するため、Viewを継承したオーバーレイクラスを配置します。
オーバーレイに表示しない場合はオーバーレイクラスを追加不要です。

main_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    >

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/main_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:keepScreenOn="true">

        <SurfaceView
            android:id="@+id/surface_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            />

        <!-- オーバーレイのクラス -->
        <your.package.name.OverlayView
            android:id="@+id/overlay_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

カメラと顔検出の初期化

ML Kitのキーポイントが2つです。

画像解析処理のMLAnalyzer<T>を作って、それをもとにカメラのLensEngineを作って、さらに解析結果のコールバックをMLAnalyzer<T>に設定すればよいです。

MainFragment.kt
private var faceAnalyzer: MLFaceAnalyzer? = null
private var lensEngine: LensEngine? = null

private fun openCamera() {
	// 顔分析オブジェクトを作成
	faceAnalyzer = MLAnalyzerFactory.getInstance().faceAnalyzer.apply {
		setTransactor(object : MLAnalyzer.MLTransactor<MLFace> {
			// 分析結果がtransactResultコールバックで渡される
			override fun transactResult(results: MLAnalyzer.Result<MLFace>?) {
				// このサンプルでは分析結果をオーバーレイに描画する
				binding.overlayView.setFaceResults(results)
			}

			override fun destroy() {
			}
		})
	}

	// ML Kit専用のカメラオブジェクトを作成
	// LensEngineの中身はCamera APIのラッパー
	lensEngine = LensEngine.Creator(context, faceAnalyzer)
		// フロントカメラ:LensEngine.FRONT_LENS
		// バックカメラ:LensEngine.BACK_LENS
		.setLensType(LensEngine.FRONT_LENS)
		// プレビューサイズ:1920x1920 - 320x320を設定する
		.applyDisplayDimension(1920, 1920)
		.applyFps(30.0f)
		.enableAutomaticFocus(true)
		.create()
		.apply {
			// カメラをSurfaceViewに出力
			run(surfaceHolder)
			// [カメラの出力]:SurfaceViewのアスペクト比とサイズを調整
			updateSurfaceSize(
				binding.surfaceView,
				this.displayDimension.width,
				this.displayDimension.height,
				surfaceWidth,
				surfaceHeight
			)
			// [顔検出結果の描画]:オーバーレイのアスペクト比とサイズの調整とカメラのパラメータの受け渡し
			updateOverlay(this)
		}
}

オーバーレイのアスペクト比とサイズの調整とカメラのパラメータの受け渡し

オーバーレイのサイズをSurfaceViewのサイズと同じにします。描画するときに、カメラのサイズとカメラの種類が必要なので、ここで渡しておきます。

MainFragment.kt
private fun updateOverlay(lensEngine: LensEngine) {
	if (activity?.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
		binding.overlayView.setCameraWidth(lensEngine.displayDimension.height)
		binding.overlayView.setCameraHeight(lensEngine.displayDimension.width)
	} else {
		binding.overlayView.setCameraWidth(lensEngine.displayDimension.width)
		binding.overlayView.setCameraHeight(lensEngine.displayDimension.height)
	}

	binding.overlayView.layoutParams.width = binding.surfaceView.layoutParams.width
	binding.overlayView.layoutParams.height = binding.surfaceView.layoutParams.height
	binding.overlayView.setLensType(lensEngine.lensType)
}

SurfaceView

MainFragment.kt
override fun onCreateView(
	inflater: LayoutInflater,
	container: ViewGroup?,
	savedInstanceState: Bundle?
): View? {
	binding = DataBindingUtil.inflate<MainFragmentBinding>(
		inflater,
		R.layout.main_fragment,
		container,
		false
	)
	binding.surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
		override fun surfaceCreated(holder: SurfaceHolder) {
			surfaceHolder = holder
			checkAndAskPermission()
		}

		override fun surfaceChanged(
			holder: SurfaceHolder,
			format: Int,
			width: Int,
			height: Int
		) {
			surfaceWidth = width
			surfaceHeight = height
			lensEngine?.let { lensEngine ->
				// [カメラの出力]:SurfaceViewのアスペクト比とサイズを調整
				updateSurfaceSize(
					binding.surfaceView,
					lensEngine.displayDimension.width,
					lensEngine.displayDimension.height,
					surfaceWidth,
					surfaceHeight
				)
				// [顔検出結果の描画]:オーバーレイのアスペクト比とサイズの調整とカメラのパラメータの受け渡し
				updateOverlay(lensEngine)
			}
		}

		override fun surfaceDestroyed(holder: SurfaceHolder) {
		}
	})

	return binding.root
}

リソース解放

MainFragment.kt
override fun onStop() {
	super.onStop()
	faceAnalyzer?.let {
		try {
			it.stop()
		} catch (ioException: IOException) {
			ioException.printStackTrace()
		}
	}
	lensEngine?.let {
		it.release()
	}
}

分析結果の描画

顔検出結果はオーバーレイで描画します。ここでは注意点がいくつかあります。

描画座標
カメラのプレビューサイズとオーバーレイのサイズが一致しない場合、faceAnalyzerのコールバックから渡されたx座標をそのまま使うと、表示がずれてしまいます。

オーバーレイのx座標 = faceAnalyzerのx座標 x キャンバスの幅 / カメラの幅
オーバーレイのy座標 = faceAnalyzerのy座標 x キャンバスの高さ / カメラの高さ

カメラの種類
フロントカメラの場合、faceAnalyzerのx座標の起点は左ではなく、右からです。
フロントカメラの場合のオーバーレイのx座標 = View.width - (faceAnalyzerのx座標 x キャンバスの幅 / カメラの幅)

OverlayView.kt
class OverlayView : View {

    private val paint = Paint().apply {
        color = Color.RED
        style = Paint.Style.STROKE
        strokeWidth = 6.0f
    }

    private var faceResults: MLAnalyzer.Result<MLFace>? = null
    private var lensType = LensEngine.FRONT_LENS
    private var cameraWidth = 1
    private var cameraHeight = 1

    constructor(context: Context?): super(context)

    constructor(context: Context?, attrs: AttributeSet?): super(context, attrs)

    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr)

    fun setFaceResults(results: MLAnalyzer.Result<MLFace>?) {
        faceResults = results
        invalidate()
    }

    fun setLensType(lensType: Int) {
        this.lensType = lensType
    }

    fun setCameraWidth(width: Int) {
        cameraWidth = width
    }

    fun setCameraHeight(height: Int) {
        cameraHeight = height
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        canvas?.let { canvas ->
            drawFaceResult(canvas, faceResults)
        }
    }

    private fun drawFaceResult(canvas: Canvas, results: MLAnalyzer.Result<MLFace>?) {
        val mlFaceList: SparseArray<MLFace> = results?.analyseList ?: return

        mlFaceList.forEach { key, value ->
            value.border?.let { border ->
                // 顔領域の描画
                canvas.drawRect(
                    translateX(canvas, border.left.toFloat()),
                    translateY(canvas, border.top.toFloat()),
                    translateX(canvas, border.right.toFloat()),
                    translateY(canvas, border.bottom.toFloat()),
                    paint)

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

                // 感情分析結果を描画
                value.emotions?.let { mlFaceEmotion ->
                    val map: MutableMap<String, Point> = mutableMapOf()
                    map[String.format("怒る:%.3f", mlFaceEmotion.angryProbability)] = Point(x.toInt(), y.apply { y -= 50.0f }.toInt())
                    map[String.format("嫌がる:%.3f", mlFaceEmotion.disgustProbability)] = Point(x.toInt(), y.apply { y -= 50.0f }.toInt())
                    map[String.format("怖がる:%.3f", mlFaceEmotion.fearProbability)] = Point(x.toInt(), y.apply { y -= 50.0f }.toInt())
                    map[String.format("無表情:%.3f", mlFaceEmotion.neutralProbability)] = Point(x.toInt(), y.apply { y -= 50.0f }.toInt())
                    map[String.format("悲しむ:%.3f", mlFaceEmotion.sadProbability)] = Point(x.toInt(), y.apply { y -= 50.0f }.toInt())
                    map[String.format("笑顔:%.3f", mlFaceEmotion.smilingProbability)] = Point(x.toInt(), y.apply { y -= 50.0f }.toInt())
                    map[String.format("驚く:%.3f", mlFaceEmotion.surpriseProbability)] = Point(x.toInt(), y.apply { y -= 50.0f }.toInt())

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

                // 顔の特徴を描画
                value.features?.let { mlFaceFeature ->
                    if (mlFaceFeature.age >= 0) {
                        canvas.drawText(String.format("年齢:%d", mlFaceFeature.age), x, y.apply { y -= 50.0f }, paint)
                    }
                    if (mlFaceFeature.hatProbability >= 0.0f) {
                        canvas.drawText(String.format("帽子:%.3f", mlFaceFeature.hatProbability), x, y.apply { y -= 50.0f }, paint)
                    }
                    if (mlFaceFeature.leftEyeOpenProbability >= 0.0f) {
                        canvas.drawText(String.format("左目:%.3f", mlFaceFeature.leftEyeOpenProbability), x, y.apply { y -= 50.0f }, paint)
                    }
                    if (mlFaceFeature.rightEyeOpenProbability >= 0.0f) {
                        canvas.drawText(String.format("右目:%.3f", mlFaceFeature.rightEyeOpenProbability), x, y.apply { y -= 50.0f }, paint)
                    }
                    if (mlFaceFeature.moustacheProbability >= 0.0f) {
                        canvas.drawText(String.format("ひげ:%.3f", mlFaceFeature.moustacheProbability), x, y.apply { y -= 50.0f }, paint)
                    }
                    if (mlFaceFeature.sexProbability >= 0.0f) {
                        canvas.drawText(String.format("性別:%s、%.3f", if (mlFaceFeature.moustacheProbability <= 0.5f) "男" else "女", mlFaceFeature.moustacheProbability), x, y.apply { y -= 50.0f }, paint)
                    }
                    if (mlFaceFeature.sunGlassProbability >= 0.0f) {
                        canvas.drawText(String.format("メガネ:%.3f", mlFaceFeature.sunGlassProbability), x, y.apply { y -= 50.0f }, paint)
                    }
                }
            }

            // 顔の輪郭を描画
            value.faceShapeList.filter {
                it.faceShapeType == MLFaceShape.TYPE_FACE
            }.forEach { mlFaceShape ->
                mlFaceShape.points.forEach { point ->
                    canvas.drawPoint(translateX(canvas, point.x), translateY(canvas, point.y), paint)
                }
            }

            // 目の輪郭を描画
            value.faceShapeList.filter {
                it.faceShapeType == MLFaceShape.TYPE_LEFT_EYE || it.faceShapeType == MLFaceShape.TYPE_RIGHT_EYE
            }.forEach { mlFaceShape ->
                mlFaceShape.points.forEach { point ->
                    canvas.drawPoint(translateX(canvas, point.x), translateY(canvas, point.y), paint)
                }
            }

            // 眉の輪郭を描画
            value.faceShapeList.filter {
                it.faceShapeType == MLFaceShape.TYPE_TOP_OF_LEFT_EYEBROW
                        || it.faceShapeType == MLFaceShape.TYPE_BOTTOM_OF_LEFT_EYEBROW
                        || it.faceShapeType == MLFaceShape.TYPE_TOP_OF_RIGHT_EYEBROW
                        || it.faceShapeType == MLFaceShape.TYPE_BOTTOM_OF_RIGHT_EYEBROW
            }.forEach { mlFaceShape ->
                mlFaceShape.points.forEach { point ->
                    canvas.drawPoint(translateX(canvas, point.x), translateY(canvas, point.y), paint)
                }
            }

            // くちびるの輪郭を描画
            value.faceShapeList.filter {
                it.faceShapeType == MLFaceShape.TYPE_TOP_OF_UPPER_LIP
                        || it.faceShapeType == MLFaceShape.TYPE_BOTTOM_OF_UPPER_LIP
                        || it.faceShapeType == MLFaceShape.TYPE_TOP_OF_LOWER_LIP
                        || it.faceShapeType == MLFaceShape.TYPE_BOTTOM_OF_LOWER_LIP
            }.forEach { mlFaceShape ->
                mlFaceShape.points.forEach { point ->
                    canvas.drawPoint(translateX(canvas, point.x), translateY(canvas, point.y), paint)
                }
            }

            // 鼻の輪郭を描画
            value.faceShapeList.filter {
                it.faceShapeType == MLFaceShape.TYPE_BRIDGE_OF_NOSE || it.faceShapeType == MLFaceShape.TYPE_BOTTOM_OF_NOSE
            }.forEach { mlFaceShape ->
                mlFaceShape.points.forEach { point ->
                    canvas.drawPoint(translateX(canvas, point.x), translateY(canvas, point.y), paint)
                }
            }
        }
    }

    // [注意点]:カメラのプレビューサイズとオーバーレイのサイズが一致していない場合、
    // faceAnalyzerのコールバックから渡されたx座標をそのまま使うと、表示がずれてしまいます。
    // オーバーレイのx座標 = faceAnalyzerのx座標 x キャンバスの幅 / カメラの幅
    //
    // [注意点2]:フロントカメラの場合、faceAnalyzerのx座標の起点は左ではなく、右からです。
    // フロントカメラの場合のオーバーレイのx座標 = View.width - (faceAnalyzerのx座標 x キャンバスの幅 / カメラの幅)
    private fun translateX(canvas: Canvas, x: Float): Float {
        return if (lensType == LensEngine.FRONT_LENS) {
            width - x * canvas.width / cameraWidth
        } else {
            x * canvas.width / cameraWidth
        }
    }

    // [注意点]:カメラのプレビューサイズとオーバーレイのサイズが一致しない場合、
    // faceAnalyzerのコールバックから渡されたy座標をそのまま使うと、表示がずれてしまいます。
    // オーバーレイのy座標 = faceAnalyzerのy座標 x キャンバスの高さ / カメラの高さ
    private fun translateY(canvas: Canvas, y: Float): Float {
        return y * canvas.height / cameraHeight
    }
}

実装は以上になります。

最後

HMS ML Kitの顔検出機能がかなり強力です。また、FirebaseのML Kitよりも機能が多く、人のさまざまな感情も数値化でき、さらにメガネの有無も検出できるので、使い方によっていろいろなサービスが実現できそうです。しかも無料に使えます。みなさんもぜひお試しください。

GitHub

APK

参考

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?