おかざきPの記録

Mac, QNAPの設定やプロデューサー業の記録

ImageJ Fijiとpythonでフォルダ内の画像を一括処理

ImageJを使う理由

 ImageJ Fijipython(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の他にFileOpenerFileSaverなるクラスがあるが, これらは事前に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.'

 突如現れるImagePlusImageProcessor 画像を開いた時に返されるのがImagePlusImageProcessorはその中に含まれる. 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,お前はダメだ.