3Dモデルをいろいろ作ってみたい

3Dモデルをいろいろ作ろうとがんばっています。苦労した点、役に立ちそうな情報を発信していきます。

3DモデルのViewerに機能を追加してみた

前回作成した3DモデルViewerにモデルデータのセーブの機能を追加しました。

f:id:ichidaya:20220220210448j:plain

セーブ機能つきのViewer画面

 

大きな変更点は読み込んだ3Dモデルの間でメッシュ<=>ボクセル の相互変換をできるようにして、ボクセルに変換したデータをvox形式で読み書きできるようにしたことです。メッシュ(サーフェス)からボクセルの変換はIM-NETで処理するデータを作成するのに必須の処理です。なんですが、今まで使っていたBinVoxはコマンドラインベースのツールで勝手にウィンドウを開いて処理状況を表示したりするので他のツールと組み合わせて使うのがやりにくい。vedoにもライブラリがあったのですがどうもうまく動いてくれなかったので今回はいちから実装しました。ボクセルからメッシュへの変換はVedoのライブラリの機能(おそらくMarching Cubeアルゴリズム)を利用しました。

vox形式というのはボクセルデータを扱う3DモデリングツールMagicaVoxelのファイル形式です。githubでvox形式の読み書きを行うPythonのコードを見つけて利用させていただきました(MagicaVoxelの最新版には対応してないのがちょっと残念ですが)。

f:id:ichidaya:20220221204238j:plain

MagicaVoxel画面

IM-NETで生成した3DモデルをVox形式で出力しMagicaVoxelで編集、それをIM-NETの学習に利用して新たな3Dモデルを生成する、といったワークフローを目論んでいます。

というわけで、次の目標はこのViewerにIM-NETのモデル生成機能を組み込むことになります。

 

 

vedo libraryを使って3DのViewerを作ってみた

IM-NETを使った3Dモデルの生成の可能性が確認できたところで、さらに手法をいろいろ試すためにはGUIを持ったテストベッドを用意したいと思うようになりました。まず欲しい機能としては以下の2種類の3Dデータ:

  1. ボクセルデータ(NxNxNの格子の点の有無で形を表現する、Pixelの3D版)
  2. サーフェスデータ(頂点と面の集合で形を表現する)

の表示機能です。

ボクセルデータは今までmatplotlibを使った自作プログラムで表示していたのですが、Nが大きくなると「重い」、「遅い」で非常に使いにくい。サーフェスデータの表示はMeshLabなどの既存のツールを使って表示していたのですが、複数のモデルを同時に表示しての比較がやりにくい等細かい点で自分の使い方に合わないところがあります。

じゃあ、ちょっとしたViewerを自分で作ろうか、3Dゲーム用のライブラリのPygletを使えばすぐにできるんじゃないか、と思ったのですが試してみると意外と難しい。こちらが期待するとおりの見え方にするためには投影変換(レンズの選定に相当)とビューイング変換(カメラの位置に相当)の設定を(どちらかというと低レベルの)OpenGLの機能を呼び出して行わなければならない。さらにサーフェスデータの表示(レンダリング)のためには頂点と面の情報だけでなく、各頂点ごとの法線ベクトルを呼び出し側で計算して供給してあげないといけない(無しで表示すると影がなく同じ色で塗りつぶされた全く立体感のない表示になる)。法線ベクトルの計算はやればできるけど、objやplyといったファイルフォーマットごとに頂点と面の情報を抜き出して法線を計算するコードを書くのはいかにもめんどくさい。

結局、解答として見つけたのはvedoというライブラリです。EMBL(European Molecular Biology Laboratory)という欧州19か国の出資で1974に創設された分子生物学の研究所が公開しているライブラリです。" python module for scientific analysis and visualization of эd objects"ということで研究者が自分たちで使うために作ったのではないかと推察しています。Anacondaとpipの両方でインストールできることになっていますが、私の環境ではconda installコマンドではインストールに失敗するためpipでインストールしました。

日本語のドキュメントは見つけられなかったけどapiのマニュアルと豊富なサンプルプログラムが提供されているので見よう見まねで何となくやりたいことをプログラムすることができました。PyQTと組み合わせることもできるので、ボタンや表示用のウィジェットを配置して以下のような簡易viewerを作ってみました。

f:id:ichidaya:20220118224923j:plain

vedoを使った簡易viewerの画面

ボクセルデータはnumpyファイルを読み込み後、legosurface()というメソッドでレゴブロックが並んだような形状のサーフェスデータを作って表示しています。

 

ロボットの顔のモーフィング

IM-NETを使った3Dモデルのモーフィング処理ですが、これまでに集めたロボット頭部のデータをIM-NETに学習させてみました。

1.2つのロボット頭部の3DボクセルデータをそれぞれIM-NETエンコーダで処理

2.得られ2つのたコード(256次元)を加重平均

3.平均ベクトルをIM-NETデコーダで処理、MarchingCubeで3Dサーフェスを復元

f:id:ichidaya:20211218173131j:plain

2つの3Dモデルの平均

加重平均の重みを連続的に変化させてみるとこんな感じ。

f:id:ichidaya:20211218173648g:plain

3Dデータのモーフィング例(1)

ほかにもいくつか試してみました。

f:id:ichidaya:20211218174439g:plain

3Dデータのモーフィング例(2)

f:id:ichidaya:20211218174716g:plain

3Dデータのモーフィング例(3)

例えば以下の2つの元データから、

f:id:ichidaya:20211218175735j:plain

モーフィングの元データ

加重平均の重みを変えてみると元データのどちらとも少しずつ異なる形状が生成されていることがわかります。

f:id:ichidaya:20211218192554j:plain

モーフィング結果の例

IM-NETを使ったモーフィングによる3D形状作成では、アンテナなどのとんがった形状がなまってしまうという難点はあるものの、3Dの形状として成立するデータを生成できることが確認できました。

 

学習用3Dデータの準備に関する備忘録

IM-NETのプログラムが掲載されているgitのリポジトリには学習用の3Dデータをどこから取得するかという点についても記述がありました。ShapeNetとうい3Dデータを集めるプロジェクトがあるんですね。

shapenet.org

Robotの顔なんていう特殊なカテゴリはありませんが、airplaneとかcarとかの3Dデータが3万個ぐらいはあるので、thingiverseから集めたロボットの顔データと組み合わせればよい学習ができそうです。

ShapeNetのデータはmat形式というmatlabで使われている形式で格納されています。scipyパッケージを使ってロードできますが、簡単なデータ圧縮が行われている状態で読み込まれるようです。圧縮を伸長して256x256x256のボクセルデータを得るコードはこんな感じ。

from scipy.io import loadmat
 
voxel_model_mat = loadmat(filename)
voxel_model_b = voxel_model_mat['b'][:].astype(np.int32)
voxel_model_bi = voxel_model_mat['bi'][:].astype(np.int32)-1
voxel_model_256 = np.zeros([256,256,256],np.uint8)
for i in range(16):
    for j in range(16):
        for k in range(16):
            voxel_model_256[i*16:i*16+16,j*16:j*16+16,k*16:k*16+16]
          = voxel_model_b[voxel_model_bi[i,j,k]]
#古いShpapeNetのフォーマットとの座標系の違いを補正
voxel_model_256 = np.flip(np.transpose(voxel_model_256, (2,1,0)),2)

上記の例だと縦、横、高さで256x256x256のデータが16x16x16個の小区画(1区画は16x16x16のボクセルデータ)に分割され”b”に1次元配列として格納されています。"bi"の3次元配列は”b”の1次元配列へのインデックスを保持しています。形状の外側は16x16x16のボクセルデータが全部0、内側は全部1になることが多いので1次元配列の長さは16x16x16=4096をワーストケースとして通常はそれよりもだいぶ短くできるわけです。

あとOBJ形式のサーフェスモデルからボクセルデータを生成する処理を自作のプログラムからshapeNet推奨のbinvoxに乗り換えることにしました。binvoxのほうが速いしサーフェスの内部を埋める処理を私のいいかげんプログラムより正確にやってくれるからです。

binvox   -rotz -rotz -rotx -rotx  5073_Spiderman_Bust_v1.04.obj

-rotz -rotxはShapeNetと私のデータとで前後の向きが逆だったのでその補正をしています。出力はbinvoxという形式ですがbinvox_rwというパッケージで簡単に生の3次元データを抜き取ることができます。

import binvox_rw as bv_rw
 
with open(file_name, 'rb') as f:
model = bv_rw.read_as_3d_array(f).data
model2 = model.reshape([model.shape[0], model.shape[1],
model.shape[2]]).astype(np.uint8)
 

state-of-the-artのDeep Learning(IM-NET)を試してみた

前回、Deep Learningを使った3Dデータのモーフィングを試してみて、なんらかの形を作ってはくれるもののちょっと残念な結果に終わったわけですが、我流で進めるのもそろそろ限界かなと思いちょっと世の中の状況を調べてみました。さすがに日本語の書籍で3Dモデルの生成を扱っているようなものはまだないですが、論文ではいくつか発表されていました。

そのうちの1つが2019年のCVPR(Computer Vision and Pattern Recognition)で発表された

Learning Implicit Fields for Generative Shape Modeling

で、オートエンコーダのデコーダ部分(IM-NET)に大きな特徴があります。通常、エンコーダが出力した何次元かのコード情報から、転置畳込み層とMax pooling層を使って3次元のボクセルまたはサーフェスのデータを作っていくのですが、IM-NETではコードと3次元の点の座標を受け取ってその点がコードが表す形状の内側にあるのか外側にあるのかを出力するネットワークを学習していきます。ある程度の数の点について形状の内側か外側かがわかったところでMarching Cubeというアルゴリズムを走らせて内と外の境界をサーフェス(メッシュ)として出力します。

ちなみにCVPRという国際会議は30年以上前の研究所勤務の時代には文字認識関係の論文を漁ったりしていたこともあるのですが、まさか会社を辞めてからまた論文を読むことになるとは思わなかった...

昔と大きく異なるのは、私のころは国際会議の論文にアクセスするには海外の会議に出席するか高価な予稿集を購入するかしか方法がなかったのが、いまはネットで簡単に論文を読むことができるということ。さらに、論文内容を実装したプログラムや実験に使ったデータまでgitHubから手にいれることができます。公開されているPytorchを使った実装とデータを使ってオートエンコーダの処理を私の環境でも動かすことができました。そこからちょっとだけプログラムを変えてcarの3Dモデルとairplaneの3Dモデルとの間でモーフィングを行ってみました。

f:id:ichidaya:20211123224508g:plain

carとairplaneの間で形状のモーフィングを行う

我流で行ったモーフィングよりも断然スムーズに形状が変化していってる!

次は自分で集めたロボットの頭部データをIM-NETの学習データに取り込んでロボット頭部のモーフィングを試してみたいと思っています。

 

ロボットの頭部をAIでデザインできないかと試してみたい ーVAEでモーフィングー

前回のブログの終わりに法線ベクトルを使った3Dモデル(VAE:変分オートエンコーダ)の学習を実行中と書きましたがその結果がやっとでました。あれから2週間以上もコンピュータを動かしつづけたわけではなくて、結果を確認してみたら学習に失敗していて何回か計算をやりなおすはめになったからです。ネットワークの構成としてはだいたいかたまっていたのですがいくつかのハイパーパラメータ(データから学習するのではなくて人間があらかじめ決めておかかなければならないパラメータ)、具体的にはエンコーダの出力ベクトル長、最適化アルゴリズム(Adam)の学習率、損失を計算するときのKL情報量の重みといったあたりについては試行錯誤を繰り返して設定する必要がありました。

特にKL情報量の重みは書籍に載っているVAEのプログラムの値より(対象が違うから当然といえば当然ですが)だいぶ小さくしないとだめでした。不適切な設定だと損失関数の値は小さな値に最適化されていくのですが、作られている形状をみてみると何もないからっぽということが多かったです。もともとの形状データでも64x64x64個の点のうち意味のある値(0以外の値)を持つのはごく一部です。何を入力されてもみんな値0のからっぽの3Dデータを出力すれば元の入力との差異(損失関数の値)をそこそこには小さくできる、という方針で最適化を行ってしまっているようです。

それやこれやで、なんとかVAEの学習ができているところまで達したと思えたので3Dデータのモーフィングを試してみました。

f:id:ichidaya:20211017215857j:plain

3Dデータのモーフィング例(1)

モーフィング処理の考え方を簡単に記すと以下のようになります。

1.学習済エンコーダーネットワークに既存の3dデータモデルAを入力してベクトルデータVA(今回は64次元)を得る

2.同様に既存の3dデータモデルBをエンコーダネットワークに入力してベクトルデータVBを得る

3.VAとVBの重みつき(以下の例では0.5:0.5)平均VXを計算する。
4.学習済デコーダネットワークにVXを入力して新規の3Dデータモデルを得る

上記の画像には3つの3Dデータモデル(それぞれ正面と側面)が表示されていますが、左端と右端のモデルは学習に用いたデータ(上述のAとB)です。正確に言えば、AをエンコーダでVAに変換してからさらにデコーダで復元したA’(Aダッシュ)です。AとA'が等しくなるようにエンコーダとデコーダの重みを学習したわけですが、現状ではなんとか似たような形状を出力しているといったところでしょうか。真ん中のデータモデルはVAとVBの平均VXをデコーダに入力して得られたもので、AとBの面影はあるものの学習データのセットには存在していなかった形状になります。

f:id:ichidaya:20211018215243j:plain

                                           3Dデータのモーフィング例(2)

 

f:id:ichidaya:20211018215500j:plain

3Dデータのモーフィング例(3)

この結果を観て思ったこと、肯定的な評価と否定的な評価の両方。

まず、肯定的な評価としては面が閉じている頭部パーツとしてそれなりに成立している形状が作成できていること。64個の数値から64x64x64x3個のデータが生成されてひとつながりの面データを表現できているのは「すごい!」と思いました。

否定的な評価として、アンテナなど形状を特徴づけるとんがった部分がエンコーディング/デコーディングの過程で失われていく傾向があることですね。実際、モーフィング以前の問題として上述のAとA'(Aダッシュ)を比べてみると細めのアンテナっぽい形状は失われてしまっている。いわゆる周波数の低い成分しか残らない。

f:id:ichidaya:20211018224531j:plain

オリジナル3Dモデルとエンコーディング・デコーディングを行った結果

デザインの支援に使うという意味ではむしろアンテナのような特定の形状を強調したいのですが、どうしたらいいのだろう? まだまだ試行が続きそうです。

 

 

 

ロボットの頭部をAIでデザインできないかと試してみたい ー3Dモデル表現方法の変更ー

これまでDeep Learningの入力/出力となるデータとして例えば1x64x64x64といった形式のNumpyデータを使ってきました。64というのは空間をX方向、Y方向、Z方向に分割するときの解像度(数字が大きいほど形状を細かく表現できる)ですが、残った1次元(1チャネル)は64x64x64で区切られた小区画の中にサーフェスモデル(OBJ形式)の頂点が存在する確率になります。VAEやGANのネットワークで生成する3Dデータもこの形式なので、実際にネットワークの出力結果を3Dのモデラーで使うためにはNumpy形式のデータからOBJ形式に変換しなければなりません。OBJ形式には頂点だけではなく3または4個の頂点で作られる面の情報が必要なので、点データの近傍を検索して貪欲算法で近傍点とで構成する面を出力するというプログラムを作ってOBJ形式データを作っていました。

で、今回はDeep Learningのネットワークがどうこうという以前にOBJ形式⇒1チャネルのNumpy配列⇒OBJ形式という変換をやってみてどのくらい元の形状を再現できるのか、というのがテーマです。

以下はテストに使ったオリジナルの3Dモデルです。Thingiverseで見つけたモデルをZBrushで読み込んだ後ダイナメッシュで細かいメッシュのモデルに変案しています。

f:id:ichidaya:20210928190009j:plain

オリジナル3Dモデル

ニューラルネットで処理できるよう1x64x64x64のNumpy配列に変換します。これをmatplotlibで表示するとこんな感じです。

f:id:ichidaya:20210928192105j:plain

3DモデルのNumpy配列表現をmatplotlibで表示

だいたいの形状は表現できていると思うのですが、これを上述の貪欲算法のプログラムでOBJ形式に変換して表示してみると、ちょっと残念な感じになってしまいました。

f:id:ichidaya:20210928192654j:plain

1チャンネルNpyからサーフェスモデルを復元

近傍の点同士を面としてつなげる閾値の設定にもよるのですが、目とか耳のあたりの凹凸が埋まってわからなくなっている反面、ちょっと細めのアンテナは消えてしまっています。ちなみにMeshLabなどのツールでは一様でない点群データからサーフェスを生成する一般的なアルゴリズム(ボールピボットアルゴリズムというのがあるらしい)を実装していますが、これを単純に適用した結果は上記よりもさらに残念なものでした。なんとなくこんなもんかと思って、この形式のNumpyデータをニューラルネットの入力に使っていたのですが、これだとネットの学習がうまくいって良いデータが生成されたとしてもOBj形式に変換するときに劣化してしまいます。

今回、もうちょっとなんとかしたいということで考えたのはもともとのOBJ形式がもっていた面の情報をNumpy形式データに取り込むこと。具体的にはある区画に頂点が存在するかという1チャンネルの情報ではなく、頂点を含む面の方向、正確には頂点を含む面に垂直なベクトル(法線ベクトル)を示す3チャンネルの情報を使うことにしました。OBJ形式データには頂点とその頂点が構成する面の情報があるので、面に対する法線ベクトルを計算して3x64x64x64のNumpyデータを出力します。NumpyデータからOBJ形式に復元するさいには、法線ベクトルと垂直な平面上に位置する近傍点を優先して面データを作成します。3x64x64x64のNumpyデータからこの方式でOBJを復元すると以下のようになりました。

f:id:ichidaya:20210928221034j:plain

3チャンネルNpyからサーフェスモデルを復元

オリジナルにはおよばないもののだいぶ形状を復元できるようになりました。

Pytorchの使い方もわかってきたところで、現在3x64x64x64のデータでVAE(変分オートエンコーダ)の学習をおこなっています(いまいま、まる4日PCを動かし続けている…)。どんな3Dデータが生成できているか、期待しながら待っているところです。