はじめに
本記事はシリーズになっています。他の記事はこちらから。
前の記事
前回は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ではこれが結構面倒だったりしますね...)。
-
visible
bool: 見えるようにするかどうかをTrue
/False
でセットできる。 -
colored
bool: 色を付けるかどうかをTrue
/False
でセットできる。 -
font_size
float: フォントの大きさを指定する。 -
unit
str: 長さの単位を指定する。"u"は"µ"に直してくれる。"mm" ↔ "µm" ↔ "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]
番目の要素を:
に変えればスライスが作れます。
# スライスの作成
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
ではキーバインドの需要がちゃんと理解されており、非常に簡単にキーバインドを作れます。以下それについて解説します。
※ 公式ドキュメントでは、例えばこちらに例があります。
使えるキー
アルファベットと記号は基本的に全て使えます。特殊なキーに関しては、以下の文字列を与えることになります。
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スクリプト上での解析の利点が組み合わせられるので、画像解析が捗りますね!