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を追加します。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="xxx">
...
<uses-permission android:name="android.permission.CAMERA" />
...
</manifest>
パーミッション要請
ユーザーにカメラ権限を要請します。
// カメラ権限があるかどうかを確認し、ある場合はカメラを起動し、ない場合はユーザーに要請します。
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サイズの調整
表示のアスペクト比を調整し、画面いっぱいに表示します。
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の作業
- AppGallery Connectに入って、[My projects]を選びます。
- リストから対象アプリに切り替えます。
- [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の導入
- agconnect-services.jsonをプロジェクト内に配置します。
- プロジェクトのbuild.gradleにSDKを追加します。
- モジュールのbuild.gradleにSDKを追加します。
顔検出関連のライブラリはこちらです。
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に追加しなければなりません。
<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を継承したオーバーレイクラスを配置します。
オーバーレイに表示しない場合はオーバーレイクラスを追加不要です。
<?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つです。
- カメラであるLensEngine
- 画像解析処理であるMLAnalyzer<T>
画像解析処理のMLAnalyzer<T>を作って、それをもとにカメラのLensEngineを作って、さらに解析結果のコールバックをMLAnalyzer<T>に設定すればよいです。
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のサイズと同じにします。描画するときに、カメラのサイズとカメラの種類が必要なので、ここで渡しておきます。
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
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
}
リソース解放
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 キャンバスの幅 / カメラの幅)
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
参考
- HMS:https://developer.huawei.com/consumer/jp/
- HMS ML Kitの紹介: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
- HMS ML Kitの顔検出の概要:https://developer.huawei.com/consumer/jp/doc/development/HMSCore-Guides/face-detection-0000001050038170
- Huawei Developers:https://forums.developer.huawei.com/forumPortal/en/home
- Facebook Huawei Developersグループ:https://www.facebook.com/Huaweidevs/