はじめに
ImageJ/Fijiにはしばらくお世話になっていたのですが、scipy
やscikit-image
、機械学習関連などといったPythonの豊富なライブラリが利用できず、何か実装したかったらJavaで書くしかありません。これは、実験者と開発者の分断を助長する、あまりにもよろしくない事態です。加えて、ImageJとExcelを行き来せざるを得なかったり、ウィンドウが増えすぎたりと、使っていてかなり不便だと感じる場面が少なくないと思います。
この現状を打破するべく調査していて行きついたのがnapari
という素晴らしいPythonのライブラリでした。これがあれば、すべての解析をPythonで統一できます。ぜひ知ってもらいたいので、複数回に分けてまじめに紹介していこうと思います。
目次
napariとは
Pythonで画像を表示する際、Matplotlibでもいいのですが、拡大縮小したり、スライダーを動かして動画として見たり、立体的にぐるぐるしたりしたいですよね。napariは
napari is a fast, interactive, multi-dimensional image viewer for Python
と謳っている通り、快適に多次元画像を可視化することができます。詳しくは
をお読みください。ここでは重要なところに絞って使い方を説明していきます。
※ Julia言語からでも使えます。GitHubのこちらに上がっています。JuliaImagesを使い慣れているJulianの方は参考までに。
※ scikit-image
もviewer
というサブモジュールを有していますが、バージョン0.20で消されるので他のを使ってねと言っており、その中でnapariが紹介されています(こちら)。
この記事はnapariのバージョン0.4.10現在に関するものです。ホットに開発が進んでいるので、これから新機能の追加や変更があるかと思います。
napariでできること
ImageJとの比較みたいになりますが、主に次のような特徴が挙げられます。
1. 1つのウィンドウ内で、画像やラベル、長方形などのオブジェクトをレイヤーとして複数重ねることができる。
これでウィンドウが増えすぎることが防げるうえ、複数の画像を重ねながら比較する際に毎回"Merge Channel"とかやらずに済むので非常に便利です。
2. キーボードショートカット、マウスのクリック時・ドラッグ時の機能を非常に簡単に登録でき、レイヤーごとに設定することもできる。
例えば、画像の特定領域を切り出すのはGUI頼みなので、そこからscipy.optimize
でフィッティングにスムーズに繋げることが困難でしたが、"F"ボタンにフィッティングを行う関数を登録しておけば解決します。ソフトを使い分ける時代はもう終わりで、これからは必要に応じて素早くUIを作り変える時代です。
3. 手動で選択したレイヤーやオブジェクトの情報をコンソールから対話的に取得できる。
2.とも関連しますが、Python上での画像解析の弱点は、人間が目で見て判断する操作は自動化できないという点です。細胞の領域を取り出してくる、結晶の周期構造がx軸に平衡になるように回転させる、といった操作くらいは人間がやることだと思います。これを毎回matplotlib
でプロットしてはおおよその値を入力するのでは効率が悪いですが、napari
を通して行えばずっとスムーズになります。
4. なんならImageJを呼び出せる。
pyimagej
というものをインストールするとPythonからImageJを起動し、データをやり取りできます(需要はあるのか?)。環境構築が一筋縄では行かなかったのですぐにやめました。
インストール
画像解析をするならcondaを使いましょう。
conda install napari -c conda-forge
PyQtなどが最新のもの (>=5.12) になっていないと思うように動作しないかもしれないので、conda-forgeチャネルの最新のものにしておきましょう。
conda install pyqt -c conda-forge
基本操作
Viewerを立ち上げて画像を表示
モジュールをインポートします。
import napari
import numpy as np
import skimage
napari.Viewer
クラスが、napari
のウィンドウ本体になります。ウィンドウ内にあるものはすべてここからアクセスします。次のようにインスタンスviewer
を作成します。
viewer = napari.Viewer()
napari.run() # Jupyterであれば、これは不要
インスタンスが作成された時点で、図のようにnapari
のウィンドウが開きます。
(初期設定によって多少レイアウトが変わる可能性あり)
続いて画像を追加します。napari.Viewer
のメソッドadd_image
を使います。
img = skimage.data.camera() # 画像を取得
viewer.add_image(img)
[Out] <Image layer 'img' at 0x...>
これでウィンドウに画像が表示されました!ドラッグで平行移動、スクロールで拡大縮小できます。
ちなみに、ウィンドウを立ち上げるだけのことはほとんどないので、同時に画像を送る関数view_image
も用意されています。
viewer = napari.view_image(img) # 新しいウィンドウを立ち上げてそこに画像を送る
ウィンドウ上での簡単な操作
napariはレイヤーを追加していく形をとります。次のGIFのように、"layer list"ウィジェット内にあるボタンから順にPoints
レイヤー、Shapes
レイヤー、Labels
レイヤーを追加できます。
各レイヤーは選択中に、左上の"layer control"ウィジェットから固有の操作ができます。例えば、
-
Points
...matplotlib
の操作を手動で行うイメージ- 新しい点の追加/削除(3次元的にもできる!)
- 点の移動
- 点の形の変更
-
Shapes
... PowerPointのイメージ- 長方形、折れ線、楕円などのオブジェクトを追加/削除
- オブジェクトの選択、拡大/縮小、回転
- オブジェクト頂点の追加/削除
-
Labels
... ペイントのイメージ- 色を塗ったり消したりする(異なる色は異なる自然数のラベルに対応する)
- 塗りつぶし
- カラーピック
- 3次元描画(!)
などができます。ショートカットの整備が不十分なのと、一部Ctrl+Zが利かないのがこれからの課題ですが、欲しい機能はだいたい揃っている感じがしますね。
他にも、上部のメニューバーから座標軸やスケールバーを表示したり、スクリーンショットと撮ったりできます。
画像の3D表示
napari
が得意とする3D表示をやってみます。今回はメニューバーFile > Open Sample > scikit-image > Binary Blobs (3D)
とすることでサンプル画像を取得します。scikit-image
のサンプル画像を全部ダウンロードしている場合は"Brain (3D)"や"Cells (3D+2Ch)"など、もっとマシな画像が使えるかと思います。
デフォルトでは2D表示なので、下部のスライダーを動かして断面をみる形になります。3D関係の操作は"layer list"の下のボタンから行います。
3Dに切り替えると、"Binary Blobs (3D)"だとほぼ真っ白になってしまいます(バイナリ画像なので)。これは、レンダリングが"mip (max intensity projection)"になっているためで、バイナリ画像の場合"iso (iso-surface)"に設定すると見やすくなります。ぐるぐる回しましょう。
プラグインの導入
napari
は機能追加のしやすさが売りなので、当然プラグインシステムが既に確立しています。上部のメニューバーのPlugins >> Install/Uninstall Package(s)...
を選択すると、次の図に示すように新しいウィンドウに現在手に入るプラグインや更新可能なプラグインなどが表示され、そのまま手動でpipインストールできます。
簡単にアニメーションを作れるnapari-animationは特におすすめです。
スクリプトからのレイヤーの追加
続いては、スクリプトからレイヤーを追加していく方法について詳しく説明します。すでに述べたようにadd_image
を用いることでとりあえず画像をnapariに送ることができますが、実際には異なる色でマージした状態で送ったり、画像ではなく点や長方形を追加したりしたいこともあります。napari
には多彩なレイヤーが用意されているので、状況に応じて使い分けられるとよいですね。
より詳細な情報はこちらを参照。
0. 各レイヤー共通のパラメータ
レイヤーはすべてLayer
クラスを継承しており、レイヤーの種類にかかわらず、共通したプロパティがあります。これらはキーワード引数も共通しているので、初めにまとめてしまいます。
詳細を表示
name=None, metadata=None, scale=None, translate=None, rotate=None, shear=None,
affine=None, opacity=1, blending='translucent', visible=True, multiscale=False
scale=(0.5, 0.2)
なら、y軸で50%、x軸で20%のスケールがかかり、縦長のピクセルになる(行/列はy/xの順に対応することに注意!)。translate=(3,5)
なら、下に3ピクセル、右に5ピクセル移動して画像が表示される。shear=[a]
とするとx軸が$y=ax$方向に向いたように画像がつぶれる。scale
, translate
, rotate
, shear
はアフィン変換行列を分かりやすく分解したものなので、それらを指定するのとaffine
を指定するのは数学的には同義となる。add_image([img, img[::4, ::4]], multiscale=True)
のように与えれば、ズームアウトしていくと途中で勝手に4ピクセルごとの表示に切り替わる。
なお、レイヤーの有効な利用例がGitHubリポジトリのこちらにまとまっています。
1. Image layer
viewer.add_image(...)
で配列を画像として追加します。
詳細を表示
add_image(data=None, *, channel_axis=None, rgb=None, colormap=None,
contrast_limits=None, gamma=1, interpolation='nearest', rendering='mip',
iso_threshold=0.5, attenuation=0.05, name=None, metadata=None, scale=None,
translate=None, rotate=None, shear=None, affine=None, opacity=1, blending=None,
visible=True, multiscale=None)
numpy.ndarray
だが、メモリに乗らない大きな画像をdask
で渡してもちゃんと表示される。dask
の使い方に関しては以下を参照。
channel_axis=0
と指定することでチャネルごとに色付けされる。blendingも指定しなければ"additive"に切り替わる。napari
が内部で使っているvispy
で用意されているものを使うのが手っ取り早い。"gray", "plasma"など、matplotlib
のカラーマップの代表的なものが用意されていると考えるとよい。np.percentile
などで外れ値を一部サチュレートさせたコントラストを使うことになる。rendering="iso"
のときのパラメータ。rendering="attenuated_mip"
のときのパラメータ。
2. Labels layer
viewer.add_labels(...)
で整数配列をラベルとして追加します。異なる整数は使える色を使いきるまで異なる色で表示され、0は背景となり透明になります。
data array ... ラベルデータ。 例えば2D画像 で得られる配列詳細を表示
add_labels(data, *, num_colors=50, properties=None, color=None, seed=0.5, name=None,
metadata=None, scale=None, translate=None, rotate=None, shear=None, affine=None,
opacity=0.7, blending='translucent', visible=True, multiscale=None)
scipy.ndimage.label
などの出力をそのまま用いるのが基本となる。img
に対しfrom scipy import ndimage as ndi
binary_input = img>100
structure = [[0,1,0],[1,1,1],[0,1,0]]
labels, n_labels = ndi.label(binary_input, structure)
labels
を渡すことで可視化できる。
skimage.measure.regionprops_table
の結果と互換性があるが、こちらはデフォルトでは背景を含まないので注意 (→ scikit-imageの公式ドキュメント)。
3. Points layer
viewer.add_points(...)
で座標のリストをポイントとして追加します。
詳細を表示
add_points(data=None, *, ndim=None, properties=None, text=None, symbol='o', size=10,
edge_width=1, edge_color='black', edge_color_cycle=None, edge_colormap='viridis',
edge_contrast_limits=None, face_color='white', face_color_cycle=None,
face_colormap='viridis', face_contrast_limits=None, n_dimensional=False, name=None,
metadata=None, scale=None, translate=None, rotate=None, shear=None, affine=None,
opacity=1, blending='translucent', visible=True, property_choices=None)
Labels
と同様だが、Points
レイヤーでは後述するtext
やface_colormap
と組み合わせてることで、さらに高度な利用ができる。properties
との組み合わせで、format文字列の形式でポイントにテキストを付随させる。例えば、xy平面上でランダムに点を配置し、座標を$(x, y)$の書式のテキストで追加する場合は次のようになる。イメージとしては、各点に関してtext.format(**properties)
のようなコードが走っていると考えるとよい。arr = np.random.random((8,2))*100 # 8点
viewer.add_points(arr,
text="({x:.1f}, {y:.1f})",
properties={"x": arr[:,1], "y": arr[:,0]}
)
properties
との組み合わせで、各点に付随した値に応じて枠線/塗りつぶしの色を変えることができる。例えば点の座標に信頼度のようなものがあり、信頼度の高い点ほど赤色で明るくしたいとき、次のようなコードで実現できる。arr = np.random.random((4, 2))*100 # 4点
properties = {"confidence": [0.9, 0.3, 0.6, 0.1]} # 各点の信頼度
viewer.add_points(arr,
properties=properties,
face_colormap="red",
face_color="confidence"
)
properties
で指定された各プロパティが取りうる値。
4. Shapes layer
viewer.add_shapes(...)
により長方形、折れ線、円などのオブジェクトを追加します。手動での編集、テキストの追加も容易なので、PowerPointのように画像のアノテーションに使ったり、ImageJのROIのように使ったりできます。
オプションはPoints
レイヤーとほぼ同じです。キーワード引数の詳細はそちらを参照してください。
例えば長方形を追加する場合 のようになる。詳細を表示
add_shapes(data=None, *, ndim=None, properties=None, text=None,
shape_type='rectangle', edge_width=1, edge_color='black', edge_color_cycle=None,
edge_colormap='viridis', edge_contrast_limits=None, face_color='white',
face_color_cycle=None, face_colormap='viridis', face_contrast_limits=None,
z_index=0, name=None, metadata=None, scale=None, translate=None, rotate=None,
shear=None, affine=None, opacity=0.7, blending='translucent', visible=True)
list
で複数同時に追加することができる。頂点の座標の数は次元数やオブジェクトの種類によって変わる。viewer.add_shapes([[2,2],[2,10],[8,10],[8,2]], shape_type="rectangle")
5. Surface layer
viewer.add_surface(...)
により、3D表面を表示できます。これは3D物体の境界を定量的に強調したり、地図と標高から立体的な俯瞰図を作成したりするときに用います。
data tuple of arrays ... (頂点, 平面を形成する頂点の組, 値)の詳細を表示
add_surface(data, *, colormap='gray', contrast_limits=None, gamma=1, name=None, metadata=None, scale=None, translate=None, rotate=None, shear=None, affine=None, opacity=1, blending='translucent', shading='flat', visible=True)
tuple
で指定する (最後の値は任意)。現実的には3D画像から例えばskimage.measure.marching_cube
を用いて生成することになる。from skimage.meansure import marching_cubes
verts, faces, _, values = marching_cubes(image)
viewer.add_surface((verts, faces, values))
scikit-image
のサンプル画像である"brain"からSurfaceを生成するとこんな感じになります。
Image
と同様。
6. Track layer
viewer.add_tracks(...)
により、時間的に移動している点に残像を付けることで軌跡を表現できます。惑星の運動や、蛍光輝点の追跡結果の表示に非常に便利です。
詳細を表示
add_tracks(data, *, properties=None, graph=None, tail_width=2, tail_length=30,
name=None, metadata=None, scale=None, translate=None, rotate=None, shear=None,
affine=None, opacity=1, blending='additive', visible=True, colormap='turbo',
color_by='track_id', colormaps_dict=None)
ID
t
y
x
0
0
$y^0_0$
$x^0_0$
:
:
:
:
0
10
$y^0_{10}$
$x^0_{10}$
1
4
$y^1_4$
$x^1_4$
:
:
:
:
1
15
$y^1_{15}$
$x^1_{15}$
data
の行数に一致する。時間経過に伴う軌跡の状態変化なども記述できるということ。dict
で{子:親}
の形で表現する。例えばgraph = {1: 0, 2: 0, 3: [1, 2]}
であれば、点0が点1,2に分離し、その後点1,2が点3に融合する軌跡が描ける。詳細はこちらのコードで確認できる。properties
を与えていれば好みのプロパティで色付けできる。napari.utils.Colormap
しか受け付けない。
7. Vector layer
viewer.add_vectors(...)
によりベクトル場を表示できます。add_image
と組み合わせてポテンシャル場・ベクトル場の時系列変化を可視化できるので、流体力学シミュレーションなどに向いているかと思います。
詳細を表示
add_vectors(data, *, properties=None, edge_width=1, edge_color='red',
edge_color_cycle=None, edge_colormap='viridis', edge_contrast_limits=None, length=1,
name=None, metadata=None, scale=None, translate=None, rotate=None, shear=None,
affine=None, opacity=0.7, blending='translucent', visible=True)
scipy.ndimage.map_coordinates
に似た様式)と、各点のベクトル値を与える、ベクトル場らしい記述パターンがある。D次元空間でのdata
の形状は
data[0, 0, :]
は2D平面上の点 $(0, 0)$ におけるベクトルの2成分に対応する。Points
レイヤーと同様。
スクリプトからレイヤーへのアクセス
napari
のもう一つの強みは、手動で描いた点や図形の座標情報をnumpy.ndarray
として受け取れる点です。これでPython上での画像解析の弱点を完全に克服できます。
レイヤーをスクリプトから指定して情報を得る
napari.Viewer
は多次元画像の可視化に必要ないくつもの変数からなっています。例えば
-
axes
(座標軸) -
camera
(カメラの向きなど) -
layers
(レイヤーリスト) -
scale_bar
(スケールバー)
などがあります。
ここではlayers
からレイヤーの情報を得ることについて解説します。layers
はLayerList
クラスのオブジェクトで、Layer
オブジェクトを格納したPythonのlist
のようなものです。したがって、一番下のレイヤーが欲しければ
layer = viewer.layers[0]
でアクセスします。もしくは、"XXX"という名前のレイヤーであれば
layer = viewer.layers["XXX"]
で指定してもOKです。
ここで得られる変数layer
はLayer
オブジェクトなので、配列データ、名前、メタデータなどすべて含まれています。それぞれ次のようにアクセスします。
layer.data # 配列データ
layer.name # 名前
layer.metadata # メタデータ
他にもレイヤーの種類によって様々な情報が得られますが、前章で登場したキーワード引数は、ほとんどすべてがそのままレイヤーオブジェクトの属性になっているので、難しくないと思います。
マウスで選択したレイヤーの情報を得る
毎回レイヤーの順番や個数は変わりうるので、できればマウスでクリックしてレイヤーを指定したいです。例えば以下のように、サンプル画像にフィルタやラベル付けをし、顔の部分を手動で囲った状況で、長方形の座標を得たいとしましょう。レイヤーリストを見ると"Shapes"が選択されています。
これはLayerList
クラスのselection
プロパティで得ることができます。
viewer.layers.selection
[Out] Selection({<Shapes layer 'Shapes' at 0x...>})
見ての通り選択されているレイヤーがPythonのset
のようなものに格納されているので、次のようにアクセスする必要があります。
viewer.layers.selection.pop() # OK。pop()なので選択は解除される。
list(viewer.layers.selection)[0] # OK。もとのsetは変わっていないので選択は解除されない。
viewer.layers.selection[0] # Error!
これで、コード自体は変えずに、状況に応じて手動で選択できるようになりました。
layer = viewer.layers.selection.pop()
layer.data[0] # 0番目の図形の座標
[Out]
array([[ 60.44640515, 159.68169239],
[ 60.44640515, 274.63154028],
[203.29670156, 274.63154028],
[203.29670156, 159.68169239]])
終わりに
読んでいただきありがとうございます。napari
の多彩な機能は1回では紹介しきれないので、また次回に回します。予定としては
- よく使う自作関数をキーボードショートカットとして登録する
- マウスの動きを認識させてViewerでライブで解析する
- ウィジェットを追加する
などを書いていこうかと思います。他にもご要望があればコメント欄にどうぞ!
追記 (2021/08/09)
続きはこちらから。