おかざきPの記録

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

ImageJとpythonで粒子解析を自動化する

追記(2022/06/26)

 自動化の難易度が高い画像を手動で分析する方法も投稿しました.

やったこと

 ImageJを用いて粒子形状を自動で解析する初歩的な内容はこちらの方がまとめられている. ただ個人的には改善したい点もたくさんあって,

  1. もっとオブジェクト指向にしてコードの見通しを良くしたい
  2. RoiManagerが表示されてると実行速度が絶望的に遅い
  3. 同じくIJ.run()は裏でGUIを操作しているため速くない
  4. 長さとか面積とかに単位が欲しい
  5. ParticleAnalyzerのオプションを色々弄りたい
  6. 元画像のどこが粒子として認識されたか確認したい

とか色々ある. このため前述の巨人の肩に乗りながら(コードを流用しながら),上記の要望を叶える方法を調べてコードを改変した. なお少なくともImagePlusImageProcessorの関係と使い方は理解していないと本コードを理解するのは苦労するため, 拙著を流し読みした方が良いかもしれない.

改変したpythonコード

import os

from ij import IJ
from ij.process import ImageProcessor
from ij.plugin import Colors
from ij.plugin.frame import RoiManager
from ij.plugin.filter import ParticleAnalyzer, BackgroundSubtracter, EDM
from ij.measure import ResultsTable, Measurements



def main(imagefilepath):
    # 画像を開いてImagePlusを取得する
    imp = IJ.openImage(imagefilepath)

    # 1ピクセルが相当する実世界の長さを設定
    # ImagePlusから該当するmetaデータを取り出して編集する
    cal = imp.getCalibration()
    cal.pixelHeight = 0.1
    cal.pixelWidth = 0.1
    cal.setUnit('um')

    # 忘れずにImagePlusに反映する
    cal.setImage(imp)


    # 背景を均一にする
    bs = BackgroundSubtracter()
    ip = imp.getProcessor()

    # 後でimp.setProcessor()すると戻せなくなるため
    # 無編集のデータも先に分けておく
    ip0 = imp.getProcessor().duplicate()

    # 背景の差引
    # bs.rollingBallBackground(ColorProcessor, radius, createBackground, lightBackground, useParaboloid, doPresmooth, correctCorners)
    bs.rollingBallBackground(ip, 40.0, False, True, False, True, True)


    # 粒界を定める閾値を設定する
    # 第一引数で2値化のアルゴリズムを選択できる
    # ip.setAutoThreshold(method, darkBackground, lutUpdate)
    ip.setAutoThreshold('Default', False, ImageProcessor.NO_LUT_UPDATE)

    # 2値化した画像を取得
    mask = ip.createMask()

    # 今回は粒界が離れているため省略
    #EDM().toWatershed(mask)

    # 2値化していてもParticleAnalyzerのために閾値の設定は必要
    mask.setAutoThreshold('Default', True, ImageProcessor.NO_LUT_UPDATE)


    # 粒子解析

    # 粒子の輪郭をROIで保存する
    # 引数にTrueかFalseを与えるとウィンドウが表示されなくなって高速化できる
    # どちらを与えても同じで,どちらも与えないと表示される
    rm = RoiManager(False)

    # 解析結果の保存先
    # こちらは何もしなくてもウィンドウは表示されない
    rt = ResultsTable()


    # 粒子解析する本体

    # インスタンス化せずにクラスメソッドとして実行すると次のインスタンス化時に反映される
    ParticleAnalyzer.setRoiManager(rm)

    # インスタンス化
    # 第一引数は解析方法の指定を行なっており2進数の和で表現されている
    # ただそれぞれ覚えていられないため次のようにクラス変数を参照する
    # 第二引数も同様に数値化する項目を指定できるが全部解析して後で省けばよし
    # 第4,5,6,7引数は解析対象とするそれぞれ最小面積,最大面積,最小円形度,最大円形度
    # 工夫してノイズを除外すること
    pa = ParticleAnalyzer(
                ParticleAnalyzer.EXCLUDE_EDGE_PARTICLES
                + ParticleAnalyzer.INCLUDE_HOLES
                + ParticleAnalyzer.ADD_TO_MANAGER,
                Measurements.ALL_STATS,
                rt, 0., 1.e9,
                0., 1,
            )

    # 長さの情報をもつImagePlusと2値化画像のImageProcessorを渡す
    pa.analyze(imp, mask)


    # 保存
    savefilepath = imagefilepath[:-4]+"_"
    rt.saveAs(savefilepath + "Results.csv")
    imp.setProcessor(mask)
    IJ.saveAs(imp, "bmp", savefilepath + "masks.bmp")
    rm.runCommand("Save", savefilepath + "Roi.zip")


    # 抽出した粒子の輪郭を描画する

    # 輪郭を黄色にする
    # 使いたい色の16進数表記を変換してColorsオブジェクトにする
    c = Colors().decode('#ffff00')

    # そのままだとグレースケールの画像になっている
    # そこに色を渡してもグレースケールに変換されてしまうため
    # 元の画像をRGBカラーに変換しておく
    ip = ip0.convertToColorProcessor()

    # やっと色を指定できる
    ip.setColor(c)

    # ROIそれぞれを描画するとImageProcessorに渡した色が使われる
    for roi in rm.getRoisAsArray():
        ip.draw(roi)

    # ImagePlusに画像を反映して保存
    imp.setProcessor(ip)
    IJ.saveAs(imp, "jpg", savefilepath + "AnalyzedResult.jpg")

    # close roi manager, results table and images
    rm.close()
    rt.reset()
    imp.close()

 フォルダ内の画像に一括適用したいなら この関数をos.walkする. ip.setAutoThreshold()の第一引数は文字列で 他のアルゴリズムを与えられる. 解析した粒子を明示するのは重要で,自動化していると想定外な輪郭を取得される可能性もあり,それを簡単に確認できる必要がある. 初めて扱う種類の画像であればip.setAutoThreshold()の第一引数を全て試して最適なアルゴリズムを選択するべきだろう.
 なおRoiManagerを使っているとheadlessモードでエラーが出てRoiManagerインスタンス化に失敗する.このため本コードはImageJのエディタから実行する必要がある.