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

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

**【キュンです】**

#１、データを集める

学習には画像データが必要です。
集めてフォルダ分けします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/868e6b50-25e9-0f1a-ae21-943fdbb1cc9b.png" width=200> 


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

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

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

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

HandPoseClassificationを選択します。
（HandPoseClassificationは、macOS Monterey / Xcode13以上の環境で使えます。）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/adea7a38-0b64-78ed-4e2a-0bdbcf6e94e2.png" width =500>

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

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/4e94a77e-581b-3696-3eba-8cbcacce5533.png" width =600>

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/a7098ce3-e11d-2b71-c937-a88c048d18f3.png" width =600>

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

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


Outputタブからモデルを入手します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/cc06a0a6-7a41-af84-59bc-6bb5448adbbb.png" width =500>

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

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

```swift
import CoreML
import Vision

...

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

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


```swift
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でモデルに入力・推論実行します。

```swift
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

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

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

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

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

```swift
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)
    
    // 指はカメラから２０cm奥にあると想定する
    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平面内の２次元の人差し指の位置と上記２０cm奥の位置をスクリーン平面に定義する
    var screenPos = arScnView.projectPoint(pointInWorld)
    screenPos.x = Float(deNormalizedIndexPoint.x)
    screenPos.y = Float(deNormalizedIndexPoint.y)
    // スクリーン平面の指の位置を３次元座標にマッピングする
    let finalPosition = arScnView.unprojectPoint(screenPos)
    return finalPosition
}
```

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

```swift
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 ](https://github.com/john-rocky/HandPoseClassificationAR)**

#####引用／参考文献：

手のポイントの取得について:
**[VisionでHand Pose Detection 手のトラッキング:　MLBoyだいすけ](https://qiita.com/john-rocky/items/29c2cf791051c7205302)**

２次元座標の３次元へのマッピングはKBoy様のコードを使わせていただきました：
**[ARKitのための3D数学:　Kei Fujikawa](https://qiita.com/kboy/items/775633fe3fd6da9c5fb6)**

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

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

[GitHub](https://github.com/john-rocky)
[Twitter](https://twitter.com/JackdeS11)
[Medium](https://medium.com/@rockyshikoku)