ここから始まるお手軽地形計測 iPhoneへLiDARスキャナ搭載【ARKit】

f:id:aptpod_tech-writer:20201221150954j:plain

aptpod Advent Calendar 2020 22日目の記事です。担当は製品開発グループの上野と申します。一昨年昨年と引き続きとなりまして今年もiOSの記事を書かせていただきます。

はじめに

皆さんはつい先日発売されたばかりのiPhone 12は購入されましたか? 私個人としてはiPhone12 miniを購入したのですがiPhone SEの第1世代を彷彿とさせる角ばったデザインと小ささが良いですね、指紋認証が無いのが痛い所ですが...

それはさておき、その中で発売されたiPhone 12 ProシリーズにはLiDARスキャナと呼ばれる物が搭載されました。 今回はそちらについて検証を行い分かったこと、視えてきたことについてお話ししたいと思います。

LiDARとは

そもそもLiDARとは、Light Detection and Rangingの略で光を物体へ照射し距離や性質を分析する為の技術です。
今回のiPhoneからLiDARスキャナが搭載されましたが、今年発売されたiPad Pro 2020にも搭載されていたりします。

f:id:aptpod_tech-writer:20201215125044p:plain
LiDARスキャナiPadPro2020搭載
出典:Apple

LiDARスキャナには ToF(Time-of-Flight) と呼ばれるセンサ(※もしくはカメラ)が搭載されています。 ToFセンサの物体との距離を求める方法にもいくつか存在し、物体に光を照射し反射して戻ってくるまでの時間で距離を計算するdToF(Direct ToF)方式と反射した光の位相差から距離を求めるiToF(Indirect ToF)方式がありiPhone/iPadでは引用した画像にもあるようにdToF方式が採用されているようですね。
このdToF方式の場合iToF方式より屋外での計測や長距離の側位が強く、より安定した方式を採用していると考えられます。
また現状iPhone/iPadでは最大の5mの奥行きしか計測できないとも書かれています。 iPhone/iPadのToFセンサの情報はかなりニュースとして上がっているので興味があれば調べてみてください。

LiDARスキャナが搭載される前との精度の違い

iPhoneにLiDARスキャナが搭載された事によって大きく変わった点はより高速に、より正確に地形を把握する事ができる様になったことだと思います。 今までバックカメラでのワールドトラッキング(地形把握)方法はモーションセンサとカメラ画像から物体との位置関係を算出する方法でした。 具体的には計測を開始した位置から端末を動かしてその差分からどの程度の距離関係があるかと言った方法を取る必要がありました。 しかし上記の方法では凸凹としない平面の物体の検出しかできず正確な奥行きの検出はできませんでした。

f:id:aptpod_tech-writer:20201215130112j:plain
iPhone 12 Pro(LiDARスキャナ搭載端末)の計測アプリキャプチャ

f:id:aptpod_tech-writer:20201215130146j:plain
iPhone 12 mini(LiDARスキャナ未搭載端末)の計測アプリキャプチャ

f:id:aptpod_tech-writer:20201215130208j:plain
実際の長さ

実際に標準の計測アプリを使って奥行きを測定し、LiDARスキャナ搭載のiPhone 12 Proと未搭載端末のiPhone 12 miniの結果を比較すると格段に精度が上がっていることがわかります。 ※フロントカメラにはTrueDepthカメラと呼ばれる搭載された赤外線カメラや環境光センサ等をまとめて総称したカメラがFaceID導入当時から搭載されており、実はこちらを利用しても精度も高かったりします。

LiDARスキャナのデータに触れてみる

ここからは実装をしながらお話ししたいと思います。

確認環境

  • Xcode 12.2 (12B45b)
  • Swift version: 5.3.1
  • iPhone 12 Pro OS: 14.2.1

ひとまず現在(※執筆日2020/12)のARKitを利用したプロジェクトを作成してみます。

f:id:aptpod_tech-writer:20201215131833p:plain
Augmented Reality Appでプロジェクト作成
f:id:aptpod_tech-writer:20201215132039p:plain
Content TechnologyはRealityKit
プロジェクトテンプレートはAugmented Reality App、Content TechnologyはRealityKit を選んでください。
f:id:aptpod_tech-writer:20201215132416p:plain
ARAppテンプレートのViewController
このプロジェクトテンプレートは開発者にとってとても優しい作りになっており、カメラを利用する為のInfo.plistへのプライバシーの記述や、ARViewの自動設置、3D空間上のホームポジションへのボックスのデモ配置等を行ってくれます。

// ViewController.swift
        ...
        // Add the box anchor to the scene
        arView.scene.anchors.append(boxAnchor)
        
        // オクルージョンを有効化
        arView.environment.sceneUnderstanding.options.insert(.occlusion)
        // メッシュ表示の有効化
        arView.debugOptions.insert(.showSceneUnderstanding)
    }

プロジェクトテンプレートに2行コードを追記した物を実行して動画撮影したものです。 あっという間に撮影しているオフィスのメッシュデータを収集し可視化してくれています、すごい。

f:id:aptpod_tech-writer:20201215134716p:plain
LiDARスキャナでメッシュ情報スキャン

赤、緑、青と順番にメッシュの色分けをしておりiPhoneからの実際の距離が近ければ赤、遠ければ青と表現しているようですね。

f:id:aptpod_tech-writer:20201215141712p:plain
オブジェクトオクルージョン

3D仮想空間上に設置したボックスに対してオクルージョン(今回の例だとMacの裏側にある仮想オブジェクトを隠す)も動作してくれています。
これらメッシュデータの可視化やオブジェクトオクルージョン機能はLiDARスキャナを搭載しているiPhone/iPadでなければ動作できません。

LiDARスキャナ使って点群を検出してみた

LiDARスキャナでできることの代表的な例は点群データの収集です。 サンプル例は検索していただければと思いますが基本的には3次元座標データ(XYZ)と色情報(RGB)の集合で表されます。

Appleでも点群の収集、可視化の為のサンプルコードを提供してくれています。 Sample Code - Visualizing a Point Cloud Using Scene Depth

このサンプルは可視化方法にMetalKitを利用しており、少しGPUを利用したレンダリングの知識が必要です。Metalの使い方が全然わからないという方は昨年のアドベントカレンダーでデモアプリを作りながら解説した記事があるのでご覧ください。解説している内容のかなり発展した内容がこの点群可視化サンプルには実装されていると思います。

f:id:aptpod_tech-writer:20201215154458p:plain
Apple提供、点群可視化デモ

特に変更を加える事なく実行してみました。
点群が可視化されてますね、ですが少し見えづらいので中のソースコードを少し書き換えます。

// Renderer.swift
...
final class Renderer {
    ...
    // Number of sample points on the grid
    private let numGridPoints = 10000
    ...

f:id:aptpod_tech-writer:20201215154738p:plain
Apple点群可視化デモ修正ver

かなり鮮明に点群がみえるようになりました。
上記で書き換えた内容は3D空間上のカメラの視界に対して、どれだけの点を描画するかのパラメータになります。よって取得した情報の可視化には可視化するアプリケーション、ビジュアライザ側も工夫が必要な事が分かります。

LiDARスキャナによる地形計測の為に

ここまでの説明でLiDARスキャナが搭載されたiPhoneを利用すれば点群の可視化や撮影した空間の認識ができるので、今回のお題にもある地形計測がなんとなくできる気がしてくると思います。 ここからは地形計測を実用的にするための方法を考えお話していきたいと思います。

地形計測を実用的にするためにはiPhone内で完結する事なく算出した点群情報のファイルへの出力やクラウドストレージへのデータストアが必須です。
弊社ではintdashという取得したデータをサーバへ伝送し保存できるプラットフォームや、伝送されたデータをリアルタイムに可視化できるアプリケーションを提供しています。
intdashを用いればクラウドストレージへのデータストアも行えるので今回のゴールとしてはiPhoneで取得したデータをサーバへ伝送し、サーバ経由で取得したデータを別アプリケーションで可視化できる事をゴールとしたいと思います。

算出した点群データを伝送する

取り敢えず点群可視化のサンプルで算出した点群データをサーバへ伝送し、可視化してみました。
※かなりのボリュームになってしまうのでPCで可視化している方法は今回は解説しません。
ARKitで表現されている座標系は右手座標系なので一旦そこに注意とだけ言っておきます。

f:id:aptpod_tech-writer:20201216141811p:plain
シンプルに点郡を伝送した場合のビットレート

動画ではサンプルアプリで少し書き換えた1フレーム当たりの点の数を1万のままXYZの座標と色情報を秒間1フレーム間隔で送っており、ビットレートは約3.5Mbpsです。
ARSessionのリフレッシュレートは60Hzですので単純計算で約210Mbpsの全フレームをリアルタイムに伝送しきる事はかなり難しいと言えます。
また、同じ位置の点がかなり存在しているのであまり効率の良い方法とは言えないと思います。

取得した画像データを伝送する

点群データをそのままリアルタイムに送り続けるのは難しい事がわかりましたので他の方法を考えてみます。点群可視化のサンプルコードを修正した物の動画を撮影しました。

サンプルコードの点群の算出にはARSessionから取得したARFrameから取得できるカメラ情報(ARCamera)と深度マップ(depthMap)、信頼度マップ(confidenceMap)、カメラ映像(capturedImage)を用いて行っています。

動画では点群、深度マップ、信頼度マップ、カメラ映像の順番で可視化しており点群以外の画像データはCVPixelBufferという画像の縦横のサイズやカラーフォーマット、データバッファ等を含んだクラスで構成されています。
と言う事はこれら全ては画像としての出力が可能なのでiOSで一般的に使われている画像データクラスのUIImageに変換し表示させてみました。
UIImageへの変換方法ですが調べてもあまり出てこなかったので比較的簡単な方法で変換する方法も共有します。

f:id:aptpod_tech-writer:20201216153151p:plain
深度マップを可視化した例

深度マップの可視化例です。

// 深度マップのUIImage化サンプル
import ARKit
import UIKit

extension ARFrame {
    var depthMapImage: UIImage? {
        guard let pixelBuffer = self.sceneDepth?.depthMap else { return nil }
        let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
        let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent)
        guard let image = cgImage else { return nil }
        return UIImage(cgImage: image)
    }
}

...

func update(frame: ARFrame) {
    imageView.image = frame.depthMapImage
}

深度マップはFloat32の単色で取得でき、特に設定を変えていない状況でbytesPerRow1024バイトの幅256ピクセル、高さ192ピクセルでした。 距離が近ければ0に近い値を出力し、遠ければ4.0以上の小数も生成していました。

この値が現実世界の空間上のメートル、奥行きの値として扱われるわけですね。

f:id:aptpod_tech-writer:20201216160059p:plain
信頼度マップを可視化した例

信頼度マップの可視化例です。信頼度マップは深度マップと同じピクセルサイズでUInt8の単色で取得できますが深度マップの様にそのままUIImage化しても黒い画像で表示されてしまって可視化できたとは言えません。

// 信頼度マップのUIImage化サンプル
import ARKit
import UIKit

extension ARFrame {
    var confidenceMapImage: UIImage? {
        guard let pixelBuffer = self.sceneDepth?.confidenceMap else { return nil }
        // 0 ~ 2 -> 0 ~ 255
        let lockFlags: CVPixelBufferLockFlags = CVPixelBufferLockFlags(rawValue: 0)
        CVPixelBufferLockBaseAddress(pixelBuffer, lockFlags)
        guard let rawBuffer = CVPixelBufferGetBaseAddress(pixelBuffer) else { return nil }
        let height = CVPixelBufferGetHeight(pixelBuffer)
        let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
        let len = bytesPerRow*height
        let stride = MemoryLayout<UInt8>.stride
        var i = 0
        while i < len {
            let data = rawBuffer.load(fromByteOffset: i, as: UInt8.self)
            let v = UInt8(ceil(Float(data) / Float(ARConfidenceLevel.high.rawValue) * 255))
            rawBuffer.storeBytes(of: v, toByteOffset: i, as: UInt8.self)
            i += stride
        }
        CVPixelBufferUnlockBaseAddress(pixelBuffer, lockFlags)
        let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
        let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent)
        guard let image = cgImage else { return nil }
        return UIImage(cgImage: image)
    }
}

...

func update(frame: ARFrame) {
    imageView.image = frame.confidenceMapImage
}

信頼度マップのデータはARConfidenceLevelという列挙型で定義された範囲で出力され、最小値はARConfidenceLevel.lowの0、最大値がARConfidenceLevel.highの2です。 ですのでモノクロ画像として表示でよければ場合は0~255の範囲に変換してからUIImage化する必要があります。

その変換例が上記のサンプルとなります。

f:id:aptpod_tech-writer:20201216161040p:plain
カメラ画像の可視化例

// カメラ画像のUIImage化サンプル
import ARKit
import UIKit
import VideoToolbox

extension CVPixelBuffer {
    
    var image: UIImage? {
        var cgImage: CGImage?
        VTCreateCGImageFromCVPixelBuffer(self, options: nil, imageOut: &cgImage)
        guard let image = cgImage else { return nil }
        return UIImage.init(cgImage: image)
    }
}

...

func update(frame: ARFrame) {
    imageView.image = frame.capturedImage.image
}

最後にカメラ画像の可視化例です、特に変則的な処理はしていません。
こちらも特に設定を変えていない状況で出力されていたピクセルサイズは幅1920ピクセル、高さ1440ピクセルでした。

ここまで説明した画像データ+カメラの位置情報があれば点群の算出を行う事ができるのでこの4つのデータを伝送し、他の可視化アプリケーションで取得すれば大量の算出した点群データを送る事なく可視化できますね。 また画像として取得できるのであれば弊社でも取り扱っているH.264等の動画圧縮形式にする事ができますので伝送量はかなり抑えることも可能でしょう。

因みに自動車の自動運転にもLiDARのデータというのは利用されています。自動運転のサポートにとても便利と言うわけではないですが、深度マップ形式でデータを伝送しておけば物体検知の様な地形計測以外の用途にも利用できるので活用範囲が広がりますね。

最適化されたメッシュデータを伝送する

先ほどまでの方法は汎用的に扱う為の伝送方法で可視化するビジュアライザで点群の算出や、不要な点を削除するフィルタ等が必要でして実用までの敷居が高いと言う事になりかねません。

f:id:aptpod_tech-writer:20201215134716p:plain
LiDARスキャナでメッシュ情報スキャン

なので他に取れる方法ですが、実は一番始めにプロジェクトテンプレートから触ってみて確認したARViewのデバッグオプションとして表示しているメッシュデータも参照可能です。
このメッシュデータは常に最適化された頂点情報を出力しているのでこのデータを時系列データとして伝送する事ができればかなり楽に扱えますね。

上記が最適化されたメッシュデータ送ってみたデモ動画です。 この場合、色情報は送信できませんがかなり軽量なデータ量でかつ、すぐに点群として表示はもちろんポリゴンメッシュとしても表示できます。

f:id:aptpod_tech-writer:20201216203037j:plain
取得した点の数

動画では撮り忘れましたが点の数は動画で移動した範囲だと63596点でした。

最適化されたメッシュデータの取得方法

ではメッシュデータの取得方法も共有します。

class ViewController: UIViewController, ARSessionDelegate {
    ...
    func setupARView() {
        // ARSessionDelegate
        arView.session.delegate = self
    }
    ...
    //MARK:- ARSessionDelegate
    func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
        NSLog("session didUpdate anchors: \(anchors.count) - ARSessionDelegate")
    }
    
    func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
        NSLog("session didAdd anchors: \(anchors.count) - ARSessionDelegate")
    }
    
    func session(_ session: ARSession, didRemove anchors: [ARAnchor]) {
        NSLog("session didRemove anchors: \(anchors.count) - ARSessionDelegate")
    }
}

基本的にはARSessionのDelegateからARAnchorの配列情報を取得する所から始まります。

func sendPointCloud(camera: ARCamera, anchors: [ARAnchor]) {
    // メッシュデータの取得
    var meshAnchors = anchors.compactMap({ $0 as? ARMeshAnchor })
    for mesh in meshAnchors {
        // ARMeshGeometryの取得
        let geometry = mesh.geometry
        ...
        // 頂点情報の取得
        let vertices = geometry.vertices
        for i in 0..<vertices.count {
            let vertexPointer = vertices.buffer.contents().advanced(by: vertices.offset + (vertices.stride * i))
            let vertex = vertexPointer.assumingMemoryBound(to: (simd_float3).self).pointee
            ...
        }
        ...
    }
}

ARAnchorの配列にはARMeshAnchorというARAnchorの継承クラスが含まれておりこの中に最適化されたメッシュデータが格納されています。 ARMeshAnchorに格納されている頂点群はある一定の3次元範囲毎にまとめられているので多すぎない頂点数で扱う事ができるのでかなり扱いやすい様になっています。 取得できるデータはARMeshGeometryをご覧ください。

今回この後の送信方法は深くは語りませんがARAnchorのidentifierでARMeshAnchorを一意のデータとして扱えるので、必要なタイミングで必要なデータを送る事が望ましいと思います。

さいごに

ここまでで私が調べて共有する地形計測を行うため方法の解説は以上です。
今回はiPhoneにLiDARスキャナが搭載されその能力を活用する為にはどうしたらよいかと言う面で調査を行いました。
近年ARクラウドという言葉が広がり始めており、5Gの普及やスマートフォン、PCの処理能力向上によりリアルタイムに扱う事ができるデータ量もかなり増えてきていると言えます。誰でも手軽に手に入るスマートフォンここまでできるとなるとイノベーションが劇的に進みそうですね。

個人的にはロールプレイングゲームでよくあるような個人間のリアルワールドマッピングアプリなんて出たら面白そうとか思っています。

以上、ご覧いただきありがとうございました。