はじめに
この記事はHoudini Apprenticeアドベントカレンダー2020 14日目の記事です。
iPhone12 Proを買ったので、ゲームとSNSするだけの高い端末にならないようにARKitのLiDARスキャナーと
点群、メッシュ、FBX/Pixar USD/GLTF とほぼなんでも読み込み可能なHoudiniを組み合わせて検証してみました。
3Dスキャンやフォトグラメトリ的にはn番宣じ感が強いのと、建築関係のトピックが多く門外漢からするとテーマ設定的に
後悔しているのですが、頑張って内容を差別化して書きます。
はじめに断っておきますと、最終的ないい成果を得ることができなかったので、そういう失敗込みで許容できる方に読んでもらえると嬉しいです。🙇🙇🙇
LiDARとARKit4のおさらい
ドキュメントやGoogle先生で頑張って情報を漁るよりもWWDC2020のセッション をみるのが一番わかりやすいです。
要約するとARKitのARFrame
から取得できるARDepthData
(LiDAR搭載デバイスでsmoothedSceneDepth
プロパティから取得)、ARPointCloud
はそれぞれ
ピクセルバッファとして取り出せば深度マップとして使うことができ、pointcloud の Array データとして取り出せば、点群データとして書き出すことができます。
Swiftコードはこんな感じになります。
import ARKit
...
let session = ARSession()
let configuration = ARWorldTrackingConfiguration()
if type(of: configuration).supportsFrameSemantics(.sceneDepth) {
configuration.frameSemantics = .sceneDepth
}
session.run(configuration)
...
func session(_ session: ARSession, didUpdate frame: ARFrame) {
guard let depthData = frame.sceneDepth else { return }
var pixelBuffer: CVPixelBuffer!
pixelBuffer = depthData.depthMap
var pointcloud: ARPointCloud!
pointcloud = frame.rawFeaturePoints
}
LiDARの有効距離が最大で5mなので、歩き回れば細かくスキャンできるものの、広域スキャンという感じではないです。
点群の認識精度はそこまで高くない(精度何ミリまでは調べるだけでは分からなかった)ので、処理結果の正確さはCVPixelBuffer
の画像処理に
依存する形になります。ARConfidenceLevel
は信頼度マップをアプリから切り替えられるプロパティですが、アプリ内で調整や加工ができるかどうかは割と重要です。
オフラインでのキャプチャになってしまうので、デスクトップのアプリケーションに移してから点群を頑張って修正する作業は避けたいですよね。
収録データやアセット製作工程を後に回してしまって良いとなると、だいたい素材製作工程が適当になってポスプロ工程にしわ寄せが来るようになる懸念があるので...
HoudiniのLiDARのおさらい
LiDARスキャンデータを取り扱うにあたり、Houdiniにはそのものずばり LiDAR Import SOP というノードがあります。
詳しくは@reisyuさんの 記事 にとてもよくまとまっています。読み込みサポート形式はlasとe57の2種類です。
lasは静岡県伊豆の点群データが有名ですが 3次元点群 でよく見るフォーマットです。
e57は記事作成時点で見かけることがあまりなかったのですが、Leica BLK360 でiOSアプリと連携して出力できるらしいです。(新車が買える値段)
スキャンされたサンプル も公開されています。データサイズもでかいです。
Apprentice的に検証にそこまでお金をかけられないので、手元にあるiPhoneと連携する部分で中身を掘り下げて書きたいと思います。
それぞれの点群フォーマットについて掘り下げたい方は、下記のリンク先を参照ください。
対応フォーマットについて
最初に断っておくと標準フレームワーク内で前述のlasやe57といったデータを書き出すことはできません。
MetalやSceneKitから3Dファイルを出力する際はModelIO
フレームワークを使います。 出力対応フォーマット内6 にある ply で出力しておけば、点群データとして使えます。
それ以外のデータフォーマットに対応する場合はARPointCloud
をasciiで直書きするか、それぞれの対応フォーマットに沿ったシリアライザを自分で書きます。
(ARKitからのメッシュ出力関連は独自パーサーというより assimp で対応していると思います。FBXがないのが判断基準の一つです。)
LiDAR 対応アプリと連携させる
本当は以上の検証を基にして、自分でARKitを使ったSwiftでアプリを作るところまで実践するのが一番なのですが、
時間(と気持ち)に余裕がなかったので、無償有償問わずLiDAR対応のアプリをダウンロードして使ってみる事にしました。
- pronoPointsScanはこの中だと唯一の国内アプリでかつ精度が一番高いです。usb又はwifiで繋いだデスクトップのファイル共有からコピーして使います。Unityアプリですね。
- 3d Scanner App™は無料なのが信じられないレベルで出力対応フォーマットも多いです。最初に使ってみるARアプリとしてもおすすめです。
- Record3Dは対応フォーマットは少ないものの、USBで繋ぐとストリームデータとしてRGBD(DはDepth)で取り出す事ができる面白いアプリです。C++/Python ライブラリがGitHubで公開されています。
- ScandyProはよく検索にかかる機能豊富なアプリです。サブスクリプションです。機能豊富なんですが肝心のスキャンがいまいちなので、継続的にお金払う気にはならないです...(使い込んでいないというのもあります)
- Polycamはスキャン後の後加工がアプリ内で完結するので、非常に使いやすいです。サブスクでも払う価値あります。
いろいろ試した結果、汎用的に使えるplyを読み込むのもいいのですが、画像で取り出すことのできるRecord3Dが割と面白そうなのでHoudiniと組み合わせて使ってみる事にします。
HoudiniにLiDARデータを読み込む準備
record3D のPython パッケージはpython -m pip install record3d
でインストールできます。inlinecpp
モジュールやその派生元のHDKを通じてC++に切り替えもできます。
USBでPCと接続してください。WiFi化は #issue が立っているので近日対応ですかね?
demoアプリを試す際はpython -m pip install opencv-python==4.1.2.30 opencv-contrib-python==4.1.2.30
で先にパッケージをインストールしてください。numpyは依存関係上、勝手に入るはず。
(注意)ここからは検証がうまく行かなかった内容を含みます!
COP ジェネレーターの定義
まずはPythonでCOP Generatorオペレータ を定義して、record3D から RGBD ストリームデータを取り出します。雰囲気でやっていきます。
RGB ストリームデータあるいは Depth ストリームデータを取得できますが、デバイスが繋がっていない時も考慮してColorパラメータを返すようにします。
from record3d import Record3DStream
import numpy as np
from threading import Event
class App:
def __init__(self):
self.event = Event()
self.session = None
def on_new_frame(self):
self.event.set()
def on_stream_stopped(self):
pass
def connect_to_device(self, dev_idx):
devs = Record3DStream.get_connected_devices()
self.session = Record3DStream()
self.session.on_new_frame = self.on_new_frame
self.session.on_stream_stopped = self.on_stream_stopped
if len(devs) > 0:
return self.session.connect(devs[dev_idx]) # Initiate connection and start capturing
return False
def start_processing_stream(self, cop_node, plane):
while True:
self.event.wait() # Wait for new frame to arrive
if plane == "A":
pixels = self.session.get_depth_frame() # > numpy.ndarray[float32]
else:
pixels = self.session.get_rgb_frame().astype(np.float32) / 255 # > numpy.ndarray[uint8]
x, y, d = pixels.shape[:3]
c_pixels = pixels.reshape(y, x, d).copy()
try:
cop_node.setPixelsOfCookingPlaneFromString(c_pixels, component=None, interleaved=False, depth=hou.imageDepth.Float32)
except hou.OperationFailed as e:
raise hou.NodeWarning(e.instanceMessage() + "\n" + str(c_pixels))
finally:
self.event.clear()
def resolution(cop_node):
return cop_node.parmTuple("res").eval()
def output_planes_to_cook(cop_node):
return ("0", "A")
def cook(cop_node, plane, resolution):
app = App()
if app.connect_to_device(0):
app.start_processing_stream(cop_node, plane)
else:
num_pixels = resolution[0] * resolution[1]
rgba = cop_node.parmTuple("color").eval()
pixel = (rgba[3:] if plane == "A" else rgba[:3])
cop_node.setPixelsOfCookingPlane(pixel * num_pixels)
record3d は numpy.ndarray
型で上記のストリームデータを返すので、hou.CopNode.setPixelsOfCookingPlaneFromString
に直接渡すことができるらしいです。
numpy
を用いたサンプルでうまく動作したものが見つからなかったので、情報あれば下さい(必死)。forumも探してみましたが良い回答はなかったです。
普通に渡してAuto Updateでcookするとエラー時に Houdini が巻き込まれて落ちることがあり、printデバッグもできないので難しいです。
Pythonオペレータをテストする際はraise hou.NodeWarning(STR)
で例外を投げて、ノードエディタに警告を出すのが安全です。
COP → SOP へ
COP ジェネレーターで取り出した各画像をSOPでAttributeに流し込んでいきます。
これをAttribute from Map SOP
に繋いで、アトリビュートに取り出します。
...
ここまで、LiDARの深度データをCOPで取り出してSOPにつなげるまでの一連の流れを検証してみました。
おわりに
途中脱線しまくりでApprenticeの内容にまとめきれなくてすみません。
(良い結果が得られるかはともかく)周辺デバイスと組み合わせてビジュアライズするまでのハードルがかなり低く、
プロトタイプにも使いやすいHoudiniを積極的に使っていきたいと改めて思いました。プロセスをあれこれ試行錯誤するのが何より楽しい。