14
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 1 year has passed since last update.

iOSAdvent Calendar 2021

Day 3

【キュンです】指ハートを機械学習してエフェクトを出す

Last updated at Posted at 2021-12-02

Macで機械学習でHand Pose Classification(手のポーズ分類)ができます。
今回はデモとして、CreateMLでポーズ認識モデルを作成して、「指ハート」と「ピースサイン」を認識するスマホアプリを作ってみます。

【キュンです】

#1、データを集める

学習には画像データが必要です。
集めてフォルダ分けします。

必要な画像:
・識別したいポーズの画像(今回は"fingerHeart"と"peace")
・識別する必要のないポーズの画像("background")

を、それぞれのクラスごとにフォルダ分けします。
"background"には、識別する必要のないさまざまなポーズと、分類したいポーズに移行するときの中途半端な手の画像を含めます。
さまざまな肌の色、年齢、性別、照明条件の画像を用意します。

今回はそれぞれのクラスで200枚前後を用意しました。

#2、CreateMLでモデルをトレーニングする

CreateMLをひらきます。
(Xcodeをコントロールクリックして、OpenDeveloperToolsから開く。)

HandPoseClassificationを選択します。
(HandPoseClassificationは、macOS Monterey / Xcode13以上の環境で使えます。)

データセット(Training Data)を選択。
検証データは指定しなければ、自動で生成されます。

データセット拡張(Augmentations:画像を回転させたりして水増し)を適宜設定します。今回は手の左右は問わないので、水平反転と回転を入れてみました。

Trainを押して学習を開始します。

数分で学習が終わります。

トレーニングが終わると、Previewタブから手持ちの画像でテストができます。

macのカメラでライブプレビューもできます。

Outputタブからモデルを入手します。

#3、アプリでモデルを使用する

モデルをXcodeプロジェクトにドロップして初期化します。

import CoreML
import Vision

...

let model = try? MyHandPoseClassifier_1(configuration: MLModelConfiguration())

Visionフレームワークで手のポイント(指先、関節などのキーポイントの位置:上記CreateMLプレビューの点の位置)を検出してからモデルに入力します。

func session(_ session: ARSession, didUpdate frame: ARFrame) {
    // 今回はARSessionからカメラフレームを取得します
    let pixelBuffer = frame.capturedImage
    // 手のポーズの検出リクエストを作成
    let handPoseRequest = VNDetectHumanHandPoseRequest()
    // 取得する手の数
    handPoseRequest.maximumHandCount = 1 
    
    // カメラフレームで検出リクエストを実行
    // カメラから取得したフレームは90度回転していて、
    // そのまま推論にかけるとポーズを正しく認識しなかったりするので、
    // orientationを確認する
    let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: .right, options: [:])
    do {
        try handler.perform([handPoseRequest])
    } catch {
        assertionFailure("HandPoseRequest failed: \(error)")
    }
        
    guard let handPoses = handPoseRequest.results, !handPoses.isEmpty else {
        return
    }

    // 取得した手のデータ
    guard let observation = handPoses.first else { return }
    
    // 毎フレーム、モデルの推論を実行すると処理が重くなり、
    // ARのレンダリングをブロックする可能性があるので、インターバルをあけて推論実行する   
    frameCounter += 1
    if frameCounter % handPosePredictionInterval == 0 {
        makePrediction(handPoseObservation: observation)
        frameCounter = 0
    }
}

(注)カメラから取得したフレームは90度回転していて、そのまま推論にかけるとポーズを正しく認識しなかったりします。フレームの向き(orientation)を確認してから後続の作業をした方がいいと思います

取得した手のポイントのデータをMultiArray(多次元配列)に変換しCoreMLでモデルに入力・推論実行します。

func makePrediction(handPoseObservation: VNHumanHandPoseObservation) {
    // 手のポイントの検出結果を多次元配列に変換
    guard let keypointsMultiArray = try? handPoseObservation.keypointsMultiArray() else { fatalError() }
    do {
        // モデルに入力して推論実行
        let prediction = try model!.prediction(poses: keypointsMultiArray)
        let label = prediction.label // 最も信頼度の高いラベル
        guard let confidence = prediction.labelProbabilities[label] else { return } // labelの信頼度
        print("label:\(prediction.label)\nconfidence:\(confidence)")
    } catch {
        print("Prediction error")
    }    
}

label:fingerHeart
confidence:0.9999963045120239

#4、手のポーズに応じてARをつける

取得した分類ラベルに応じて処理をスイッチします。

if confidence > 0.9 { // 信頼度が90%以上で実行
    switch label {
    case "fingerHeart":displayFingerHeartEffect()
    case "peace":displayPeaceEffect()
    default : break
    }
}

エフェクトを指の位置に出現させるために、
Visionで取得した指の位置をカメラから20cm奥にマッピングします。

func getHandPosition(handPoseObservation: VNHumanHandPoseObservation) -> SCNVector3? {
    // 人差し指の第二関節の位置に出現させる
    guard let indexFingerPip = try? handPoseObservation.recognizedPoints(.all)[.indexPIP], 
          indexFingerTip.confidence > 0.3 else {return nil}

    // Visionの指の位置の検出結果は0~1に正規化されているので、
    // view.boundsのサイズに直す。これにはVisionの関数が使える。
    // また、Visionの座標原点は左下なので、Yを反転させてviewの座標システムに合わせる
    let deNormalizedIndexPoint = VNImagePointForNormalizedPoint(CGPoint(x: indexFingerTip.location.x, y:1-indexFingerTip.location.y), viewWidth,  viewHeight)
    
    // 指はカメラから20cm奥にあると想定する
    let infrontOfCamera = SCNVector3(x: 0, y: 0, z: -0.2)
    guard let cameraNode = arScnView.pointOfView else { return nil}
    // カメラのワールド座標位置から20cm奥の位置を求める
    let pointInWorld = cameraNode.convertPosition(infrontOfCamera, to: nil)
    // view平面内の2次元の人差し指の位置と上記20cm奥の位置をスクリーン平面に定義する
    var screenPos = arScnView.projectPoint(pointInWorld)
    screenPos.x = Float(deNormalizedIndexPoint.x)
    screenPos.y = Float(deNormalizedIndexPoint.y)
    // スクリーン平面の指の位置を3次元座標にマッピングする
    let finalPosition = arScnView.unprojectPoint(screenPos)
    return finalPosition
}

SceneKitをつかって、3Dオブジェクトをアニメーションさせます。

func displayFingerHeartEffect(){
    guard !isEffectAppearing else { return } // エフェクトが起動中かチェック
    isEffectAppearing = true
    // 人差し指の第二関節の位置を取得
    guard let handPoseObservation = currentHandPoseObservation,let indexFingerPosition = getHandPosition(handPoseObservation: handPoseObservation) else {return}
    // エフェクトを指の位置に移動
    heartNode.position = indexFingerPosition
    // アニメーションを定義
    let fadeIn = SCNAction.fadeIn(duration: 0.2)
    let up = SCNAction.move(by: SCNVector3(x: 0, y: 0.1, z: 0), duration: 0.1)
    let shakeHalfRight = SCNAction.rotate(by: -0.3, around: SCNVector3(x: 0, y: 0, z: 1), duration: 0.025)
    let shakeLeft = SCNAction.rotate(by: 0.6, around: SCNVector3(x: 0, y: 0, z: 1), duration: 0.05)
    let shakeRight = SCNAction.rotate(by: -0.6, around: SCNVector3(x: 0, y: 0, z: 1), duration: 0.05)
    let shakeHalfLeft = SCNAction.rotate(by: 0.3, around: SCNVector3(x: 0, y: 0, z: 1), duration: 0.025)
    let shake = SCNAction.sequence([shakeLeft,shakeRight])
    let fadeOut = SCNAction.fadeOut(duration: 1)
    let shakeRepeat = SCNAction.sequence([shakeHalfRight,shake,shake,shake,shake,shakeHalfLeft])
    let switchEffectAppearing = SCNAction.run { node in
        // エフェクトのフラグをoffにしておく
        self.isEffectAppearing = false
    }
    // アニメーションを実行
    heartNode.runAction(.sequence([fadeIn,up,shakeRepeat,fadeOut,switchEffectAppearing]))
}

【キュンです】

このデモのGitHubリポジトリ(Xcodeプロジェクト): HandPoseClassificationAR

#####引用/参考文献:

手のポイントの取得について:
VisionでHand Pose Detection 手のトラッキング: MLBoyだいすけ

2次元座標の3次元へのマッピングはKBoy様のコードを使わせていただきました:
ARKitのための3D数学: Kei Fujikawa

🐣


フリーランスエンジニアです。
お仕事のご相談こちらまでお気軽に🐥
rockyshikoku@gmail.com

Core MLやARKitを使ったアプリを作っています。
機械学習/AR関連の情報を発信しています。

GitHub
Twitter
Medium

14
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
14
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?