Bokeh対話機能を用いた画像処理結果の可視化

# 本日は

ちょっとタイトルが悪いですが，Bokehの対話機能，つまり，スライダーを用いてパラメータを動かせるようなアプリケーションの練習として，画像処理の様子を可視化しましょう．

題材は　scipy レクチャーノートから拝借します．

- [2.6. Numpy と Scipy を利用した画像の操作と処理](http://www.turbare.net/transl/scipy-lecture-notes/advanced/image_processing/index.html)

例えば

- [Image sharpening](http://www.turbare.net/transl/scipy-lecture-notes/advanced/image_processing/auto_examples/plot_sharpen.html#example-plot-sharpen-py)

において，　画像をシャープにするコードがあります．


```py3
#一部抜粋
f = scipy.misc.face(gray=True).astype(float)
blurred_f = ndimage.gaussian_filter(f, 3)

filter_blurred_f = ndimage.gaussian_filter(blurred_f, 1)

alpha = 30
sharpened = blurred_f + alpha * (blurred_f - filter_blurred_f)
```

上記のコードにある`alpha` と `blurred_f = ndimage.gaussian_filter(f, 3)` の３に当たる値を`sigma`とし，これらを動かした時の画像の変化の様子を可視化しましょう．

# 実装例

上でロジックは何をするべきかを決めましたので，　可視化部分をbokehを用いて実装してみましょう．個人的にも実務でも再利用していきたいというのもあって，真面目に可視化用のclassを実装しています．
（bokehもmatplotlibでもそうなんですが，サンプルがベタがきで書いてあってオブジェクト指向で実装をしていないのが若干不満です．そんな人たちは自分で実装せいやという事なのだろうか？）

```py3:sharpe.py
from scipy import ndimage as ndi
import numpy as np
import scipy
from bokeh.io import curdoc
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import Slider, TextInput
from bokeh.layouts import row, widgetbox
from bokeh.layouts import gridplot
from bokeh.client import push_session


class ImageViewer():

    def __init__(self, target):
        self.target = target[::-1]
        self.source1 = ColumnDataSource(data=dict(image=[self.target]))
        self.alpha = Slider(title="alpha", value=30,
                            start=10, end=50, step=1)
        self.sigma = Slider(title="sigma", value=3,
                            start=1, end=20, step=1)

        self.fig1 = self.define_figure('image')
        self.regist_image(self.fig1,self.source1)

        blurred = ndi.gaussian_filter(self.target, sigma=self.sigma.value)
        self.source2 = ColumnDataSource(data=dict(image=[blurred]))
        self.fig2 = self.define_figure('blurred')
        self.regist_image(self.fig2,self.source2)


        filtered = ndi.gaussian_filter(blurred, sigma=1)
        sharped = blurred+self.alpha.value*(blurred-filtered)
        sharped = sharped.astype(np.uint8)
        self.source3 = ColumnDataSource(data=dict(image=[sharped]))
        self.fig3 = self.define_figure('sharped')
        self.regist_image(self.fig3,self.source3)

        widget_list = [self.alpha, self.sigma]
        for widget in widget_list:
            widget.on_change('value', self.update_data)
        inputs = widgetbox(*[widget_list])
        self.plot = row(inputs, gridplot(
            [[self.fig1, self.fig2, self.fig3]]), width=600)

    def define_figure(self, title):
        return figure(title=title,
                      plot_width=self.target.shape[1]//2,
                      x_range=[0, self.target.shape[1]],
                      plot_height=self.target.shape[0]//2,
                      y_range=[0, self.target.shape[0]])

    def regist_image(self, fig, source):
        fig.image('image',
                  x=0, y=0,
                  dh=self.target.shape[0], dw=self.target.shape[1],
                  source=source, palette='Greys256')

    def update_data(self, attr, old, new):
        blurred = ndi.gaussian_filter(self.target, sigma=int(self.sigma.value))
        filtered = ndi.gaussian_filter(blurred, sigma=1)
        sharped = blurred+self.alpha.value*(blurred-filtered)
        sharped = sharped.astype(np.uint8)
        self.source2.data = dict(image=[blurred])
        self.source3.data = dict(image=[sharped])
        self.fig1.title.text = '{} {}'.format(
            self.sigma.value, self.alpha.value)


def main():
    target = scipy.misc.face(gray=True)
    viewer = ImageViewer(target)
    document = curdoc()
    document.add_root(viewer.plot)

main()
```

実装の方針としては次の通り，

- 画像処理対象画像オブジェクトをインスタンスの引数とするクラスを作ります．
- そして `__init__` 内部でクリクリ動かすスライダー， 画像を表示する為の領域の確保のためにfigureオブジェクトを作ります（`define_figure`　メソッド）．
- `update_data` メソッドでスライダーをクリクリ動かすごとにどういう動作をさせるかを定義しています．
- matplotlibで言う所の　`ax.imshow()` に相当するのが `regist_image`　メソッドです．
- `main` 関数内で　`document` を定義してそこに `plot` を　add する．

適宜メソッドの内部を書き換える事で対話機能を用いた可視化ツールを作る事ができるでしょう．

# 実行例

```shell-session
$ bokeh serve --show sharpe.py
```

ブラウザが立ち上がり下図のようなものが出ます．

<img width="1198" alt="スクリーンショット 0029-11-21 午後7.09.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/135978/ecec97b3-da25-6eb5-b51a-6225183788d3.png">

結果がレクチャーノートと微妙に異なるのは
`f = scipy.misc.face(gray=True).astype(float)　`としていないからです．
修正は読者の練習問題とします．

ともかく，ここでは左上にあるスライダーを動かして変化の様子をみてみましょう．グリグリ動かすと画像が変化していきます．

# matplotlib+ipywidgetsと比べて

これぐらいのツールであれば　jupyter notebook を起動して　ipywidgets と matplotlib の描画機能を利用してできるのですが，　スライダーを動かした時の挙動が結構カクカクします．
体感的としては今回のbokehでの実装の方が相対的にスムーズに動きます．とはいえ超絶ヌルヌル動くかといえばそうでもないです．

jupyter notebook で物足りない方はbokehにトライしてみてくださいまし．

# 参考文献

- [2.6. Numpy と Scipy を利用した画像の操作と処理](http://www.turbare.net/transl/scipy-lecture-notes/advanced/image_processing/index.html)
- [Image sharpening](http://www.turbare.net/transl/scipy-lecture-notes/advanced/image_processing/auto_examples/plot_sharpen.html#example-plot-sharpen-py)
- [slider.py (Bokeh)](https://bokeh.pydata.org/en/latest/docs/gallery/slider.html)
- [image.py(Bokeh)](https://bokeh.pydata.org/en/latest/docs/gallery/image.html)