はじめに
以下の動画のような LiDAR を利用した iPhone用アプリを試しに作ってみた話です。
自分が普段使いしている iPhone 16 Pro の LiDAR を、自作のアプリで使ってみたいと以前から思っていたのですが、先送りにしたままでした。
それについて、ようやく今回、データ取得と RGBカメラとセットにした描画をシンプルに行ってみました。上記の動画のような、それらを 1画面で並べて表示する iOSアプリを作って、実機で動かしています。
今回のゴール
今回のゴールは以下の 2つです。
- Claude・ChatGPT を活用してとりあえず実機で動くものを作る
- とりあえず動く状態のものに関して、改善した方が良さそうなことを書き出す
今回やったこと
以下で、今回やったことを書いていきます。
Xcode での下準備(ざっくりとしたメモ)
まずは、Xcode でプロジェクトを作成して、設定などを進めます。以降は、ざっくりした内容のメモという感じのものになります。
プロジェクトの作成
Xcode で新規プロジェクトを作成します。iOS用アプリのプロジェクトを、以下の設定で作りました。
プロジェクトの設定
今回のアプリは横向きの画面用に作る想定です。そのため、以下の「TARGETS」の「General」タブにある「iPhone Orientation」を「Landscape Right」のみに制限しています。
また、今回のアプリで必須機能となるカメラについて、権限周りの設定も行います。具体的には、以下の「TARGETS」の「Info」タブで「Custom iOS Target Properties」の中で「Privacy - Camera Usage Description」を追加します。
今回のコード
今回、プロジェクトで「【プロダクト名】.swift」と「ContentView.swift」が準備されています。それらについて、「【プロダクト名】.swift」の側の処理は削除し、「ContentView.swift」のみに処理を一括で書く形で試しました。
ContentView.swift のコードは、以下のとおりです。今回のコーディングは、主に Claude を使っています。
import SwiftUI
import ARKit
import CoreImage
import Combine
@main
struct LiDARViewerApp: App {
var body: some Scene {
WindowGroup { ContentView() }
}
}
struct ContentView: View {
@StateObject private var manager = SessionManager()
var body: some View {
HStack(spacing: 2) {
pane(image: manager.depthImage, label: "LiDAR Depth")
pane(image: manager.rgbImage, label: "RGB")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.ignoresSafeArea()
.statusBarHidden(true)
.onAppear { manager.start() }
.onDisappear { manager.stop() }
}
@ViewBuilder
private func pane(image: UIImage?, label: String) -> some View {
ZStack(alignment: .topLeading) {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
Color.gray.opacity(0.2)
}
Text(label)
.font(.caption).bold()
.foregroundColor(.white)
.padding(6)
.background(.black.opacity(0.5))
.padding(8)
}
}
}
final class SessionManager: NSObject, ObservableObject, ARSessionDelegate {
@Published var rgbImage: UIImage?
@Published var depthImage: UIImage?
private let session = ARSession()
private let ciContext = CIContext()
private let processQueue = DispatchQueue(label: "frame.process")
private let maxDepth: CGFloat = 5.0 // 深度の可視化レンジ [m]
func start() {
guard ARWorldTrackingConfiguration.supportsFrameSemantics(.sceneDepth) else {
print("sceneDepth 非対応デバイスです")
return
}
let config = ARWorldTrackingConfiguration()
config.frameSemantics = .sceneDepth
session.delegate = self
session.delegateQueue = processQueue // 変換処理をメイン外で
session.run(config)
}
func stop() { session.pause() }
func session(_ session: ARSession, didUpdate frame: ARFrame) {
// --- RGB (capturedImage は横向き右がネイティブ向き) ---
let rgbCI = CIImage(cvPixelBuffer: frame.capturedImage)
let rgb = render(rgbCI)
// --- LiDAR 深度 (Float32, メートル値, 256x192) ---
var depth: UIImage? = nil
if let depthMap = frame.sceneDepth?.depthMap {
let ci = CIImage(cvPixelBuffer: depthMap)
.applyingFilter("CIColorMatrix", parameters: [
"inputRVector": CIVector(x: 1 / maxDepth, y: 0, z: 0, w: 0),
"inputGVector": CIVector(x: 0, y: 1 / maxDepth, z: 0, w: 0),
"inputBVector": CIVector(x: 0, y: 0, z: 1 / maxDepth, w: 0),
"inputAVector": CIVector(x: 0, y: 0, z: 0, w: 1),
"inputBiasVector": CIVector(x: 0, y: 0, z: 0, w: 0)
])
.applyingFilter("CIColorClamp") // 0..1 に制限
.applyingFilter("CIFalseColor", parameters: [
"inputColor0": CIColor(red: 0, green: 0, blue: 1), // 近い = 青
"inputColor1": CIColor(red: 1, green: 0, blue: 0) // 遠い = 赤
])
depth = render(ci)
}
DispatchQueue.main.async {
self.rgbImage = rgb
self.depthImage = depth
}
}
private func render(_ image: CIImage) -> UIImage? {
guard let cg = ciContext.createCGImage(image, from: image.extent) else { return nil }
return UIImage(cgImage: cg)
}
}
ちょっとしたエラーが出たりしつつ、それを解消していって、とりあえず動作するアプリができました。
処理の流れなど
冒頭で読み込んでいる「SwiftUI、ARKit、CoreImage」で処理している内容は、以下のとおりです。
- SwiftUI
- UI用
- ARKit
- カメラ画像・LiDAR深度情報の取得
- CoreImage
- ARKit の Pixel Buffer の画像への変換、深度画像への色付け
また処理の流れを以下で示します。
ARSession を開始
↓
ARKit がカメラフレームを取得
↓
ARFrame
├─ capturedImage カラーカメラ画像の CVPixelBuffer(ARKit からは YCbCr形式で取得し、Core Image で表示用RGB画像へ変換)
└─ sceneDepth.depthMap LiDAR深度マップ(ARKit が推定したシーン深度(Float32、メートル))
↓
Core Image で画像化・色変換
↓
UIImage へ変換
↓
@Published を更新
↓
SwiftUI の画面を再描画
この記事で出てくる sceneDepth は、LiDAR を利用して ARKit が生成した深度マップであり、LiDARセンサーの生データや点群を直接取得しているものではありません。
実機で動作させる
あとは、iPhone を Mac に接続し、実機にビルドしたアプリをインストールします。
Mac・Xcode での、アプリのビルドまわりの設定・手順の詳細は省略します
iPhone でインストールしたアプリを起動すると、以下のダイアログが出るので、カメラ利用を許可する操作をします。
これで、記事の冒頭に掲載した動画のように、アプリを動作させられました。
その他
改善が必要そうな部分
とりあえず動くアプリができたのですが、改善が必要な部分がありそうです。このあたりは調査・検証中ですが、ひとまず情報をメモしておきます。
処理の高速化
今の実装について、高速化する余地がありそうです。このあたりは、例えば Metal が関連するところがありそうで、調査中です。
余談ですが、Metal は以前、Mac用アプリでシェーダーを扱う際に少し触ったことがあるものです。
●Mac用アプリでシェーダーを使った描画を少し試す(VS Code での Swift開発)【その2】 - Qiita
https://qiita.com/youtoy/items/ea5f6c3392b975d99b1a
信頼度マップの利用
sceneDepth.confidenceMap という信頼度のマップが取れるようなので、これを用いてエッジ付近や反射面といったところで出ると想定される不正確な深度値をうまく扱うようにすると良さそうです。
今後のためのメモ
以下は、今後、自分に必要そうだと思った情報のメモです。
LiDAR で取得した値に関するメモ
今の処理では、各フレームで LiDAR で取得した値を使っています。
それに関連するのが config.frameSemantics = .sceneDepth という部分です。ARKit では、この部分で .smoothedSceneDepth と指定することで時間方向に平滑化された深度も扱えるようです。
RGB と深度の解像度の違い
今回横並びで表示した RGB と深度は、解像度が違うようなのでこれらを合わせて使う場合に注意が必要そうです。



