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

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

ImageJ / Python プログラミング Tips: getMaskメソッドのバグ(?)を回避する

 前回修正したものを公開した、ImageJ対応 汎用色チャンネルマスク画像作成ツールのバグですが、以下のようなものでした。

f:id:yasuo_ssi:20210412141848j:plain

二値化マスク作成の際クリップされる範囲


 要は、左端と上端に、ROIが一切かからない x または y の範囲が存在すると、その部分が二値化マスクを作るときにクリップしてしまうという問題です。

 

 そもそも二値マスクの取得は次のようなコードで実現していました(若干簡略化して示します)。

1: impZone.getProcessor().setThreshold(zoneLTh,zoneHTh, ImageProcessor.NO_LUT_UPDATE)
2: roi = ThresholdToSelection.run(impZone)
3: impZone.setRoi(roi)
4: try:
5:     maskimp = ImagePlus("Mask", impZone.getMask())

  :

 impZoneは、二値マスクを作成するときのもとの画像(ImagePlusオブジェクト)

 zoneLth, zonHthは二値化するときの下の閾値と上の閾値

 roiは、閾値の範囲を領域選択したデータ(=注目領域: ROI)

  maskimpはマスク画像のimagePlusオブジェクト

 

 要は何をやっているかというと、1行目で元の画像に閾値を適用し、2行目で閾値の範囲を範囲選択したものを roiという変数に代入し、3行目で元の画像に対しこの選択範囲をROIデータとして貼り付け、これから5行目でgetMaskメソッドを使ってmaskimpという画像データ(ImagePlusオブジェクト)にマスク画像として落とし込んでいます。

 で、デバッグ作業で調べてみると、impZoneの画像サイズが変わるということはありませんでした。サイズが変わっているのは、getMaskメソッドで二値のマスク画像を取得してmaskimpに落とすときに、maskimpの大きさがimpZoneの大きさから一部切り取られた画像になっていました。どうもgetMaskメソッドのバグではないかと思われます。

 で、何か解決策はないかとネットを調べてみると、ImageJのQ & Aのフォーラムで、開いている画像上のROIを二値のマスクに転換しようとしているところで、ROIが長方形だと、getMaskメソッドでマスク画像を取得しようとすると失敗するし、他のROIは元の画像とサイズが異なるマスク画像が得られてしまうので、何とかならないかと相談している人がいました。

https://forum.image.sc/t/getting-the-binary-masks-of-rois-in-imagej/27970

 回答者は、最終的にアクティブな画像のROIから直接getMaskでマスク画像を取得するのではなく、ByteProcessorを使って、一旦白紙(といっても値は0なので、実は真っ黒)のマスク画像を作成し、そこに元の画像上のROIを貼り付け、そこからgetMaskメソッドでマスク画像を取得するコードを提案していました。ByteProcessor とは、ImageProcessorの一種で、8bit画像とそれに対するメソッドです。

 ひょっとして、どうもこれは同じ問題かも、と思って、そのコードを参考に上のコードを次のように変えました。

まず、ライブラリの宣言で次の1行を追加しました。

from ij.process import ByteProcessor

ByteProcessorを読み込みます。

 

そして上のコードを以下のように変えました。

ip = impZone.getProcessor()
mask = ByteProcessor(ip.width, ip.height)

 →以上で、impZoneと同じ白紙(値は0: 真っ黒)のマスク画像 (mask) を一旦作成。

mask.setValue(255) →描画色に255(最明色=真っ白)を設定

(中略)

impZone.getProcessor().setThreshold(zoneLTh,zoneHTh, ImageProcessor.NO_LUT_UPDATE)
roi = ThresholdToSelection.run(impZone)
mask.setRoi(roi) → roiを元の画像ではなく、白紙のマスク画像に貼り付け
mask.fill(roi) →ByteProcessorのfillメソッドでroiの内部をsetValueで設定した描画色255(真っ白)で塗りつぶし、二値の画像を作成
try:
     maskimp = ImagePlus("Mask", mask)

 →maskからmaskimpを作成

 

 これにより、とりあえず二値マスク画像の一部が切れることがなくなりました。つまりROIから直接マスク画像のImagePlusに転換するのではなく、一旦以下のようなByteProcessor画像を作成し、ROIではなくその画像をImagePlusに転換すれば、クロップされないということですね。

 なお、上のQ & Aには、mask.fill(roi.getMask())としていましたが、 getMask()を入れるのは間違いのようです(動作はしますがgetMask()メソッドは無視されると同時に、エラーメッセージが出ます)。

f:id:yasuo_ssi:20210412143348j:plain

作成したByteProcessor画像

 

 よく分かりませんが、要はオリジナル画像の周辺部にROIが一切かからない空白域があると、オリジナルのroiからそのままgetMaskメソッドを実行してダイレクトにImagePlusを作成するとサイズが切れてしまうというgetMaskメソッドのバグ(なのか仕様なのか)があるのではないかと思われます。従って、getMaskメソッドを使わずに、一旦マスク画像を明示的に作成したうえでImagePlusに転換する必要がある、ということのようです。一旦作成する画像はByteProcessorではなく通常のImageProcessorでも、setValue, fillメソッドはサポートしているので構わないかもしれませんが、試していません。

[2022.6追記]

 結局、画像データ上にROIがかかっていると、その画像をコピーしたり、画像からデータを取得しようとしてもROIの最大矩形領域 (バウンティングボックス) しかコピーしたり取得できないというのは仕様のようです。というのは同様に以下の問題も発見しました。

yasuo-ssi.hatenablog.com