はじめまして。VOYAGE GROUP VR室長の @jujunjun110 です!
いきなりですが、VOYAGE GROUPでは10月からVR室を立ち上げ、VRという新領域に取り組みはじめました。
また、それに伴いVR室ブログも立ち上げました。
こちらは毎週水曜日更新ですので、ぜひチェックしてみて下さい!
...さて、以上で私の言いたいことは120%言い終わったのですが、これだけで更新するのも申し訳ないので、今回はVRアプリケーションで使うための3Dスキャナーを自作したときの話を寄稿させていただきます!
非エンジニアにもかかわらずこの場に書かせていただけて大変光栄です!
目次
今回作る3Dスキャナーの仕組み
突然ですが、一つのモノを様々な角度から撮影した大量の2次元写真が欲しいなと思ったこと、みなさんも一度はありますよね?
今回作っていくのは、みなさんのそんな課題をズバッと解決してくれるマシンです!
Multiple View Geometryについて
今回作る3DスキャナーはMultiple View Geometryというコンピュータービジョンの技術を利用したものです。
(wikipedia より)
Multiple View Geometry とは、その名の通り、一つのモノを様々な角度から撮影した大量の2次元写真を組み合わせて、1つの3Dモデルを構成する技術です。
具体的にはこんな感じ。
- 写真に写っているオブジェクトの中で、複数の写真で「同じ部分」と認識できる箇所(特徴点)を見つける
- 1で発見した特徴点を元に、写真が撮られたときのカメラ位置を特定 (Structure from Motion: SfM)
- 3で特定したカメラ位置から、オブジェクト自体の形状を復元
この手順自体は、RealityCaptureという市販ソフト*1が非常によくできているので、それほど難しくはありません。
しかし、一つのモノを様々な角度から撮影した大量の2次元写真を撮影するのがとにかく面倒だし難しい。
やってみるとわかりますが、取り漏れる面があったり、変に背景が写り込んだり、理想的な写真を撮影するのはなかなか困難です。なにより綺麗なモデルを作るには数十枚の写真が必要なのですが、これを撮るのはかなりの重労働です。
そこで、今回はMultiple View Geometry用の大量の画像を自動で撮影するマシーンを作成していきたいと思います。
Let's 電子工作!
今回作るマシンの仕組みは至ってシンプルです。
- 回転テーブルに撮影したいものを乗っける
- 回転テーブルを少し回す
- カメラのシャッターを切る
- 2に戻る
今回はArduinoをベースにこれを作っていきます。
全体像はこんな感じになります。
(↑回路図の読み書きが全くできない哀しき男の書いた図)
非常にシンプルですね。早速見ていきましょう。
なお、電子工作初心者なので説明が不正確・不十分なところがあると思います。はてぶコメントなどで指摘いただければ幸いです。
連続回転サーボ制御による回転テーブル作成
まず、360°ぐるっと写真を撮影する必要があるので、連続回転サーボを使って回転テーブルを使います。
ANBE MG996Rタイプ連続回転サーボ サーボメタルギア・メタルホーンセット (MG996Rタイプ連続回転サーボ)
- 出版社/メーカー: ANBE
- メディア:
- この商品を含むブログを見る
一般的なサーボモーターは0〜180°までしか回らないのに対して、何回でも自由回転できるようになっているのがこの連続回転サーボです。
参考にさせていただいたこの記事( Arduino 連続回転サーボ | アンドロイドな日々 )によると、
制御信号は、周期的なパルスで、周期 20ms、パルス幅 1.0ms – 2.0ms です。 パルス幅 1.5ms で停止、1.0 – 1.5ms で時計周り、1.5 – 2.0ms で反時計周りです。 停止の 1.5ms から離れるに従い回転数が増えます。
とのこと。
こんな感じでPWM信号のパルスを設定してやると、キュっと一瞬だけ動いて止まる動作が実現できます。回転角度はこのパルスを調整することである程度調整可能です。
digitalWrite(TablePin, HIGH); delayMicroseconds(2000); // PMW信号のパルスを設定 digitalWrite(TablePin, LOW);
普通のサーボと違って回転角度を厳密に指定してやるのは難しいですが、今回は一定の角度ずつ回し続けられればよいのでこれで十分です。
ちなみにテーブル面は、フィギュア用の回転テーブルのものを使いました。*2
フォトカプラによるシャッターの制御
これで回転テーブルの部分はできたので、次にカメラのシャッターを自動制御する部分を作成していきます。
今回撮影に利用した一眼レフ(Canon EOS Kiss X7)は、2.5mm ステレオミニプラグがシャッタースイッチになっているものなので、適当に使えそうな延長ケーブルを利用します。
富士パーツ商会 スーパースリムプラグ・オーディオ変換ケーブル/1.2m 3.5mm ステレオミニプラグと2.5mm ステレオ超ミニプラグ φ3.5とφ2.5 /AD-SPS-12
- 出版社/メーカー: 富士パーツ商会
- メディア: エレクトロニクス
- この商品を含むブログを見る
これをおもむろにニッパーで半分に切ると、2本のケーブル(赤、白)とその外側の銅線(GND)が出てきます。
2.5mmジャックの側をカメラに挿した状態で、
- 赤とGNDを触れさせると、ピントを合わせる
- 白とGNDを触れさせると、シャッターを切る
という動作をすることが確認できます。意外とシンプルな機構なんですね。
つまりシャッターを切りたいタイミングで白のケーブルとGNDを通電させれば、タイミングをコントロールできるので、フォトカプラを使って実現します。
【ノーブランド品】DIP-4817CフォトカプラIC 10個
- 出版社/メーカー: 【ノーブランド品】
- メディア: エレクトロニクス
- この商品を含むブログを見る
フォトカプラからは4本の足が出ており、下の画像のように、ある2本に電流を流すと光の信号を通じてもう2本の間が通電します。
Arduinoに接続されている側のPINがHIGHになると、フォトカプラの逆側も通電し、シャッターがおりて写真が撮影されるというわけです。
void shot() { digitalWrite(ShutterPin, HIGH); delay(1000); // 1秒くらい待たないと、ピントが合いきらずシャッターがおりないことがある digitalWrite(ShutterPin, LOW); }
Arduinoのソースコード
今までの部分をソースコードにまとめるとこんな感じになります。
メインのループである loop( ) 関数で、テーブルを回転 → 1秒待機 → シャッターを切る(1秒かかる) → 3秒待機 となっているのが分かると思います。
int TablePin = 12; int ShutterPin = 13; int width = 2000; // 初期設定 void setup() { pinMode(TablePin, OUTPUT); pinMode(ShutterPin, OUTPUT); } // メインループ void loop() { rotateTable();// テーブルを回す delay(1000); // 1秒待機 shot(); // シャッターを切る delay(3000); // 3秒待機 } void rotateTable() { digitalWrite(TablePin, HIGH); delayMicroseconds(width); // PMW信号のパルスを設定 digitalWrite(TablePin, LOW); } void shot() { digitalWrite(ShutterPin, HIGH); delay(1000); // 1秒くらい待たないと、ピントが合いきらずシャッターがおりないことがある digitalWrite(ShutterPin, LOW); }
ユニバーサルプレートによる組み立てと配線
これで基礎となる仕組みはできたので、使いやすいように組み立てていきます。
枠組みにはタミヤのユニバーサルプレートを使います。
タミヤ 楽しい工作シリーズ No.157 ユニバーサルプレート 2枚セット (70157)
- 出版社/メーカー: タミヤ
- 発売日: 2009/06/23
- メディア: おもちゃ&ホビー
- 購入: 3人 クリック: 6回
- この商品を含むブログ (1件) を見る
タミヤ 楽しい工作シリーズ No.172 ユニバーサルプレートL 210×160mm (70172)
- 出版社/メーカー: タミヤ
- 発売日: 2009/06/23
- メディア: おもちゃ&ホビー
- クリック: 3回
- この商品を含むブログ (3件) を見る
ユニバーサルプレートはネジだけで電子工作の骨組みができるすごいヤツです。
こんな感じでニッパーで穴をあけてサーボモーターを固定し、
下面にはArduinoを固定します。
配線については、設計時はブレッドボードで行いますが、実際稼働させるとなると配線が抜けやすかったり邪魔だったりするので、はんだ付けで固定します。
フォトカプラ周辺などは、むき出しのままだと不意に配線同士が触れて予期せぬ動きをするので、こんな感じにグルーガンで固めて絶縁すると良いです。
これで完成です!
うーん、無骨で漢らしくも、繊細でデリケートな一面も垣間見える、惚れ惚れするデザインですね...!
写真撮影
それでは、早速撮影に移っていきましょう。
セッティング
クロマキーで背景抜きをする必要があるので、ブルーバックのフォトブースを買いました。
- 出版社/メーカー: ロアス
- メディア: Camera
- クリック: 67回
- この商品を含むブログ (7件) を見る
今回は、個人的に髪型に親近感を感じる懐かしキャラ、アフロ犬を撮影していきます。
こんな感じでフォトブースを途中まで組み立て、
回転台を外した状態で青い布の下に本体をセットし、
上に回転テーブルを固定すればセッティング完了です!
ちょっと暗かったので斜め前からLED照明を当てています。
撮影開始
この状態でおもむろに電源を入れると、動き出します!※倍速にしてあります
ちょっと分かりにくいですが、ターンテーブルが少し回ってはシャッターが切られているのが分かると思います。
そんな感じで撮影された写真がこちら。
壮観ですな。
今回は特徴となりそうな点が多いので、全体が写っている写真がなくても問題ないと判断し、上からと下からの2アングルから、1周ずつ撮影しています。
クロマキーによる背景処理
Multi View Geometryは、本来固定されたオブジェクトに対しカメラを回転させて撮影することが前提なので、 今回のようにオブジェクト自体を回してしまうと、背景部分が回転していないため矛盾が生じてうまく合成することができません。
そこで、ブルーバックの部分を削除するため、OpenCVを利用して簡易的なクロマキー合成を行います。
クロマキー処理の仕組み
#OpenCV HSV H:0-180, S:0-255, V:0-255 lower_color = np.array([100, 110, 30]) # 色空間の下限 upper_color = np.array([120, 255, 255]) # 色空間の上限
このような感じで、HSV形式で抜き出す色空間の下限と上限を設定します。
図にするとこんな感じ。
色味的には青っぽいところでも、あまりに白や黒に近い部分はマスク対象にしないような設定であることが分かると思います。
なお、ふつう角度は0〜360°で表しますが、OpenCVにおいては角度は0〜180°で表します。したがって、本来「青」はHSV空間で200〜240°付近にあるのですが、コード上では1/2をかけて100〜120°と表現していることに注意しましょう。
撮影するオブジェクトやライティングによって青色の範囲は変わるはずなので、環境によってこの値は調整してみてもいいかもしれません。
Pythonで一気にクロマキー処理を行う
この処理を、撮影した全ての画像に対して行っていきます。
以下は、指定したディレクトリにある拡張子.JPG
の画像全てにクロマキー処理をかけて、./chromakey/
以下に配置するスクリプトです。
#!/usr/bin/python import os, glob import cv2 import numpy as np def main(): dir_name = "path/to/directory/" if not os.path.exists(dir_name + "chromakey"): os.mkdir(dir_name + "chromakey") img_paths = glob.glob(dir_name + "*.JPG") for img_path in img_paths: file_name = img_path.split("/")[-1] export_chromakey(dir_name, file_name) def export_chromakey(dir_name, file_name): print file_name #OpenCV HSV H:0-180, S:0-255, V:0-255 lower_color = np.array([100, 110, 30]) # 色空間の下限 upper_color = np.array([120, 255, 255]) # 色空間の上限 img = cv2.imread(dir_name + file_name); hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 画像をBGR形式からHSV形式に変換 mask = cv2.inRange(hsv, lower_color, upper_color) # マスクを設定 inv_mask = cv2.bitwise_not(mask) # マスクを反転 result = cv2.bitwise_and(img, img, mask= inv_mask) # 画像からマスク部分を削除 cv2.imwrite(dir_name + "chromakey/" + file_name, result) if __name__ == '__main__': main()
綺麗にヌケたね★
RealityCaptureで立体起こしを行う
さて、こんな感じで前処理が終わったのでReality Captureにぶっこんでいきましょう。
特徴点とカメラ位置の特定
RealityCaptureにドラッグ&ドロップで画像を読み込ませ「Align Images」ボタンで特徴点の発見とカメラ位置の特定(SfM)を行います。
ドン!
おおおー!!!かなり綺麗にいきました!
今回は80枚の画像を読ませたのですが、全てが一つのコンポーネント(特徴量で紐付けられる画像群)にまとまりました!ちなみに所要時間はハイスペックPCで2分程度。
画像の周りに見えている、白い点々がそれぞれのカメラ位置です。
今回は上からのアングルと下からのアングルで1周ずつしたのがわかると思います。
特徴点がこんな感じで緑の線で紐付けられています。
ちなみにぬいぐるみのような布状のものは特徴点を見つけるのがやりやすく、うまくいきやすいです。一方でテカテカした素材や、同じ色でのっぺりした丸っこい素材のオブジェクトは特徴量を見つけるのが難しいようで、うまくいきにくいので注意です。
ちなみに一回のAlign Images で一つのコンポーネントにまとまらない場合は、それぞれのコンポーネントの、同じような角度から撮られている画像同士に、手動で特徴点(control point)を指定してあげる必要があります。これがかなり根気のいる作業なので、一発で合成できたのはかなりラッキーですね。
モデルの生成
さて、この状態だと特徴点の集まりにすぎないので、次に「Normal Detaiil」もしくは「High Detail」ボタンで点同士の間をより丁寧に埋めていきます。(だいたい20分くらいかかる)
すると、このようにモデルができるので、
ついで「Colorize」「Texture」と選択し、色とテクスチャを設定します。(これは1分くらいで終わる)
さきほどより色がカバーされている部分が増え、ぬいぐるみっぽい質感に近づきましたね!
ちょっと後頭部の薄さは気になるところですが...。
メッシュの書き出し
最後に、オブジェクトを書き出していきます。対応拡張子はply, obj, xyz, partList の4つ。
今回は扱いやすいobjを選択し、Mayaで読み込んでみます!
できました!
先程は穴になってしまっていた部分も周辺色で補完され、きっちり閉じた立体になっています。
アフロのふわふわ感もかなり綺麗に再現されています!
あとは土台の部分を取り除いたりすれば、そのままVRアプリケーションなどに使えますね!
... と言いたいところなのですが、一つ問題が。
今回作ったこのファイル、実は332MBもあります。
かなり細かく凹凸が再現されている分、ポリゴン数が300万を超えてしまっており、HTC Viveが動くようなハイスペックPCでも、Mayaで扱うとかなり重くなってしまっています。
VRにおいては、
- 常に両目分レンダリングする
- 酔いを防ぐため90fpsは欲しい(PS4のゲームでも30fpsのものが多い)
という事情もあるため、このモデルサイズは実はかなり厳しいです。
当然RealityCaputureの機能でローポリに落とすこともできるのですが、見た目のクオリティはかなり下がってしまうので、これをリアルタイムレンダリングに用いるのはもう少し処理速度の進歩を待つ必要があるかなと言った感じです。
以上、3Dスキャナ(の撮影部分)を自作してみた話でした!
...まだ難点もあるとはいえ、このクオリティの3Dスキャンが自作のツールで簡単にできるのは、かなり夢があるというのがお分かりいただけたかと思います。
今回は有料ソフトのRealityCaptureを利用しましたが、openMVGというオープンソースのライブラリもあるようなので、このあたりを使ってみて、完全自作でやってみるのも面白そうですね。
やっぱり自分で手を動かしてみて、最新の技術に触れるというのはいいもの。
VR室では、これからも「実写 × VR」で面白いものを作るために、研究開発を進めていきたいと思います!
まとめ
VOYAGE GROUP VR室ブログ、毎週水曜更新なので見てね!!!
今回はオブジェクトを3Dスキャンしましたが、部屋そのものをスキャンした時の記事なんかもありますよ!
おわり。
はーたのしかった。