ImageJ Fijiとpythonでフォルダ内の画像を一括処理
ImageJを使う理由
ImageJ Fijiとpython(Jython)を使って自動で画像処理する方法を調べたので,何段階かに分けて投稿していく. pythonならopencvとかあるのになんでImageJを選んだかといえば, 材料配合が主体の職場ではpythonの環境構築すらハードルが高く,部内配布が事実上不可能だから・・・. ImageJなら最悪GUIでtry-and-errorを繰り返せば誰でもそれらしい結果が得られるのも,画像処理を検討する雰囲気の醸成に役立つはず.
この記事で説明する内容
第一弾では画像の開き方と保存方法を確認した後,切り取り方法を確認する.
その後,上記を関数にまとめてフォルダ内の画像に一括処理する方針で進める.
なおGUIで頑張る方法はマウスポチポチしてればいいので一切説明しない.
光学顕微鏡とかSEMとかの画像が数十枚以上ある場合を想定.
そもそもpythonの文法についても知っていることを前提.
pythonのコード
コードの書き方
材料系の職場だと笑い事じゃなく,エディタがメモ帳しかない可能性が高い.
幸いにしてImageJにもエディタが付属しておりシンタックスハイライトくらいはできる.
ImageJを開いて,File>New>Text windowもしくはFile>New>Script...で起動できる.
なおこれで作業しているとデバッグは苦しい.
persistentにチェックを入れてコードを実行すると,実行完了時の環境から抜けずに止まる.
その状態でインタラクティブシェルとして変数を調査することになる.
ここではpdb
は機能しないため覚悟すること.
pdb
でのデバッグが必要であればコマンドラインからheadless
モードにて実行する必要がある(後述).
画像の開きと保存(と閉じ)
from ij import IJ from ij.io import Opener if __name__=='__main__': '''詳しいパラメータ等は下記の公式APIを参照. https://imagej.nih.gov/ij/developer/api/ij/ij/io/Opener.html ''' # 画像のパスを設定.絶対パスでなければエラー. # 今回はImageJに付属のサンプル画像をデスクトップに保存して利用. src = '/Users/OkazakiP/Desktop/AuPbSn40.jpg' # 画像を開く. imp = Opener().openImage(src) # 他の開き方.結果は同じ. imp = IJ.openImage(src) # 何も変更せずに保存.IJクラスは色々な便利メソッドの塊. IJ.saveAs(imp, 'jpg', '/Users/OkazakiP/Desktop/AuPbSn40_.jpg') # 使い終わったら片付ける. imp.close() # 作業完了の報告. print 'Finish.'
難しいことはないでしょう.
私のテスト環境がmacだからパス文字列が/
で区切られてるけど,windowsパスでも動くはず.
この程度でif __nam__=='__main__':
で始める意味はないが,
最後にこの部分(の派生系)を関数にまとめるため,インデントがあると微妙に楽.
Opener
の他にFileOpener
やFileSaver
なるクラスがあるが,
これらは事前にFileInfo
というクラスで画像の情報を与える必要があるため面倒そう.
IJ
だけ覚えれば十分だから理解するの諦めた.
スクリプトの最後には画像を明示的に閉じないと,backgroundで開きっぱなしになってしまうから注意.
画像の切り取り
from ij import IJ from ij.io import Opener if __name__=='__main__': '''ImageProcessorに対して作業をしていく. https://imagej.nih.gov/ij/developer/api/ij/ij/process/ImageProcessor.html ''' # 先ほどと同じく画像を開く.開くとImagePlusオブジェクトが返される. src = '/Users/OkazakiP/Desktop/AuPbSn40.jpg' imp = Opener().openImage(src) # ImagePlusが持つImageProcessorを取得する. ip = imp.getProcessor() # ImageProcessorに対して切り取り領域を指定. # 画像の原点は左上. # ip.setRoi(int x, int y, int width, int height) ip.setRoi(0, 0, 200, 200) # 切り取り.切り取り後のImageProcessorが返される. ip_ = ip.crop() # 切り取りの結果をImagePlusに反映する. imp.setProcessor(ip_) # 保存. IJ.saveAs(imp, 'jpg', '/Users/OkazakiP/Desktop/AuPbSn40_cropped1.jpg') # 違う位置で切り取り. ip.setRoi(200, 200, 200, 200) ip_ = ip.crop() imp.setProcessor(ip_) IJ.saveAs(imp, 'jpg', '/Users/OkazakiP/Desktop/AuPbSn40_cropped2.jpg') # 最初に取得したImageProcessorには何も変更がなされていない. imp.setProcessor(ip) IJ.saveAs(imp, 'jpg', '/Users/OkazakiP/Desktop/AuPbSn40_cropped0.jpg') imp.close() print 'Finish.'
突如現れるImagePlus
とImageProcessor
.
画像を開いた時に返されるのがImagePlus
でImageProcessor
はその中に含まれる.
ImageProcessor
は画素値を含む,いわゆる画像そのもの.
これに加えて画像のパスとか,1ピクセルあたり何umとかのmeta情報を合わせるとImagePlus
になる.
このため画像の切り取りは画素を相手にするためImageProcessor
を抜き取って操作する.
操作後の画像を保存するには上の画像を開く時に諦めたFileInfo
が必要となるため,
それを保持するImagePlus
に画素の情報を戻してから保存する.
フォルダ内の画像を一括処理
import os from ij import IJ from ij.io import Opener # 切り取りと保存を行う関数 def crop_image(src, output, x=0, y=0, w=200, h=200): imp = Opener().openImage(src) ip = imp.getProcessor() ip.setRoi(x, y, w, h) ip_ = ip.crop() imp.setProcessor(ip_) IJ.saveAs(imp, 'jpg', output) imp.close() if __name__=='__main__': folder = '/Users/OkazakiP/Desktop/Images' # 保存用のフォルダを同じ階層のConverted以下に作る parent = os.path.dirname(folder) result = os.path.join(parent, 'Converted') try: os.mkdir(result) except OSError: _result = result # 11回までは新しく作る for i in range(10): result = _result + str(i) try: os.mkdir(result) except OSError: continue break for root, dirnames, filenames in os.walk(folder): folder_name = os.path.basename(root) root_ = result + root[len(folder):] try: os.mkdir(root_) except: pass for filename in filenames: src = os.path.join(root, filename) output = os.path.join(root_, filename) # 画像以外が含まれてエラーを吐くことへの対策 try: crop_image(src, output) except: print 'Error on %s' % (src) print 'Finish.'
もはやImageJに特有の事項はなし.普通にos.walk
で関数を回すだけ.
自動処理の目的は果たせるけど,フォルダ選択はスクリプトを書き換える必要があってuser friendlyでない.
そこでフォルダ選択用のGUIをくっつける.
GUIでフォルダを選択する
import os from ij import IJ from ij.io import Opener, DirectoryChooser ... ... if __name__=='__main__': chooser = DirectoryChooser('一括処理したい最上位のフォルダを選択して下さい.') folder = chooser.getDirectory() # 次の行は誤植ではない.パスの末尾に区切り文字が付与されてしまうため必要. folder = os.path.dirname(folder) # 保存用のフォルダを同じ階層のConverted以下に作る parent = os.path.dirname(folder) result = os.path.join(parent, 'Converted') ... ... print 'Finish.'
変更がない部分は省略した.DirectoryChooser
を使うとGUIでフォルダが選択できる.
ただし末尾に区切り文字が追加されるため,そのままだと先のコードと挙動が変わってしまう.
これを避けるために一行追加している.
ばら撒くにはこれでいいけどさ?自分で使う分にはマウスポチポチも無駄じゃん?どうせなら全部コマンドで済むと楽じゃん?
フォルダをコマンドライン引数で受け付ける(headless
モード)
#@String folder import os from ij import IJ from ij.io import Opener ... ... if __name__=='__main__': # もはや不要 #folder = '/Users/OkazakiP/Desktop/Images' # 保存用のフォルダを同じ階層のConverted以下に作る parent = os.path.dirname(folder) result = os.path.join(parent, 'Converted') ... ... print 'Finish.'
一行目に追加した#@
で始まる分はScript parameterとかいうらしい.
こうするとコマンドラインから引数を渡せるようになる.ImageJはpython2系でargparse
がないから助かる.
sys.argv
?知らん.使ったことない.
さてこのスクリプトをコマンドラインからheadless
モードで実行するには次のようにする.
ここはOSによって異なるため公式のガイドを参照すること
>> cd Fiji.app/Contents/MacOS >> ./ImageJ-macos --ij2 --headless --console --run test_walk_parameter.py 'folder="/Users/OkazakiP/Desktop/Images"'
今回は追加しなかったが,関数crop_image
に渡す引数を全て#@int x
などとしてコマンドラインから渡すことも可能.
このheadless
という割には起動時のヘッドが激重.headless
モードであればpdb
を用いてのデバッグも普通に実行できる.
なおheadless
モードでは使えない機能も存在するため注意.要するにGUIに依存するタイプ.
ウィンドウ非表示ならいけるか?と思ったらそんなことなかったからタチ悪い.
所感
ImageJ APIが死ぬほど分かりにくい.pythonの標準モジュールや,デファクトスタンダード化してるモジュールのリファレンスは本当に関心する.
ただしmatplotlib
,お前はダメだ.