6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2021-08-09

はじめに

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

前の記事

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

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

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

0. napariの起動

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

import napari
viewer = napari.Viewer()

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

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

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

image.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" ↔ "µ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_labelstuple(axis_labels[i] for i in order)に変わると考えるとよい。

current_steporderを用いると、スタック画像から現在見えている2D画像を取ってくることができます。orderの最後の2つの整数がそれぞれ$y,x$軸が何番目の次元に対応するかを意味するので、current_steporder[-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"のように書きます。

: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_mapappendされます。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スクリプト上での解析の利点が組み合わせられるので、画像解析が捗りますね!

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?