はじめに
この記事ではARKitを使ってフロントカメラからDepthDataを取得し、1pixelずつの深度情報を取得する方法を備忘録的にまとめます。
もしかしたらフロントカメラ以外にも使えるかもと思い、タイトルは少し広い内容をカバーできるようにしました。
いろんな事情でiPhone11で顔認識をした時に深度情報がfloatで欲しいと思い、いろいろ調べたんですが、一つにまとまった記事が存在しなかったので、深夜テンションで記事を作ろうと思いました。
(筆者はSwiftを勉強しはじめて数ヶ月のド素人なので、コードや文章の書き方が拙いかもしれませんがお許しください...)
(改善点や技術的補足がある人は大歓迎です)
対象となる人
- ARKitで深度情報が欲しくなった人
- (TrueDepth搭載のiPhoneのフロントカメラを使用する人)
- ポインタという概念がちょこっとだけ分かる人
筆者の環境
- Xcode11.6
- Swift
- iPhone11
- iOS13.5
要約
- CVPixelBufferをコネコネすればできる
- UnsafeMutableRawPointerからUnsafeMutablePointerに変換する
- UnsafeBufferPointerに変換してからArrayに変換する
- 欲しいピクセルの深度を1次元配列から取得すれば深度情報が得られる
コード概要
とりあえずコードを初めに見せてから要点を解説していきます。
分からない人はsessionメソッドの部分に中身を丸コピしてもいいかもしれません。
func session(_ session: ARSession, didUpdate frame: ARFrame) {
// nilチェック
guard let depthData = frame.capturedDepthData else { return }
let depthMap = depthData.depthDataMap
// depthMapのCPU配置(?)
CVPixelBufferLockBaseAddress(depthMap, .readOnly)
let base = CVPixelBufferGetBaseAddress(depthMap) // 先頭ポインタの取得
let width = CVPixelBufferGetWidth(depthMap) // 横幅の取得
let height = CVPixelBufferGetHeight(depthMap) // 縦幅の取得
// UnsafeMutableRawPointer -> UnsafeMutablePointer<Float32>
let bindPtr = base?.bindMemory(to: Float32.self, capacity: width * height)
// UnsafeMutablePointer -> UnsafeBufferPointer<Float32>
let bufPtr = UnsafeBufferPointer(start: bindPtr, count: width * height)
// UnsafeBufferPointer<Float32> -> Array<Float32>
let depthArray = Array(bufPtr)
// depthMapのCPU解放(?)
CVPixelBufferUnlockBaseAddress(depthMap, .readOnly)
let fixedArray = depthArray.map({ $0.isNaN ? 0 : $0 })
print(fixedArray[width*200+400]) //(400,200)に対応する深度値
}
では解説に入っていきます。
CVPixelBufferについて
ARKitから深度情報を取得しようとした人はおそらく、ARFrameのメンバ変数であるcapturedDepthDataに目が付き、その中にあるdepthDataMapまではたどり着いたと思います。
しかしこのdepthDataMapのクラスはCVPixelBufferで、それからどうやって値を取得するか分かりにくいかと思います。
調べたところによると、CVPixelBufferはGPUのメモリ上にデータが置かれているため、そう簡単に値を取得することができないそうです。
いろいろな処理をするためにはCPUが管理できるメモリに配置される必要があります。
CVPixelBufferはCIImageに変換でき、そこからUIImage等に変換できるため、DepthMapの描画や、グレースケール画像としての画素値を取得することができたとは思います。
しかし、depthDataMap(AVDepthData)のリファレンスによると
A depth map describes at each pixel the distance to an object, in meters.
(デプスマップは、各ピクセルでのオブジェクトまでの距離をメートル単位で表します。)
とあるので、何かしらの方法を使えばメートル単位での深度情報が取得できることがわかります。距離が取れるのであれば、グレースケール画像の画素値では情報が削られてしまうのであまり好ましい方法ではありません。
そこで、CVPixelBufferをCPUで扱う関数が登場します。
CVPixelBufferLockBaseAddress
CVPixelBufferLockBaseAddress関数は、CVPixelBufferの値をGPUからCPUが扱えるメモリへ移行してくれるのです。これにより、GPUによりプログラムで扱えなかったデータが扱えるようになります。
(この感覚だと思っていますが確証はありません。そもそもGPU管理だと扱えないのかどうかすら怪しいので詳しい人がいたら教えて欲しいです)
そして、一連の作業が終了したあとはCVPixelBufferUnlockBaseAddressでGPUに値を返してあげます。
(この行為に意味があるかは分かってないです)
CPUで扱えるようになることで、CVPixelBufferGetBaseAddressを使えば先頭ポインタを取得することができます。
...ポインタ?
Swiftのポインタについて
Swiftでもポインタは避けて通れません。むしろObjective-cの名残も含め、C言語と親和性の高い作りになっていることが実感できました...
本題に戻ります。
UnsafeMutableRawPointer
CVPixelBufferGetAddressから取得できるのは、UnsafeMutableRawPointerという型のポインタです。
この型は、「変更ができない型なしのポインタ」という意味です。constでvoid的な感じです。変更できないのは参照する値が変更できないということだと認識しています。
このUnsafeMutableRawPointerから深度情報を取得するためには、まずデータ型の情報を与えてあげなければなりません。それがUnsafeMutablePointerになります。
###UnsafeMutablePointer<T>
UnsafeMutableRawPointerと見間違えそうですが、Rawがあるかどうかがポイントです。
UnsafeMutablePointerは型があるポインタです。型を指定するので、深度情報がより取得できそうです。
深度情報はFloat32で保存されています。のでFloat32の型になるよう指定します。
(このことはCVPixelBufferGetPixelFormatTypeを使って調べることができます)
UnsafeMutableRawPointerからUnsafeMutablePointerに変換するにはbindMemoryを使いました。(すいません仕様はよく分かってません)
引数capacity
は要素数だと思い、縦幅x横幅を指定しました。
実はこの状態からも値は取得できるのですが、配列にしておいた方が何かと都合が良さそうだと思い、Arrayを目指します。
###UnsafeBufferPointer<T>
UnsafeBufferPointerはいうなれば配列ポインタです。これを経由することで配列に変換することができます。
UnsafeBufferPointerへの変換はinitする形でOKです。UnsafeBufferPointerはcountをメンバ変数として持っているので、initでまた要素数を指定してあげる必要があります。(無駄を感じるのでもう少し簡単にできそうですが...)
最後に、Array()を使うことで、最終的にCVPixelBufferからArrayに変換することができます。お疲れ様です。
##DepthDataMapの値を読み取る
無事配列にすることができたのですが、1次元配列なので、欲しいピクセルの深度値を取得するためには多少の計算をする必要があります。
index = width * y + x
xとyは欲しい座標で、widthは画像の横幅、indexが配列で指定すべきインデックスとなります。
配列は左上から横に値を取得していったような形式になると思います。
depthDataMapにおける無効値はNaNで表現されます。上記のプログラムでは無効値を0に置き換え、すべて有効な数字として扱えるようにしています。必須ではないので書かなくても大丈夫です。
そして!重要なことが1つ!
これはフロントカメラに言えることなのですが、取得した深度画像は、本来の向きから90度左を向いた状態で取得することになります。なので、カメラの解像度が480x640の縦長の画像だとしたら、Arrayで取得できるのは、640x480の横長の画像として取得することになります。
画像の中心の深度値を取得したい場合は、(240,320)ではなく、(320,240)を取得する必要があることにご注意ください。
(分かる人はそもそもの向きを修正することができると思いますが...)
まとめ
- DepthDataから深度値は取れる!
- CVPixelBufferめんどい
- ポインタしんどい
- フロントカメラの向きがヤバイ
最後に
今回は深度情報を1次元配列にしましたが、おそらく2次元配列にする方法はあると思います。
すいませんそこまでやる方法が思いつかなかったので、何かやり方があれば教えてください🙇♂️
ちなみに、iOS14からAVDepthDataの代わりにARDepthDataというものが増えるそうです。
でもARDepthDataのメンバ変数であるdepthDataMapはCVPixelBufferクラスなので、結局この記事が参考になるかもしれません。
ここまで読んでいただきありがとうございました。