省型旧形国電の残影を求めて

戦前型旧形国電および鉄道と変褪色フィルム写真を中心とした写真補正編集の話題を扱います。他のサイトでは得られない、筆者独自開発の写真補正ツールや補正技法についても情報提供しています。写真補正技法への質問はコメント欄へどうぞ

ImageJ / Python Tips: 上端と下端の閾値のある二値マスクをどう書くか?

 ImageJのコマンドに上端と下端に閾値のある一定範囲の明度で区切られたマスクを作成する、Thresholdコマンドがあります。

メニュー→ [Image]→ [Adjust]→ [Threshold]

f:id:yasuo_ssi:20210215144050j:plain

ImageJ Thresholdコマンド実行状態

 これを Pythonスクリプトで書くにはどうしたら良いでしょうか? これについては以下のページにサンプルスクリプトが出ています。

wiki.cmci.info

この、Threshold to create a mask (Binary)下にある、Lower and Upper Threshold Value のサンプルについて解説をつけていきます。

  1. # an example with lower and upper threshold values, 100 and 125.
  2. from ij import IJ, ImagePlus
  3. from ij.process import ImageProcessor
  4. from ij.plugin.filter import ThresholdToSelection
  5. imp = IJ.getImage()
  6. imp.getProcessor().setThreshold(100, 125, ImageProcessor.NO_LUT_UPDATE)
  7. roi = ThresholdToSelection.run(imp)
  8. imp.setRoi(roi)
  9. maskimp = ImagePlus("Mask", imp.getMask())
  10. maskimp.show()

例によって、緑色の字は、任意に変えることのできる名前や値です。青字は、選択肢の中から選ぶべきパラメータです。

1行目はコメント欄ですが、2~4行目は必要なライブラリを読み込んでいます。

6行目、IJに関しては、リファレンスに次のように説明されています。

"This class consists of static utility methods."

つまり、ImageJのユーティリティに関するメソッドを集めたクラスで、基本的なクラスです。getImage() は、アクティブなイメージを取得するメソッドで、imp = IJ.getImage() で、impという名の imagePlus オブジェクトに今アクティブなイメージを取得しています。ただ、前にも指摘しましたように、アクティブなイメージを別のポインタに格納しているわけではなく、アクティブなポインタに、いわば imp というエイリアス(別名)をつけただけなので、アクティブなイメージを閉じてしまうと、imp もなくなってしまうことに注意してください。

7行目ですが、getProcessor() メソッドを通して impに対し、setThreshold というImageProcessorのメソッドを実行します*1setThreshold はリファレンスに次のように出ています。

setThreshold(double minThreshold, double maxThreshold, int lutUpdate)

Sets the lower and upper threshold levels.
 
最初の引数は最低閾値、2番目は、最高閾値、3番目はLook up table (画面での表示の仕方を指定するメタデータ、略称 LUT) を更新するかどうかを指定する整数値の引数です。閾値は、2バイトの実数値で指定します。
サンプルでは、
setThreshold(100, 125, ImageProcessor.NO_LUT_UPDATE)
となっています。最低閾値は 100 最高閾値は 125ということは分かりますが、3番目の引数は整数値のはずなのに、ImageProcessor.NO_LUT_UPDATE とあります。これはどういうことでしょうか?
 これは、ここに数字が入っていると分かりにくいので、システムで定数名を定め、定数名を一定の数値と対照させているのです。どの値が取得可能なのか、ImageProcessorのレファレンスの setThreshold の項目にはありませんが、setAutoThreshold のところに選択肢が出ていて、それは、RED_LUT, BLACK_AND_WHITE_LUT, OVER_UNDER_LUT or NO_LUT_UPDATE と書かれています。このそれぞれの値が具体的にどのような整数値なのかはこちらに出ていました。
RED_LUT=0, BLACK_AND_WHITE_LUT=1, NO_LUT_UPDATE=2, OVER_UNDER_LUT=3
つまり、上のサンプルは、
setThreshold(100, 125, 2)
と書いていても良いのですが、そう書くと、コードを解読する人にとって、意味が分かりにくいので、2 と書く代わりに NO_LUT_UPDATE と書いているわけです。つまり、値を設定してもルックアップテーブルは変更しませんよ、と指定しているわけです。ではその頭に、ImageProcessor. がついているのは何でしょうか? これはこの行が imp という imagePlus オブジェクトからさらに imageProcessor オブジェクトを呼び出しています。しかし、親はあくまで imagePlus オブジェクトです。そして、システムで定められた定数はオブジェクト単位で決められているので、imagePlus オブジェクトは、NO_LUT_UPDATE とは何の意味か分かりません。ですので、imageProcessorオブジェクトで定められているNO_LUT_UPDATE ですよ、と imagePlus オブジェクトに分かるようにつけているのです。
8行目ですが、ThresholdToSelection は冒頭のライブラリの呼び出しで指定している、ij.plugin.filter クラスの中のオブジェクトの一つで、閾値の範囲を選択範囲に変換するユーティリティのオブジェクトです。これに imp に対し実行する run(imp) というメソッドを実行して選択範囲に変換し、それを roi という注目範囲(ROI)変数に格納します*2
そして9行目、この選択範囲を setRoi という imagePlusのメソッドを実行することで、impに対し ROI (Region of interest 注目範囲) メタデータとして保存します*3
そして10行目が問題かもしれません。ImagePlusがあたかもメソッド(実行コマンド)のような扱いを受けていますが、ImagePlus ってオブジェクトのクラス名だったのでは...?という疑問を持たれるのではないでしょうか?
ここではImagePlusは constructor として扱われています。Web上の説明を見ると一般に (ImageJに限らず)  constructorとは、クラス名と同名のメソッド名だといったものが見受けられます。これはImagePlusのリファレンスの下の方にある、Constructor Summary というところを見てください。すると次のように書かれています。
 
Constructor and Description
ImagePlus()
Constructs an uninitialized ImagePlus.
ImagePlus(java.lang.String pathOrURL)
Constructs an ImagePlus from a TIFF, BMP, DICOM, FITS, PGM, GIF or JPRG specified by a path or from a TIFF, DICOM, GIF or JPEG specified by a URL.
ImagePlus(java.lang.String title, java.awt.Image image)
Constructs an ImagePlus from an Image or BufferedImage.
ImagePlus(java.lang.String title, ImageProcessor ip)
Constructs an ImagePlus from an ImageProcessor.
ImagePlus(java.lang.String title, ImageStack stack)
Constructs an ImagePlus from a stack.
 
要は同名のクラスのオブジェクトそれ自身を作り出すコマンドとしての機能ということのようですね。つまり imagePlusオブジェクト(画像)を新たに作るコマンドとして、このクラス名自身が使われているわけです。サンプルを見ると、
maskimp = ImagePlus("Mask", imp.getMask())
とありますが、"Mask"という題名のついた、imagePlus オブジェクトである maskimpという画像データを、バイナリーのマスク画像を作る、getMask メソッド(ちなみにこれはImageProcessorのメソッドです)を使って作る、というコマンドになっています。"Mask"という題名まで一挙につけた画像データが作れるというところがミソなのでしょう。たぶん、ImagePlusをconstructorとして使わなければ、
maskimp = imp.getProcessor().getMask()
と長ったらしく書く上に、さらにタイトルをつけるためにもう一行
maskimp.setTitle("Mask") とか書かないといけないと思います。
そして、最後に11行目でマスク画像を表示させています。
 

 

f:id:yasuo_ssi:20210316170627j:plain

ImagePlusとImageProcessorの関係

 

*1:なお、getProcessor()の (   )内に何も記述がないので、現在アクティブなImageProcessorに、setThresholdが有効なグレースケール画像が含まれているか、あるいは1枚しかimageProcessorのスライス (チャンネル) がないimagePlusオブジェクトが前提されていると思われます。

*2:ところで、ここでこういう疑問は沸きませんか?そもそも閾値の設定は imageProcessorに対して行っています。ですのでここでの引数は imagePlus でなく imageProcessor でも良いのではないか、と。でどうもレファレンスを見ると、確かに ThresholdToSelection.run(  ) の引数としてimageProcessor も取れるようなのですが、引数として入れてみると、ちゃんと 注目範囲(ROI)を作ってくれません。いろいろトライアンドエラーを行った結果分かったことは、imageProcessor を引数としてROIを作るためには ThresholdToSelection().convert( ip ) を使うようです。※ ip は任意の名前のimageProcessor

因みに、なぜimagePlusを引数として取るときは ThresholdToSelection.run で良いのに、imageProcessorを引数として取るときは ThresholdToSelection().convert と、ThresholdToSelection の後に括弧が必要なのかは... すみません。分かりません...

*3:なお、setRoi, getMask メソッドは imageProcessor にもあるようです。