[Python, Julia] Jupyter で 3D 表示 - Mayavi ライブラリ

【メモ2019/12/24。2019年末になっても本記事に「イイね」や「ストック」して頂きありがとうございます。以下の内容に基本は変わりませんが、Python 3.x、Julia 1.x の時代ですので、最新状況を追っていただくようにお願いします。 】

　[Jupyter Advent calendar 2016](http://qiita.com/advent-calendar/2016/jupyter) 12日目の記事です。
　Jupyter で 3Dグラフィックスを表示できます。Python の Mayavi ライブラリを使います。
　[Mayavi](http://docs.enthought.com/mayavi/mayavi/) は、Python用の 3次元グラフィックス表示ライブラリです。科学技術分野の可視化を念頭に開発された vtk をグラフィックスエンジンとしています。デモを見て頂くと、どのような絵が描けるか分かるでしょう。-> [デモ](http://docs.enthought.com/mayavi/mayavi/mlab.html), [MRI example](http://docs.enthought.com/mayavi/mayavi/auto/example_mri.html?highlight=mri)

# お膳立て

　Mayaviライブラリの出力を Jupyter で表示するためには、以下の二つの条件が整っている必要があります。

- Jupyter から呼ばれる Python の kernel において、Mayavi パッケージが利用可能である。
- Mayavi 向けの　nbextension が、jupyter にインストールされている。

　実は、このお膳立てが一番難しいです。表示結果に興味がある方は、この節の続きは飛ばして頂いて結構です。
 以下では、Mac OSX 上で、conda 環境を用いて、環境構築してみます。

## (1) Python2 と Mayavi インストール
　Mayavi の anaconda パッケージは、python 2 でのみ提供されていますので、現時点の最新版 Python 2.7 をインストールします。 
　Python の環境構築の概要は、[データサイエンティストを目指す人のpython環境構築 2016](http://qiita.com/y__sama/items/5b62d31cb7e6ed50f02c) にまとまっています。 複数の conda 環境と付き合うには注意が必要ですが、[pyenvとanacondaを共存させる時のactivate衝突問題の回避策3種類](http://qiita.com/y__sama/items/f732bb7bec2bff355b69) の一つめ、すなわち、「pyenv を用いて anaconda をインストールしたあと、pyenv を用いない」という方針で私は運用しています。当該記事の通り pyenv と anaconda をインストールしたら、Mayavi 向けの conda環境 (以下では py27mayavi とします) を作ります。

```shell
conda install -n py27mayavi numpy jupyter mayavi=4.4
source activate py27mayavi
```

　Mayavi は、用いるライブラリのバージョンに敏感です。Mayavi の最新版は 4.5 ですが、一つ前の 4.4 が安定しているように、私は感じています。 
　Mayavi を追加したら、Python から Mayavi が動くことを確かめておきます。3次元の絵が表示されたら成功です。

```python
from mayavi import mlab
mlab.test_plot3d()
```


## (2) Jupyter で conda 環境を使えるようにする。

　(1)で作成した conda環境 py27mayavi を、Jupyter から呼べるようにします。 
　起動中の Jupyter から複数の kernel (conda 環境)を切り替えて使うための設定方法が、記事 [Condaで作ってる仮想環境の切り替えをJupyter上で簡単に行う方法](http://qiita.com/yoppe/items/38005f415a5b8b884c7d#comment-a914efbb53ef23992ba1) で紹介されています。例えば、下図のように、New Notebook で起動する kernel を選べるので、便利です。
![スクリーンショット 2016-12-10 17.13.55.png](https://qiita-image-store.s3.amazonaws.com/0/103217/18134a80-4cd9-3510-d33d-1ac5a58b18c8.png)


## (3) Jupyter に nbextension を追加

　nbextensions も、conda を用いてインストールします。 こちらを参考に -> https://github.com/ipython-contrib/jupyter_contrib_nbextensions

```shell
conda install -n py27mayavi -c conda-forge jupyter_contrib_nbextensions
```

[Using Mayavi in Jupyter notebooks](http://docs.enthought.com/mayavi/mayavi/tips.html#using-mayavi-in-jupyter-notebooks) によると、Mayavi 向けの nbextensions をインストールするために、

```shell-session
jupyter nbextension install --py mayavi --user
```
と打ち込めと書いてありますが、これはうまく動きませんでした。しかし、Jupyter 内から、`mlab.init_notebook` を起動すると (後述)、Mayavi 向けの nbextensions が存在していますので、結果オーライとしましょう。

# 最も簡単な例 Python

　シェル (コマンドライン, ターミナル)から Jupyter を起動します。

````shell
jupyter notebook
```

　Mayavi をインストールした conda環境を kernel として、New notebook します。Mayavi を呼び出してみましょう。

　下の Jupyter Notebook: https://gist.github.com/anonymous/2c85c265cbee3c4485cfe39239593a11

````python
from mayavi import mlab
mlab.init_notebook()
s=mlab.test_plot3d()
s
```

　Mayavi でオブジェクトを表示するには、`mayavi.mlab` モジュールを通常用います。`mlab.init_notebook()` は Jupyter 上に Mayavi 画像を出力するための「おまじない」です。

![スクリーンショット 2016-12-10 17.28.32.png](https://qiita-image-store.s3.amazonaws.com/0/103217/c4875247-9856-17c1-be32-8402eb746285.png)

　出力セル `Out[*]` に、3次元表示向けの窓が開き、3次元オブジェクトが表示されました。左ボタンを押しながらのマウス操作でオブジェクトを回転・拡大縮小でき、Shiftキーと左ボタンを押しながらのマウス操作でオブジェクトを移動できます。


# 表示の仕組み

　3D表示の仕組みを、およそ以下のようになっていると思います。
　`mlab.init_notebook()` したあとに、`Notebook initialized with x3d backend` 「notebook は ３次元シーンを `x3d` 形式で扱うように設定された」というメッセージが出力されましたが、`x3d` は、３次元シーンを記述するためのフォーマット VRML (Virtual Reality Modeling Language) の後継です。Mayaviが３次元シーンを `x3d` 形式で出力するとすると、Jupyter notebook が WEB ブラウザに３次元画像を描画します。 HTML5 に準拠するmodern browser は、WebGL、すなわち、３次元グラフィックス規格 OpenGL のサブセットを表示する機能を備えていますから、これを利用するわけです。

# 鉄則 : Mayavi オブジェクトを出力せよ

　次に、簡単なプログラムを書いてみます。 表面を描く関数 `surf` の [テストプログラム](http://docs.enthought.com/mayavi/mayavi/auto/mlab_helper_functions.html#surf) です。

　下の Jupyter Notebook: https://gist.github.com/anonymous/954fdf1bebc9802a3e035092c1ac50ff

```Python
import numpy as np

def f(x, y):
    return np.sin(x + y) + np.sin(2 * x - y) + np.cos(3 * x + 4 * y)

x, y = np.mgrid[-7.:7.05:0.1, -5.:5.05:0.05]

from mayavi import mlab
mlab.init_notebook()
s = mlab.surf(x, y, f)

s
```

![スクリーンショット 2016-12-10 18.31.01.png](https://qiita-image-store.s3.amazonaws.com/0/103217/e0772d77-2188-316f-63fd-a22a1a631551.png)

　関数 `f` の様子が描画されました。
　この描画結果を得るには、最後の行に書かれた `s` が必要です。 これを削除すると何も描画されません。つまり、Jupyter の出力セルに Mayavi のオブジェクトを出力すると、それが 3次元に描画される仕組みになっているようです。

 それを踏まえて、次の例です。

　下の Jupyter Notebook: https://gist.github.com/anonymous/f55d3291bf778bd5fef98e0741d82d65

```Python
import numpy as np
np.random.seed(12345)
x = 4 * (np.random.random(500) - 0.5)
y = 4 * (np.random.random(500) - 0.5)
z = np.exp(-(x ** 2 + y ** 2))

from mayavi import mlab
mlab.init_notebook()

mlab.figure(1, fgcolor=(0, 0, 0), bgcolor=(1, 1, 1))
pts = mlab.points3d(x, y, z, z, scale_mode='none', scale_factor=0.2)
mesh = mlab.pipeline.delaunay2d(pts)
surf = mlab.pipeline.surface(mesh)

mlab.view(47, 57, 8.2, (0.1, 0.15, 0.14))
mlab.show()

mesh
surf
```

![スクリーンショット 2016-12-10 18.49.50.png](https://qiita-image-store.s3.amazonaws.com/0/103217/7c642142-ab1c-c120-c312-732f6cdb8ecc.png)

　３次元描画を得るためにには、プログラム末尾の 2行 `mesh` と `surf` が必要です。プログラム末尾で Mayavi オブジェクトを出力させると、Jupyter が、これらを拾って 3次元描画するわけです。


# Julia でも Mayavi on Jupyter

　[Julia](http://julialang.org) は、対話型で実行できるのに、極めて高い数値計算の実行性能を享受できる、新しいプログラミング言語です。 
　先週書いた拙文で、Mayavi を Julia で使う方法と、conda環境を Julia から操作する方法を解説しました。
* [3Dプロットライブラリ MayaVi を Julia から使う](http://qiita.com/tenfu2tea/items/b38c3c36c4771c963cc6) 
* [conda パッケージを Juliaから追加しよう](http://qiita.com/tenfu2tea/items/d2ac1427eaed7a548287)

## 最も簡単な例 Julia

　Jupyter 内部で Mayavi を呼び出すための、最も簡単な例を紹介します。
　
　下の Jupyter Notebook: 
https://gist.github.com/anonymous/b991896af47ac1ef3fed630b0922bd7a

```Julia
using PyCall
@pyimport mayavi.mlab as mlab
mlab.init_notebook()

mlab.test_points3d()
```

![スクリーンショット 2016-12-11 9.36.59.png](https://qiita-image-store.s3.amazonaws.com/0/103217/4db35c41-a979-bd9d-106a-278c38074f12.png)

　Julia から Python モジュールを使うためには `using PyCall` してから、使いたい Pythonモジュールを`@pyimport` で読み込みます。 Pythonの `import`命令は、Juliaの `@pyimport` 命令に相当します。 `@pyimport` で読み込まれたオブジェクトは Juliaの型がつくので、Julia内からそのまま呼び出すことができます。

```python
import mayavi.mlab as mlab     # Python
@pyimport mayavi.mlab as mlab  # Julia
```


## Spherical Harmonics

　[拙文](http://qiita.com/tenfu2tea/items/b38c3c36c4771c963cc6) で紹介した Spherical Harmonics (球面調和関数)の [例](http://qiita.com/tenfu2tea/items/b38c3c36c4771c963cc6#spherical_harmonics)を、Jupyter で実行してみます。

　下の Jupyter Notebook: 

```Julia
phi   = [ u1 for u1 in linspace(0,pi,101), v1 in linspace(0,2*pi,101) ]
theta = [ v1 for u1 in linspace(0,pi,101), v1 in linspace(0,2*pi,101) ]

r = 0.3
x = r * sin(phi) .* cos(theta)
y = r * sin(phi) .* sin(theta)
z = r * cos(phi)

using PyCall
@pyimport scipy.special as spe
@pyimport mayavi.mlab as mlab
mlab.init_notebook("x3d")

mlab.figure(1, bgcolor=(1, 1, 1), fgcolor=(0,0,0), size=(400, 300))
mlab.clf()
mlab.view(90, 70, 6.2, (-1.3, -2.9, 0.25))

u=false
for n in 1:6-1, m in 0:n-1
    s = real( spe.sph_harm(m, n, theta, phi) )
    mlab.mesh(x - m, y - n, z, scalars=s, colormap="jet")
    s[s .< 0] *= 0.97
    s /= maximum(s)
    u = mlab.mesh(s .* x - m, s .* y - n, s .* z + 1.3, 
        scalars=s, colormap="Spectral" )
    u
end
u
```

![スクリーンショット 2016-12-11 9.27.38.png](https://qiita-image-store.s3.amazonaws.com/0/103217/92103d80-515f-0e59-8e20-c9ae9d69c446.png)
![スクリーンショット 2016-12-11 9.29.22のコピー.png](https://qiita-image-store.s3.amazonaws.com/0/103217/072346b1-5aa3-1a79-4149-1375a3355e4d.png)

　ループ内で `mlab.mesh` は Mayavi オブジェクトを出力しますが、Jupyter は表示しません。ループの外で Mayavi オブジェクトを出力させてみたところ、3次元オブジェクトが表示されました。 `u` には、最後に作成された Mayavi オブジェクトだけ入っていますが、全景が描かれました。３次元描画のきっかけが存在すればよいように感じます。

# Mayavi 以外から 3D表示 (妄想中)

　3Dデータが埋め込まれた Jupyter notebook は、データ交換形式として大変魅力的です。Mayavi 以外からも利用できたらうれしいです。まだ方法を確立できていませんが、二点コメントします。
　一つ目、x3d形式の修正です。 Jupyter notebook (拡張子 `.ipynb`) をテキスト・エディタで開いてみると、x3d 形式のデータが埋め込まれています。(x3d で表現される 3Dオブジェクトは、線と面だけですから、ちょっとしたシーンでも長いテキストになります)

```text
  "outputs": [
   {
    "data": {
     "text/html": [
      "\n",
      "    <?xml version=\"1.0\" encoding =\"UTF-8\"?>\n",
      "\n",
      "<X3D profile=\"Immersive\" version=\"3.0\" id=\"scene_1\" >\n",
      "  <head>\n",
      "    <meta name=\"filename\" content=\"Stream\"/>\n",
      "    <meta name=\"generator\" content=\"Visualization ToolKit X3D exporter v0.9.1\"/>\n",
      "    <meta name=\"numberofelements\" content=\"1\"/>\n",
      "  </head>\n",
      "  <Scene>\n",
      "    <Background skyColor=\"0.5 0.5 0.5\"/>\n",
```

この x3d scene をエディタで修正して、Jupyter notebook で再び読み込むと、その通り 3D シーンが変わりました。ですから、別のプログラムから、x3d 形式でテキストを導入すれば、Mayavi 以外からも 3D 画像を表示できるはずです。 
　もう一点。 Jupyter notebook をHTML形式で保存したファイルを、Juopyterがインストールされていない環境でも ３次元描画すれば便利ですね。
さて、HTML形式で保存したファイル Jupyter notebook は x3d データを含んでいますが、これをブラウザで開いても 3D は描画されません。x3d 描画を指示する部分は、ここですが、ローカルファイルを指しているため、描画されなかったのでしょう。

```html
<script type="text/javascript">
require(["/nbextensions/mayavi/x3d/x3dom.js"], function(x3dom) { /**/
    var x3dom_css = document.getElementById("x3dom-css"); 
    if (x3dom_css === null) {
        var l = document.createElement("link");
        l.setAttribute("rel", "stylesheet");
        l.setAttribute("type", "text/css");
        l.setAttribute("href", "/nbextensions/mayavi/x3d/x3dom.css"); /**/
        l.setAttribute("id", "x3dom-css");
        $("head").append(l);
    }
    if (typeof x3dom != 'undefined') {
        x3dom.reload();
    }
    else if (typeof window.x3dom != 'undefined') {
        window.x3dom.reload();
    }
})
</script>
```

　nbextension 内の Mayavi プラグインへの参照を、x3dom.org に書き直してみます。
* `/nbextensions/mayavi/x3d/x3dom.js` を `http://www.x3dom.org/download/x3dom.js` に修正
* `/nbextensions/mayavi/x3d/x3dom.css` を `http://www.x3dom.org/download/x3dom.css` に修正

![スクリーンショット 2016-12-12 17.10.33.png](https://qiita-image-store.s3.amazonaws.com/0/103217/8e8a7c03-2093-71b0-f4d8-822542652a9d.png)

すると、3Dの描画エリアは描かれるようになったのですが、まだ 3Dオブジェクトは描かれていません。
　以上の２点、もう少し考えてみます。うまくいったら、追記したいです。
　最後は、妄想で終わってごめんなさい。