【ImageJからPythonへ】napariの使い方 (2) 

# はじめに

本記事はシリーズになっています。他の記事はこちらから。

##### 前の記事

- [【ImageJからPythonへ】napariの使い方 (1)](https://qiita.com/Hanjin_Liu/items/fcdd9dbbc9fa4fd7df18#comment-5f32ab632b97abf18435)

前回は`napari`の基本的な操作を解説しました。そこから一段階レベルアップして、今回の記事では

1. [viewerから様々なオブジェクトにアクセスする](#1-viewerから様々なオブジェクトにアクセスする)
2. [カスタムキーバインドで効率的に解析する](#2-カスタムキーバインドで効率的に解析する)

という2点についてまとめます。1.は少し退屈ですが、知っているほど今後キーバインドやウィジェットを自作するときに、より柔軟で使いやすい機能を追加できるようになります。少なくとも2.のキーバインドができるようになれば、一気に画像解析が効率化されます。

# 0. `napari`の起動

前回みたく、`napari`を起動し、`viewer`オブジェクトを用意します。

```python
import napari
viewer = napari.Viewer()
```

# 1. `viewer`から様々なオブジェクトにアクセスする

当然ですが、`napari`は数多くの「構成要素」からなっています。`napari`の強みはカスタマイズのしやすさなので、これら構成要素をスクリプトから簡単に操作することができます。

ウィンドウに映っているものだけでいえば、次の図に示すように`viewer`のattributeからアクセスできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1752357/a33518b2-009c-43fb-70fc-5c3a40fcecbf.png)

### 重要なオブジェクト

##### `viewer.layers`

`list`のような形でレイヤーを格納しているオブジェクトです。前回も出てきましたが、`list`のように`viewer.layers[0]`で要素にアクセスできるほか、`viewer.layers["name"]`のように`dict`のように要素にアクセスすることもできます。

- `selection`: 選択されているレイヤーを`set`のような形式で返す。
- `remove(name)`: `name`という名前のレイヤーを削除。存在しない名前を指定した場合は`ValueError`が吐かれる。
- `remove_selected()`: 選択されているレイヤーを削除。
- `select_all()`, `select_next()`, `select_previous()`: 関数名の通り、それぞれすべてのレイヤー、一つ次のレイヤー、一つ前のレイヤーを選択する。
 
##### `viewer.scale_bar`

スケールバーを表示するオブジェクトです。画像の拡大縮小に合わせてちょうどよい長さに合わせてくれるので非常に便利です（ImageJではこれが結構面倒だったりしますね...）。

- `visible` *bool*: 見えるようにするかどうかを`True`/`False`でセットできる。
- `colored` *bool*: 色を付けるかどうかを`True`/`False`でセットできる。
- `font_size` *float*: フォントの大きさを指定する。
- `unit` *str*: 長さの単位を指定する。"u"は"µ"に直してくれる。"mm" &harr; "µm" &harr; "nm"のような変換もしてくれる。

##### `viewer.text_overlay`

テキストを重ねて表示するオブジェクトです。オーバーレイなので、画像の拡大縮小、3D回転、スライダーの移動に関係なく常に同じ位置に表示されます。napari-animationプラグインと組み合わせて画像のタイムスタンプを付加したり、一時的に何かの情報を表示する際に使うのが一般的かと思います。
    
- `visible` *bool*: 見えるようにするかどうかを`True`/`False`でセットできる。
- `color`: 色を`"white"`のような名前かRGBA（`str`または配列）で指定する。
- `font_size` *float*: フォントの大きさを指定する。
- `position` *str*: テキストの表示位置。`"top_left"`など。
- `text` *str*: テキストの内容。改行`\n`なども対応。

##### `viewer.dims`

ビューアーの次元に関する情報を格納したオブジェクトです。

- `ndim` *int*: ビューアーの最大次元数。例えば3D+tの画像と3D画像があるとき、`ndim==4`となる。
- `ndisplay` *int*: 2D表示なら`2`, 3D表示なら`3`になる。
- `range` *tuple*: 各次元の目盛りを表す。たとえば`range[0]==(0.0, 4.5, 0.5)`であれば、0番目の次元は0.0から4.5の間を0.5間隔で取る。これがz軸であるとすれば、インデックス$0, 1, 2, ...$にたいして実際の空間でのz座標は$z=0.0, 0.5, 1.0, ...$となる。
- `current_step` *tuple*: 現在表示している座標。例えば3D画像のz=3の断面を表示しているときは`(3, 0, 0)`となる。
- `axis_labels` *tuple*: 各軸の名前。ここで指定した文字列がそのまま座標軸やスライダーの横に表示される。xy座標なら`("y", "x")`となる。
- `order` *tuple*: 軸の順番。Ctrl+EやCtrl+Tで見る向きを変えたときに関係してくる。少し分かりづらいが、例えば`order=(2, 0, 1)`であれば、本来$(z,y,x)$という順番が$(x,z,y)$に変わる。一般に、`axis_labels`が`tuple(axis_labels[i] for i in order)`に変わると考えるとよい。

`current_step`と`order`を用いると、スタック画像から現在見えている2D画像を取ってくることができます。`order`の最後の2つの整数がそれぞれ$y,x$軸が何番目の次元に対応するかを意味するので、`current_step`の`order[-2]`番目と`order[-1]`番目の要素を`:`に変えればスライスが作れます。

```python
# スライスの作成
sl = list(viewer.dims.current_step)
for i in viewer.dims.order[-2:]:
    sl[i] = slice(None)
sl = tuple(sl)

# arrayに入れる
layer = viewer.layers[0]
img2d = layer.data[sl]
```

##### `viewer.axes`

座標軸の表示に関する情報を格納したオブジェクトです。基本的にメニューバーの"View > Axes"から変更するため、スクリプトからアクセスすることはほとんどないので省略します。

##### `viewer.cursor`

カーソルオブジェクトです。カーソル位置を利用する以外使い道がない気が...。

- `position` *tuple*: カーソルの位置。小数の精度で取得できる。**レイヤーのscaleを指定している場合、必ずしもピクセルの位置とは対応しないので注意**。ビューアーがn次元ならn個の小数が返ってくる。

##### `viewer.camera`

カメラオブジェクトです。空間に画像が浮かんでいて、その画像をこのカメラに移して、我々がモニター上でそれを見ているイメージです。大きな画像の特定部位をアップで表示させたいときに使うくらいでしょうか。

- `angles` *tuple*: カメラの角度。オイラー角で$(r_x, r_y, r_z)$の順番に並んでいる。2D表示では値を変えても次の操作ですぐに戻ってしまう。3D表示用のパラメータ。
- `center` *tuple*: カメラの中心座標。2D画像に平行な面上をカメラが平行移動するイメージ。画像の$(y_0,x_0)$のピクセルに合わせたければ`center`に$(y_0,x_0)$を代入する。
- `zoom` *float*: カメラのズーム倍率。

##### その他

- `viewer.window.main_menu`: メニューバーのウィジェット。`viewer.window.my_menu = viewer.window.main_menu.addMenu("&My Functions")`みたくカスタムのメニューを追加できる。

- `viewer.window.qt_viewer.layerButtons`: レイヤー関係のボタンを格納したウィジェット。


# 2. カスタムキーバインドで効率的に解析する

画像解析では、領域選択など、手動で行うほうが圧倒的に効率が良いことがあります。しかし、ビューアーとスクリプトを行ったり来たりするのは面倒ですし、カーソルの位置を利用したかったらうまいことやらないとそもそもスクリプトに戻れないので、キーバインドは必須とも言えます。

`napari`ではキーバインドの需要がちゃんと理解されており、非常に簡単にキーバインドを作れます。以下それについて解説します。

※ 公式ドキュメントでは、例えば[こちら](https://napari.org/tutorials/fundamentals/viewer.html)に例があります。

### 使えるキー

アルファベットと記号は基本的に全て使えます。特殊なキーに関しては、以下の文字列を与えることになります。

    Shift, Control, Alt, Meta, Up, Down, Left, Right, PageUp, PageDown, Insert, 
    Delete, Home, End, Escape, Backspace, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, 
    F11, F12, Space, Enter, Tab

キーの組み合わせは、例えば`"Control-Shift-X"`のように書きます。

:warning: Ctrlと特殊キーなど、OSによっては固有のキーバインドが優先されるので、変な組み合わせは避けましょう。

### キーバインドする対象

自作関数のバインドの対象として、`napari`では3通り選べます。

1. `napari.Viewer`にバインドする。
2. 1種類のレイヤー、例えば`Shapes`レイヤーにバインドする。
3. ビューアーに追加された特定の1枚のレイヤー、例えば`viewer.layers[0]`にバインドする。

|バインドの対象|バインドする関数の引数|アクセス範囲|キーバインドが利く条件|
|:-:|:-:|:-:|:-:|
|`napari.Viewer`|ビューアー|`layers`, `scale_bar`, `cursor`など含むビューアー全体|常時|
|レイヤー1種類|レイヤー|レイヤー内のみ|その種類のレイヤーが選択されている間|
|特定の1枚のレイヤー|レイヤー|レイヤー内のみ|その1枚のレイヤーが選択されている間|

汎用性は当然1.が最も高いですが、例えば`Image`レイヤーでしか使わないような関数を1.でバインドするとコードが煩雑になってバグも増えます。また、例えば`Shapes`レイヤーにバインドする場合でも、バインドする関数を呼び出す用のレイヤーと単に図形を描きたいだけのレイヤーで分ける場合は、後者でむやみに関数が呼び出されないようにしたいです。3通りのバインドの仕方は、よく考えてうまく使い分けることが重要です。

引数が変わるだけなので、1.で主に解説します。

### キーバインドの定義の仕方

`napari.Viewer`のメソッド`bind_key`を使って関数を登録します。登録された関数はプロパティの`key_map`に`append`されます。`bind_key`は関数を引数にとるので、デコレータで書くのが最もシンプルで分かりやすいです。

次の例は、F1を押したときに`text_overlay`に文字を表示し、もう一度押したら消えるというキーバインドです。

```python
viewer = napari.Viewer()
@viewer.bind_key("F1")
def func(viewer):
    if viewer.text_overlay.visible:
        viewer.text_overlay.visible = False
        viewer.text_overlay.text = ""
    else:
        viewer.text_overlay.visible = True
        viewer.text_overlay.text = "Pushed!"
```

関数名`func`はこのまま使うことはもうないので、面倒なら`_`にするか、コメントアウト代わりに`switch_text_overlay`みたくただひたすらに分かりやすい長い名前にするのがよいかと思います。

### キーバインドの上書き

不注意でキーバインドを上書きしてしまうと困る場合があります。`napari`ではそれを避けるために、デフォルトでは上書きを禁止しています。間違った関数をバインドした場合などは

```python
@viewer.bind_key("F1", overwrite=True)
def func(viewer):
    ...
```

というように、`overwrite=True`で明示的に上書きを許可します。

### 押している間だけ発動するキーバインド

特定のキーを押している間にビューアーのプロパティを書き換え、離したら元に戻すという、`with`ブロック的なことができると便利ですよね。実は関数にyieldを入れるだけでこれができます。次の例は、F2を押している間だけ`text_overlay`に文字が表示されるキーバインドです。

```python
@viewer.bind_key("F2")
def func(viewer):
    viewer.text_overlay.visible = True
    viewer.text_overlay.text = "Pushing"

    yield # 押している間、ここで止まる

    viewer.text_overlay.visible = False
    viewer.text_overlay.text = ""
```

### 実用的なキーバインドの例

実用的な例も紹介しておきます。次の例は、ビューアーに画像を追加し、カーソルの位置の周囲$5\times5$ピクセルの平均値をグローバル変数`results`にappendしていくキーバインドになります。グローバル変数に残るので、後から解析できます。

```python
viewer = napari.Viewer()

# ランダムノイズ画像を作成してビューアーに送る
arr = np.random.random((256, 256))
viewer.add_image(arr)

# 結果を格納するリスト
results = []

@viewer.bind_key("m", overwrite=True)
def measure_local_mean(viewer):
    # カーソルの位置の整数値を取得
    y, x = viewer.cursor.position
    yc = int(y)
    xc = int(x)
    
    # 5x5画像を取得
    img = viewer.layers[0].data
    img0 = img[yc-2:yc+3,xc-2:xc+3]

    # 平均値をappend
    mean_intensity = np.mean(img0)
    results.append(mean_intensity)
    
    # 無反応だと不安なので、平均値を左下に表示しておく
    viewer.status = mean_intensity

```

他にも、`Shapes`レイヤーと`Image`レイヤーを組み合わせて、長方形で囲った領域の平均値を測るなど、かなり自由にカスタマイズできます。ビューアーを引数にとるので、キーバインドからレイヤーを追加することもできます。

### レイヤーにバインドする

最後にレイヤーにバインドする場合 (2., 3.) の書き方だけ紹介します。`Shapes`レイヤーを例にとります。

##### 2. `Shapes`レイヤーすべてにバインドする

クラスメソッドとして呼び出します。

```python
@napari.layers.Shapes.bind_key("F1")
def print_coordinates(layer):
    print(layer.data)
```

##### 3. 特定の1枚の`Shapes`レイヤーにバインドする

viewer.layers[0]のようにビューアーからアクセスしてもよいですが、キーバインド専用のレイヤーを追加する際の`add_shapes`の返り値を利用するのが楽です。

```python
# 空のShapesレイヤーを追加する。
shapes = viewer.add_shapes(ndim=2)

@shapes.bind_key("F1")
def print_last_coordinates(layer):
    print(layer.data[-1])
```

# 終わりに

キーバインドを登録すれば、手動の強みとPythonスクリプト上での解析の利点が組み合わせられるので、画像解析が捗りますね！