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のカメラでライブプレビューもできます。
#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関連の情報を発信しています。