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

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

ImageJ / Python Tips: 8/16bit画像と、閾値設定、二値化マスク関連まとめ

 今までImageJにおけるPythonプログラミングをやってきて、Threshold(閾値)設定関連で気づいたことをまとめてみます。ImageJはまだ8bit画像を前提としているところがあって、16bit (あるいはそれ以上のbit深度) 画像を扱うには注意しなければならない点が結構あるように思います。以下の記述は基本的にImageProcessorオブジェクトベースの話になります。

 

thresholdメソッドを使って、白と黒の二値マスク画像を作る

 まず、一定の閾値以上を白(255)に、未満を黒(0)に設定して二値化を図る場合ですが、簡便なのは、thresholdメソッドを使うことです。例えば、今、ipという名のImageProcessorオブジェクトを前提とすると

ip.threshold(x)

 この x に閾値を引数として入れると、ipのイメージは、それ以下が 黒(0) それ以上が白 (255) の画像に変わります。但し問題は16bit画像です。このメソッドは16bit画像でも動作します。その際は x に 8bitの時の値に256を掛けた値を与えればよいのですが、問題は、すでに指摘したように、このメソッドはあくまで 0 / 255 の値しか吐きません。従って16bit画像だと、どちらもほとんど真っ黒に見えます。二値化画像に見えません(実は二値化されているのですが)。

 対策としては、16bit画像の場合、一旦thresholdメソッドを適用したipに対し、例えばmultiply メソッドを使って値を256倍するなどが考えられます。

ip.multiply(256)

 あるいは次に述べるように setThresholdメソッドを使い、そこから二値化マスクを作成するという方法もあり得ます。

 なお、thresholdメソッドは引数に整数しか受け付けないので、小数を使いたい場合は、setThreshold メソッドを使う必要があります。また、直ちにピクセル値を変えたくないが、閾値は設定したいという場合も、setThreshold メソッドで対応して下さい。

 

 

setThresholdメソッドから二値化マスクを作成

 setThresholdメソッドは、ImageProcessorに対して、上端と下端の閾値を設定するメソッドです。但し、threshold メソッドとの違いは、上端と下端の2つの閾値を設定できる他に、引数に double型を使う、また閾値を設定しても、ピクセル値を変更することはない点です。従って、閾値に基づいて二値マスクを作りたい場合は、閾値をROIに転換して、さらにROIをfillメソッドで塗りつぶす、という作業が必要になります。面倒ではありますが、ROIの塗潰し方を指定することで、thresholdメソッドであった、8bitを前提としているという問題は解消することになります。

 書式は、

setThreshold​(double minThreshold, double maxThreshold, int lutUpdate)

つまり、

setThreshold​(下端の閾値, 上端の閾値, Look up tableの更新状態の指定)

となります。

 具体的には、例えば

ip.setThreshold(20, 128, ImageProcessor.BLACK_AND_WHITE_LUT)

 というふうに使います。上端の閾値に最高値を与えれば、thresholdメソッドに近い使い方が可能です。また、8bit画像でも16bit画像でも使用可能です。但しRGBカラー画像には対応しません。グレースケールもしくは、R, G, Bのうちどれか1チャンネルの画像のみに対応します。16bit画像で使う場合は8bitの場合の256倍の値を与えます。

 Look up table*1の更新については以下の値が使用可能です。

RED_LUT, BLACK_AND_WHITE_LUT, OVER_UNDER_LUT or NO_LUT_UPDATE

 

 但し、閾値を指定しただけでは二値化マスク画像は作成できません。一旦ROIに転換し、そこからマスク画像を作成します。但し、roiからgetMaskメソッドを使って直接マスクを得ようとすると、以前報告したように、周辺部分にroiが全くかからない矩形領域が存在すると、その部分をクロップしてマスクを作成するという (つまりバウンディングボックスに基づいてマスクを作成するということです)、バグなのか、仕様なのかよく分かりませんが (結局仕様のようです)、そのような特徴が存在します。そこで、roiから直接にマスクを取得するのではなく、一旦ImageProcessorオブジェクトを作成し、それにマスク画像を作成し、そこからマスクを取得して (getMaskメソッドを使わずに) ImagePlusに転換するというような手続きを取る必要があります。

f:id:yasuo_ssi:20210412141848j:plain

getMask で ROI を取得してマスクを作ると、作成されるサイズが
ROIをめぐるバウンティングボックスに限定される

 具体的には次のようになります。

オリジナル画像を含んだImagePlusオブジェクトをimpとすると、例えば、

ip = imp.getProcessor() →impからImageProcessorを抽出
mask = ByteProcessor(ip.width, ip.height) →元のimpと同じサイズの白紙 (色の値は 0 = 黒) のmask用ImageProdessorを作成
mask.setValue(255) →maskに対する描画色の値指定 (この場合は白)
imp.getProcessor().setThreshold(lowervalue,uppervalue, ImageProcessor.OVER_UNDER_LUT)
roi = ThresholdToSelection.run(imp) →ThresholdToSelectionを使ってimpからRoi(選択範囲)データを作成 (ThresholdToSelectionはimportで読み込んでおく必要あり)
mask.setRoi(roi) →maskに対してRoiを設定
mask.fill(roi) →maskに設定したRoiの内部を描画色で塗りつぶす
maskimp = ImagePlus("Mask", mask) →ImagePlusをコンストラクタとして使って、ImageProcessorオブジェクトである mask からマスク画像を含んだImagePlusオブジェクトを作成

 

 なお、Roiの外側を描画色で塗りつぶす (つまり反転したマスク画像を作成する) 場合は、fill メソッドの代わりに、fillOutside メソッドを使います。

 上のコードに対応したimport文として次のものが必要です。

from ij import IJ, ImagePlus
from ij.process import IImageProcessor
from ij.process import ByteProcessor
from ij.plugin.filter import ThresholdToSelection

 

 上の例はByteProcessorでマスク画像を作成したので8bitのマスクになりますが、これをImageProcessorを使って16bit画像のマスクを作成することもできます。その場合は以下のようになります。

ip = imp.getProcessor()
mask = ip.createProcessor(ip.width, ip.height)
 →ByteProcessorだと8bitのImageProcessor (=ByteProcessor)しか作れないので、createProcessorメソッドでmask用Processorを作成する。なおByteProcessorだとコンストラクタとして幅と高さを指定するだけでProcessorが作成できるが、ImageProcessorの場合はコンストラクタとしてこのような使いかたができないので、createProcessorメソッドを使う。
mask.setValue(65535) →描画色の値を16bitに合わせて変更
imp.getProcessor().setThreshold(lowervalue,uppervalue, ImageProcessor.OVER_UNDER_LUT)
roi = ThresholdToSelection.run(imp)
mask.setRoi(roi)
mask.fill(roi)
maskimp = ImagePlus("Mask", mask)

 

 これにより、0 / 65535 の二値マスクが作成できます。

  なお、代表的なImageJのチュートリアルサイトのサンプルコードはいずれもRoiに対しgetMaskメソッドを実行して、ImagePlusに転換しています。私の前のプログラムもそれに倣っていたのですが、問題に気付いて上記のように変更しました。

 あるいは、画像を二値化して細胞数を数える、というようなことをやる場合はRoiがかからない部分を切り取ったほうが都合がよくて意図的にそういう仕様になっているのかもしれません。しかし、GIMPPhotoshopでマスクとして活用する画像を作成する場合はそれでは不都合ですので、二値画像を作成したProcessorからマスク画像を含んだImagePlusオブジェクトを作成する必要があります。

*1:Look up tableとは画像データ自体を変更せずに、画像を色を付けて表示する等、見かけを変更する、フィルター的役割を果たすデータ。