Open3D(Python)でNon-blockingな描画処理

お仕事でOpen3D(Python)を使うことになったので調べてみたところ、意外と日本語の情報が少なく、
Blockingな視覚化(draw_geometry)までのことが多かった。 

ので自分が調べた範囲をつらつらまとめます。 
点群処理云々については専門外かつ他の方がまとめてそうなのでそちらを参照。

---
**2019/07/09 追記**
**Open3D 0.7.0がリリースされ、リファレンスにはあるけど実装されていない！ということが減った（なくなった？）ようです。**
**remove_geometryも実装されていましたが、下記の代替手段とはまた挙動が異なるため使い分けは依然必要な気がします。**
**（具体的には、remove_geometry時にはGeometryが「削除」されるのであって「非表示」になるのではないため、一部情報がリセットされるようです）**

## 環境
Python 3.6.1 64bit 
Open3D 0.6.0 
numpy 1.16.3 
opencv-python 3.2.0

## 参考
[公式リファレンス](http://www.open3d.org/docs/index.html) 

# ウィンドウ表示
親の顔ほど見たwhileループでウィンドウを表示するだけのやつはこちら。

```python
import open3d

# ウィンドウ初期化
vis = open3d.Visualizer()
vis.create_window(
    window_name="Hoge",  # ウインドウ名
    width=800,           # 幅
    height=600,          # 高さ
    left=50,             # 表示位置(左)
    top=50               # 表示位置(上)
)

while True:
    # 更新処理
    vis.update_geometry()
    vis.poll_events()
    vis.update_renderer()
```

これで真っ白なウィンドウが表示されます！完！ 
ちなみに閉じるボタンでは終わらないのでCtrl+Cとかで。

# Geometryをファイルから読み込んで追加
既にやりきった気分ですがこれだけでは肝心なNon-Blockingなオブジェクト操作ができません。 
Open3Dでは画像・点群・メッシュ等々はopen3d.geometry.Geometryを継承した派生クラスとして
定義されているようなので、これらを追加していけばウィンドウに描画できます。

```python
import open3d

# ウィンドウ初期化
vis = open3d.Visualizer()
vis.create_window(
    window_name="Hoge",  # ウインドウ名
    width=800,           # 幅
    height=600,          # 高さ
    left=50,             # 表示位置(左)
    top=50               # 表示位置(上)
)

# Geometry追加
img = open3d.read_image("lenna.png")
mesh = open3d.read_triangle_mesh("bun_zipper.ply")

vis.add_geometry(img)
vis.add_geometry(mesh)

while True:
    # 更新処理
    vis.update_geometry()
    vis.poll_events()
    vis.update_renderer()
```

ところでこの有名なうさぎさんは[Stanford Bunny](http://graphics.stanford.edu/data/3Dscanrep/)
っていうんですね。 
こんな感じに表示されます。
![00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417033/93ed0eb2-65f7-2ae2-64d5-189a37307c39.png)
ちなみにこの時点でデフォルトで規定されているマウス操作が可能なのでLenna嬢をバックにStanford Bunny
をぐりぐり動かせます。 
逆にこの操作を無効にする方法は分かりませんでした。誰か教えて下さい。

# 画像の操作
ここまではこんな記事を見なくてもチュートリアルを見れば事足りますが、以下ちょっと詰まったポイント。 
open3d.read_imageの戻り値はopen3d.geometry.Imageクラスですが画素データに触るのにコツが要ります。

```python
import open3d
import cv2
import numpy as np

# ウィンドウ初期化
vis = open3d.Visualizer()
vis.create_window(
    window_name="Hoge",  # ウインドウ名
    width=800,           # 幅
    height=600,          # 高さ
    left=50,             # 表示位置(左)
    top=50               # 表示位置(上)
)

# Geometry追加
img = open3d.read_image("lenna.png")
mesh = open3d.read_triangle_mesh("bun_zipper.ply")
vis.add_geometry(img)
vis.add_geometry(mesh)

# 画素値をコピー
pix = np.asarray(img).copy()

# 任意の画像処理
bgr_pix = cv2.cvtColor(pix, cv2.COLOR_RGB2BGR)
bgr_flg = True

while True:
    # 毎フレーム処理
    if bgr_flg:
        np.asarray(img)[:] = bgr_pix
    else:
        np.asarray(img)[:] = pix
    bgr_flg = not bgr_flg

    # 更新処理
    vis.update_geometry()
    vis.poll_events()
    vis.update_renderer()
```

実行結果は以下です。ようやくNon-Blockingな感じの処理です。
![anim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417033/d5f0fbf6-70ce-f9ee-edd0-96a5cb15dddc.gif)
目がチカチカする。 
上記コードのキモはopen3d.geometry.Imageクラスからnumpy.asarrayで画素値に直接アクセスするところですね。 
ちなみにcopyを忘れるとそのまま画素値の参照をとってきてしまうので上のように明滅しません。

ファイルからの画像を読み込むのではなく、オンメモリで操作したい場合は以下のように初期化できます。

```python
import open3d
import numpy as np

width = 100
height = 100
buf = np.zeros([width, height, 3], np.uint8)
img = open3d.geometry.Image(buf)
```

しかしこのコード、バッファ配列(buf)をいじっても画像には反映されなかったのでやっぱりnp.asarray(img)とかしてアクセスする必要があるみたいです。

# メッシュとか点群の操作
 [geometry.TriangleMeshの公式リファレンス](http://www.open3d.org/docs/python_api/open3d.geometry.TriangleMesh.html)を見るとrotateというメソッドがあるのでこれで回転できそうですね。 
 いやあー便利便利。

 ```python
 >> "rotate" in dir(open3d.TriangleMesh)
  False
 ```

 **は？無いが？** 
どうやらリファレンスに記載があるのに実装されていない部分が多々あるようです。 
仕方がないのでtransformを使いましょう。

```python
# 変換行列(4*4)
rad = np.deg2rad(5)
mat = np.array([
    np.cos(rad), -np.sin(rad), 0, 0,
    np.sin(rad), np.cos(rad), 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1
]).reshape(4, 4)

while True:
    # 毎フレーム処理
    mesh.transform(mat)

    # 更新処理
    vis.update_geometry()
    vis.poll_events()
    vis.update_renderer()
```

毎回全部書いてると長いので省略。 
これでZ軸方向に毎フレーム5度回転します。

# コールバックを定義する
リファレンスを眺めているとVisualizerの派生クラスにVisualizerWithKeyCallbackという素敵なクラスが定義されていました。 
意外と簡単に使えます。

```python
import open3d
import numpy as np

# ウィンドウ初期化
vis = open3d.VisualizerWithKeyCallback()  # <-コールバック対応クラス
vis.create_window(
    window_name="Hoge",  # ウインドウ名
    width=800,           # 幅
    height=600,          # 高さ
    left=50,             # 表示位置(左)
    top=50               # 表示位置(上)
)

# Geometry追加
img = open3d.read_image("lenna.png")
mesh = open3d.read_triangle_mesh("bun_zipper.ply")
vis.add_geometry(img)
vis.add_geometry(mesh)

# 変換行列(4*4)
rad = np.deg2rad(5)
mat = np.array([
    np.cos(rad), -np.sin(rad), 0, 0,
    np.sin(rad), np.cos(rad), 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1
]).reshape(4, 4)

# コールバックを登録
rotate_flg = False
def rotate_callback(_vis):
    global rotate_flg
    rotate_flg = not rotate_flg
vis.register_key_callback(ord("R"), rotate_callback)

while True:
    # 毎フレーム処理
    if rotate_flg:
        mesh.transform(mat)

    # 更新処理
    vis.update_geometry()
    vis.poll_events()
    vis.update_renderer()
```

ちなみにコールバックに渡されてるのは呼び出し元のVisualizerクラスです(多分) 
これで'R'キーを押すたびにウサギさんの回転がON/OFFされます。 
ポイントは、

```python
vis.register_key_callback(ord("R"), rotate_callback)
```

ここで指定するキーは必ず**大文字**じゃないといけないというとこですね。 
ハマりましたが理由はちゃんと分かってないので誰か詳しい方お願いします。 
(C++の実装周りとの兼ね合いだと勝手に思ってますが...)

# 点群/メッシュの表示/非表示切り替え
ここまでの総括としてコールバックを使ってボタン一つでウサギさんを消したり出したりしましょう。 
大丈夫、[Visualizerのリファレンス](http://www.open3d.org/docs/python_api/open3d.visualization.html)を見るとちゃんとremove_geometryというメソッドが用意されているのでこれを使えば簡単に実現できそうです。

```python
>> "remove_geometry" in dir(open3d.Visualizer)
False
```

**いや、無いが？？？？？** 
[ココ](https://github.com/intel-isl/Open3D/issues/543)を見ると、「C++にはあるけどPythonにはそういうインタフェース今ないっす」と書かれています。リファレンスに載せないで。 
しかしありがたいことにPythonでできる代替手段も一緒に書かれているのでこちらを参考に実装してみます。

```python
import copy
import open3d
import numpy as np

# ウィンドウ初期化
vis = open3d.VisualizerWithKeyCallback()  # <-コールバック対応クラス
vis.create_window(
    window_name="Hoge",  # ウインドウ名
    width=800,           # 幅
    height=600,          # 高さ
    left=50,             # 表示位置(左)
    top=50               # 表示位置(上)
)

# ウサギを読み込んでコピーを取って表示用と分ける
base_mesh = open3d.read_triangle_mesh("bun_zipper.ply")
view_mesh = copy.copy(base_mesh)
vis.add_geometry(view_mesh)

# コールバックを登録
usagi_on = True
def usagi_callback(_vis):
    global usagi_on, view_mesh
    if usagi_on:
        view_mesh.vertices = open3d.Vector3dVector([])
        view_mesh.triangles = open3d.Vector3iVector([])
    usagi_on = not usagi_on
vis.register_key_callback(ord("U"), usagi_callback)

while True:
    # 毎フレーム処理
    if usagi_on:
        view_mesh.vertices = base_mesh.vertices
        view_mesh.triangles = base_mesh.triangles

    # 更新処理
    vis.update_geometry()
    vis.poll_events()
    vis.update_renderer()
```

これで'U'キーを押すたびにウサギさんが出たり消えたりします。 
ポイントは2点で、

```python
view_mesh = copy.copy(base_mesh)
vis.add_geometry(view_mesh)
```

ここでコピーを取って表示用と元データ用に分けて表示用だけVisualizerに登録しています。 
肝心のremove_geometryの代替処理は、

```python
view_mesh.vertices = open3d.Vector3dVector([])
view_mesh.triangles = open3d.Vector3iVector([])
```

の部分で、頂点と三角メッシュに空の値を代入しています。 
ちなみに、verticesとtrianglesはいずれもnp.asarrayで生データを見れますが、直接空にしようとするとShapeが違う！！！と怒られます。

# で、何ができるの？
以上、結構簡単に3Dモデル+RGB画像のNon-Blocking処理が実装できます。 
RGBカメラとcv2.arucoとかでARごっこも簡単にできると思うので、余裕ができたらまとめてみたいと思います。