ARSCNViewビデオフィードのビジョンフレームワークによって検出されたオブジェクトの3D座標の抽出

この記事は KINTOテクノロジーズアドベントカレンダー2024 の21日目の記事です🎅🎄
はじめに
こんにちは!iOSエンジニアのViacheslavです。
今年、私は当社のUnlimitedアプリの新機能である「これなにガイド」に取り組む機会を得ました。これなにガイドは、車のダッシュボードをスマホでスキャンすることで、車のボタンやスイッチの上に仮想マーカーを表示できる拡張現実(AR)マニュアルです。特定のボタンに対応するマーカーを選択すると、機能を確認できるマニュアルページにアクセスできます。
今日は、この機能の開発中に遭遇した課題の1つである、「画面上でVisionフレームワークによって認識された物理オブジェクトの座標を正確にキャプチャし、それらの座標をARシーン内の3D座標に変換する」という課題に対する、短くてシンプルな解決策を共有したいと思います。
最初は些細な作業に思えたものが、予想以上に複雑であることが判明しました。いくつかのアプローチを模索し、多くの手動計算を実行した後、ようやく、単純かつ驚くほど簡潔な解決策にたどり着きました。ARKit と CoreML の統合に関する情報は比較的少ないので、始めたときに知っていればよかったと思います。では、知識ベースに追加していきましょう!
いくつかの前提条件
実際のコードに進む前に、作業する環境を明確に定義してみましょう。
-
ARSCNView
これは、デバイスのカメラからのビデオフィードを表示し、現実世界をキャプチャして3Dオブジェクトを「ブレンド」させてARエクスペリエンスを実現するビューです。ARSCNView
はAppleのARKitの一部で、ARシーンでの3Dオブジェクトのレンダリングを処理するSceneKit上に構築されます。 -
Core ML オブジェクト検出モデル
オブジェクトの座標を決定する前に、まずデバイスのカメラによって提供されるビデオ フィード フレーム内でオブジェクトを認識する必要があります。Visionフレームワークは、この目的のためにCore MLオブジェクト検出モデルを利用します。本記事では、読者がすでに使用できるモデルを持っているものと想定します。そうでない場合は、YOLOv3-Tiny など、ダウンロード可能な事前訓練済みモデルが多数あります。こちらから入手できます。
最低限のソリューションに必要なのはこれだけです。ARSCNView
からビデオフレームをキャプチャし、Core MLモデルを使用してARSCNView
ビューポート内のオブジェクトの位置を検出し、「ヒットテスト」と呼ばれる手法を適用して3D AR空間におけるオブジェクトの座標を決定します。
ARSCNViewにおいて認識されたオブジェクトの座標をキャプチャする
Vision を使用して認識要求を実行する場合の一般的な設定は以下の通りです。
Core MLモデルと VNCoreMLRequest
を初期化して、そのモデルを使用して認識を処理します。
let vnModel = try!VNCoreMLModel(for: myCoreMLModel)
let vnRequest = VNCoreMLRequest(model: vnModel) { [weak self] request, _ in
guard let observations = request.results else { return }
// Observations handling
}
request.imageCropAndScaleOption = .centerCrop
次に、vnRequest
への参照を適切な場所に保存し、次の引数セットで実行できるように準備します。引数のタイプは、ビデオフィードフレームをどこからキャプチャするかによって異なります。
私たちのシナリオでは、ARSCNView
からフレームを渡し、ARSessionDelegate
の session(_:didUpdate:)
メソッドにおいてフレームをキャプチャする必要があります。このデリゲートメソッドは、ARSCNView
で表示できる新しいフレームが利用可能になるたびに呼び出されます。
func session(_ session:ARSession, didUpdate frame:ARFrame) {
guard let vnRequest else { return } // 1
let options: [VNImageOption:Any] = [.cameraIntrinsics: frame.camera.intrinsics] // 2
let requestHandler = VNImageRequestHandler(
cvPixelBuffer: frame.capturedImage, // 3
orientation: .downMirrored, // 4
options: options
)
try? requestHandler.perform([vnRequest]) // 5
}
コードを分解する:
VNCoreMLRequest
の参照:新しいフレームを受信すると、先ほど初期化したリクエストを実行する準備が整います。- カメラ内部パラメータ:
frame.camera.intrinsics
は、Visionがシーンの幾何学的特性を解釈するのに役立つカメラ較正データを提供します。 - 画像入力:
VNImageRequestHandler
は、ARフレームから取得された生画像データをCVPixelBuffer
として受け入れます。 - 画像の向き:
.downMirrored
方向は、Visionのデフォルト方向と比較して、カメラフィードの座標系の反転を考慮します。 - リクエストの実行:準備されたリクエストは、リクエストハンドラーを使用して実行されます。
Vision にフレームを渡し始めると、オブジェクト検出の結果が VNCoreMLRequest
完了ハンドラー内のVNRecognizedObjectObservation
オブジェクトの配列として返されます。これらの結果を信頼度レベルでフィルタリングしたり、その他の処理を実行したりすることもできますが、今日は認識された特定のオブジェクトの座標を抽出することに焦点を当てます。
境界ボックス座標の抽出
最初は、VNRecognizedObjectObservation
に boundingBox
プロパティ(認識されたオブジェクトを囲む CGRect
)があるため、これは簡単に思えるかもしれません。ただし、いくつかの複雑な問題があります。
boundingBox
は、物体認識モデルの入力画像に対する正規化された座標系(座標値は0から1の間)であり、それもY軸が反転しています。- カメラフィード、Core MLモデル入力、そして
ARSCNView
ビューポートのサイズとアスペクト比はそれぞれ異なります。
つまり、boundingBox
を ARSCNView
ビューポートの座標系に変換するには、一連の座標変換と再スケーリングの手順が必要になります。これらの変換を手動で行うのは面倒で、間違いが起こりやすくなります。幸いなことに、CGAffineTransform
を使用すると、これを処理できる非常に簡単な方法があります。方法は次のとおりです。
let sceneView:ARSCNView
func getDetectionCenter(from observation:VNRecognizedObjectObservation) -> CGRect? {
guard let currentFrame = sceneView.session.currentFrame else { return nil }
let viewportSize = sceneView.frame.size
// 1
let fromCameraImageToViewTransform = currentFrame.displayTransform(for: .portrait, viewportSize: viewportSize)
let viewNormalizedBoundingBox = observation.boundingBox.applying(fromCameraImageToViewTransform)
// 2
let scaleTransform = CGAffineTransform(scaleX: viewportSize.width, y: viewportSize.height)
let viewBoundingBox = viewNormalizedBoundingBox.applying(scaleTransform)
return viewBoundingBox
}
説明:
- ビュー座標への変換:
displayTransform(for:viewportSize:)
を使用して、検出された境界ボックスは、入力画像の正規化座標系からARSCNView
の正規化座標系に変換されます。 - ピクセル寸法へのスケーリング:正規化された境界ボックスは、
ARSCNView
ビューポートのサイズに合わせてスケーリングされ、画面のピクセル寸法での境界ボックスが生成されます。
以上です!これで、ARSCNView
ビューポートの座標系に境界ボックスが作成されました。
3番目の座標を取得する
私は、ARシーンの3D座標空間内で認識されたオブジェクトの座標を取得すると約束しました。
そのためには、「ヒットテスト」と呼ばれる手法を利用します。これにより、ビューポート内の任意のポイントで最も近い物理オブジェクトまでの距離を測定できます。この手法は、デバイスからビューポート内の選択したポイントにある物理オブジェクトとの最初の交点まで直線の光線を投影し、その光線の長さを測定するものと考えることができます。この機能は ARKit の一部であり、非常に使いやすいです。
先ほど検出したオブジェクトの知覚可能な中心の3D座標を見つける方法は以下の通りです。
func performHitTestInCenter(of boundingBox:CGRect) -> SCNVector3? {
let center = CGPoint(x: boundingBox.midX, y: boundingBox.midY) // 1
return performHitTest(at: center)
}
func performHitTest(at location:CGPoint) -> SCNVector3? {
guard let query = sceneView.raycastQuery( // 2
from: location,
allowing: .estimatedPlane, // 3
alignment: .any // 4
) else {
return nil
}
guard let result = sceneView.session.raycast(query).first else { return nil } // 5
let translation = result.worldTransform.columns.3 // 6
return .init(x: translation.x, y: translation.y, z: translation.z)
}
説明:
- ここでは、ヒットテストを実行するために1つのポイントが必要なため、前の手順から境界ボックスの中心を計算します。
- 指定された2Dポイントから始まるレイキャストクエリを作成します。
- ヒットテストで、ARKitが推定することしかできない非平面の表面や平面を考慮できるようにします。
- 水平面と垂直面の両方のヒットテストを有効にします(既定値は水平面のみです)。
- ARセッションを使用してレイキャストクエリを実行します。交差がない場合は
nil
を返します。 - 各
ARRaycastResult
には、ワールド空間で検出されたポイントの3D変換を表す4x4行列であるworldTransform
が含まれます。columns.3
には、交点の3D位置を指定する並進ベクトルが含まれています。この並進ベクトルはSCNVector3
として返され、ARKit/SceneKitはこれを使用して3D位置を表します。
完了しました!これで、Visionによって検出されたオブジェクトの 3D座標を取得できました。目的に合わせてご利用ください。:)
最後に
Unlimitedアプリでは、これらの3D座標を使用して、車内にARマニュアルマーカーを表示します。もちろん、ユーザーエクスペリエンスをよりスムーズにし、マーカーの位置をより安定させるために私たちが採用している追加テクニックは数多くありますが、これはコアテクニックの1つです。
とはいえ、この方法は、考えられる他のあらゆる目的にも使用できます。お役に立てたら幸いです。
最後に、私たちのテストプロセスと、オブジェクト検出後にARマニュアルマーカーがどのように表示されるかについて少し紹介します。
今日はここまでです。読んでいただきありがとうございました!
楽しいクリスマスをお過ごしください。そして、幸せな新年をお迎えください!
関連記事 | Related Posts

ARSCNViewビデオフィードのビジョンフレームワークによって検出されたオブジェクトの3D座標の抽出

Migrating to MapKit: A New Chapter for Unlimited App

Reading Vehicle Inspection QR Codes
![Cover Image for [Event Report] Mobility Night #1 – Exploring GPS and Location-Based Technologies](/assets/common/thumbnail_default_×2.png)
[Event Report] Mobility Night #1 – Exploring GPS and Location-Based Technologies
![Cover Image for How to Receive Routes from the [iOS] Maps App in the "Route App"](/assets/common/thumbnail_default_×2.png)
How to Receive Routes from the [iOS] Maps App in the "Route App"

Credential Setup for Private Repositories in SwiftPM
We are hiring!
【データサイエンティスト】データサイエンスG/東京・名古屋・大阪
データ分析部について※データ分析部は、データサイエンスGが所属している部門です。クルマのサブスクというビジネスモデルを展開するKINTOでは、市場やお客様のニーズを捉え、最高の顧客体験を提供するために、マーケティング分析においても挑戦と創造が求められます。
生成AIエンジニア/AIファーストG/東京・名古屋・大阪・福岡
AIファーストGについて生成AIの活用を通じて、KINTO及びKINTOテクノロジーズへ事業貢献することをミッションに2024年1月に新設されたプロジェクトチームです。生成AI技術は生まれて日が浅く、その技術を業務活用する仕事には定説がありません。