はじめに
本記事はシリーズになっています。他の記事はこちらから。
前の記事
前回はnapariの基本的な操作を解説しました。そこから一段階レベルアップして、今回の記事では
という2点についてまとめます。1.は少し退屈ですが、知っているほど今後キーバインドやウィジェットを自作するときに、より柔軟で使いやすい機能を追加できるようになります。少なくとも2.のキーバインドができるようになれば、一気に画像解析が効率化されます。
0. napariの起動
前回みたく、napariを起動し、viewerオブジェクトを用意します。
import napari
viewer = napari.Viewer()
1. viewerから様々なオブジェクトにアクセスする
当然ですが、napariは数多くの「構成要素」からなっています。napariの強みはカスタマイズのしやすさなので、これら構成要素をスクリプトから簡単に操作することができます。
ウィンドウに映っているものだけでいえば、次の図に示すようにviewerのattributeからアクセスできます。
重要なオブジェクト
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ではこれが結構面倒だったりしますね...)。
-
visiblebool: 見えるようにするかどうかをTrue/Falseでセットできる。 -
coloredbool: 色を付けるかどうかをTrue/Falseでセットできる。 -
font_sizefloat: フォントの大きさを指定する。 -
unitstr: 長さの単位を指定する。"u"は"µ"に直してくれる。"mm" ↔ "µm" ↔ "nm"のような変換もしてくれる。
viewer.text_overlay
テキストを重ねて表示するオブジェクトです。オーバーレイなので、画像の拡大縮小、3D回転、スライダーの移動に関係なく常に同じ位置に表示されます。napari-animationプラグインと組み合わせて画像のタイムスタンプを付加したり、一時的に何かの情報を表示する際に使うのが一般的かと思います。
-
visiblebool: 見えるようにするかどうかをTrue/Falseでセットできる。 -
color: 色を"white"のような名前かRGBA(strまたは配列)で指定する。 -
font_sizefloat: フォントの大きさを指定する。 -
positionstr: テキストの表示位置。"top_left"など。 -
textstr: テキストの内容。改行\nなども対応。
viewer.dims
ビューアーの次元に関する情報を格納したオブジェクトです。
-
ndimint: ビューアーの最大次元数。例えば3D+tの画像と3D画像があるとき、ndim==4となる。 -
ndisplayint: 2D表示なら2, 3D表示なら3になる。 -
rangetuple: 各次元の目盛りを表す。たとえば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_steptuple: 現在表示している座標。例えば3D画像のz=3の断面を表示しているときは(3, 0, 0)となる。 -
axis_labelstuple: 各軸の名前。ここで指定した文字列がそのまま座標軸やスライダーの横に表示される。xy座標なら("y", "x")となる。 -
ordertuple: 軸の順番。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]番目の要素を:に変えればスライスが作れます。
# スライスの作成
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
カーソルオブジェクトです。カーソル位置を利用する以外使い道がない気が...。
-
positiontuple: カーソルの位置。小数の精度で取得できる。レイヤーのscaleを指定している場合、必ずしもピクセルの位置とは対応しないので注意。ビューアーがn次元ならn個の小数が返ってくる。
viewer.camera
カメラオブジェクトです。空間に画像が浮かんでいて、その画像をこのカメラに移して、我々がモニター上でそれを見ているイメージです。大きな画像の特定部位をアップで表示させたいときに使うくらいでしょうか。
-
anglestuple: カメラの角度。オイラー角で$(r_x, r_y, r_z)$の順番に並んでいる。2D表示では値を変えても次の操作ですぐに戻ってしまう。3D表示用のパラメータ。 -
centertuple: カメラの中心座標。2D画像に平行な面上をカメラが平行移動するイメージ。画像の$(y_0,x_0)$のピクセルに合わせたければcenterに$(y_0,x_0)$を代入する。 -
zoomfloat: カメラのズーム倍率。
その他
-
viewer.window.main_menu: メニューバーのウィジェット。viewer.window.my_menu = viewer.window.main_menu.addMenu("&My Functions")みたくカスタムのメニューを追加できる。 -
viewer.window.qt_viewer.layerButtons: レイヤー関係のボタンを格納したウィジェット。
2. カスタムキーバインドで効率的に解析する
画像解析では、領域選択など、手動で行うほうが圧倒的に効率が良いことがあります。しかし、ビューアーとスクリプトを行ったり来たりするのは面倒ですし、カーソルの位置を利用したかったらうまいことやらないとそもそもスクリプトに戻れないので、キーバインドは必須とも言えます。
napariではキーバインドの需要がちゃんと理解されており、非常に簡単にキーバインドを作れます。以下それについて解説します。
※ 公式ドキュメントでは、例えばこちらに例があります。
使えるキー
アルファベットと記号は基本的に全て使えます。特殊なキーに関しては、以下の文字列を与えることになります。
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"のように書きます。
Ctrlと特殊キーなど、OSによっては固有のキーバインドが優先されるので、変な組み合わせは避けましょう。
キーバインドする対象
自作関数のバインドの対象として、napariでは3通り選べます。
-
napari.Viewerにバインドする。 - 1種類のレイヤー、例えば
Shapesレイヤーにバインドする。 - ビューアーに追加された特定の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に文字を表示し、もう一度押したら消えるというキーバインドです。
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ではそれを避けるために、デフォルトでは上書きを禁止しています。間違った関数をバインドした場合などは
@viewer.bind_key("F1", overwrite=True)
def func(viewer):
...
というように、overwrite=Trueで明示的に上書きを許可します。
押している間だけ発動するキーバインド
特定のキーを押している間にビューアーのプロパティを書き換え、離したら元に戻すという、withブロック的なことができると便利ですよね。実は関数にyieldを入れるだけでこれができます。次の例は、F2を押している間だけtext_overlayに文字が表示されるキーバインドです。
@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していくキーバインドになります。グローバル変数に残るので、後から解析できます。
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レイヤーすべてにバインドする
クラスメソッドとして呼び出します。
@napari.layers.Shapes.bind_key("F1")
def print_coordinates(layer):
print(layer.data)
3. 特定の1枚のShapesレイヤーにバインドする
viewer.layers[0]のようにビューアーからアクセスしてもよいですが、キーバインド専用のレイヤーを追加する際のadd_shapesの返り値を利用するのが楽です。
# 空のShapesレイヤーを追加する。
shapes = viewer.add_shapes(ndim=2)
@shapes.bind_key("F1")
def print_last_coordinates(layer):
print(layer.data[-1])
終わりに
キーバインドを登録すれば、手動の強みとPythonスクリプト上での解析の利点が組み合わせられるので、画像解析が捗りますね!
