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

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

ImageJ / Python Tips: 32bit 画像を扱う時は常にディスプレイ範囲を意識せよ!

 ImageJ 上で、32bit 画像を扱うプログラムを昨年末あたりから書き始めていますが、8bit, 16bit 画像とは異なり、意図した結果が得られないことが多く苦労します。そのため、その理由をいろいろ調べていましたが、結局、ImageJ 上で 32bit 画像を扱うには、8bit, 16bit 画像とは異なる注意点があることが分かりました。今回はこれについて分かった範囲で書いてみます。

 まず、そもそも ImageJ 上における 32bit 画像の位置づけですが...

1) そもそもデータ保存形式としてあまり前提されていない

2) 計算過程上必要なデータ形式として位置付けられている

ということが言えます。

 1) に関しては、そもそも bio-format プラグインであっても、ImageJ 独自スタック形式以外の 32bit カラー画像の読み込みはサポートされていないことからも明らかです。これについて、何とか読み込む方法について記事を書きましたが、外部ソフトでの加工と ImageJ 上での処理が必要です([追記] 最近の bio-format プラグインのアップデートで、通常の32bit フルカラー TIFF の読み込みに対応するようになったのに気づきました。おそらくVer. 6.12.0のアップデートからのようです。Fiji では 6 月ごろに アップデートとして配布された様です。なお、書き込みの方は変更なく、スタック形式の RGB フルカラー画像は ImageJ 独自形式の TIFF でしか出力できません。)

yasuo-ssi.hatenablog.com

 2) に関しては、例えば画像の掛け算、割り算を行う際は、整数データでは不都合なので、このような加工のためのプロセスで使える小数を扱えるデータ形式として 32 bit データがサポートされていることは明らかです。このため、整数 8bit, 16bit データでは、値の範囲 (ディスプレイ範囲) がそれぞれ、0-255, 0-65535 と決まっていますが、32bit 浮動小数点データでは、値の範囲を決め打ちすることができない、ということです。

 従って、32bit データの bit 深度変更などの加工を掛ける際に注意を要する点が出てきます。整数形式であれば深い bit 深度から浅い bit 深度に変換する場合、自動的に値の範囲も調整します (但し浅い bit 深度から深い bit 深度に変換する場合は自動調整を行わないので、bit 深度を変換した後 256 を掛ける必要があります)。

 ところが、32bit 浮動小数点データは、値の範囲が決まらないので、自動調整してくれず、変な値になります。※もし元々の 32 bit データが、0.0 - 1.0 の範囲だと、他 bit 深度に変換すると、値が 0 か 1 となりほぼ真っ黒の画像になってしまいます。また元の範囲が、0 - 16,777,215 だとすると、大半の値は、8 bit や 16 bit 画像のホワイトポイントを超えるのでクリップし、ほぼ真っ白な画像になります。

 そこで、32bit 浮動小数点データを 16bit 整数に下げる際に、最大値を65535 になるようにあらかじめ乗算してから変換すると.... なぜか意図通りになりません。やはり真っ白の画像になってしまいます。

 いくつか 32 bit 画像をいじっていて気づいたよくあるパターンは...

・上のように、値を大きくしたり、小さくしたら、真っ白、または真っ黒な画像になった。

・以下のようなヒストグラムの画像をいじっていたら、その下のようにヒストグラム全体が、勝手にずれてしまった。

オリジナル

ずれたヒストグラム

ヒストグラムの途中でカットしようとしたら、二値化されてしまった。

赤線から下をカットしたい

二値化されてしまった

 結局どうすればよかったのかというと、32bit 画像の段階でディスプレイ範囲を設定すればよかったということが分かりました。つまり、もし 32bit 画像の値の範囲が 0.0 - 1.0 なら、

imp.setDisplayRange(0.0, 1.0)

imp は ImagePlus

と指定します。オーバーしたり、アンダーのピクセルは、最大値、最小値の明るさで表示してくれます。

 これで、ImageConverter を使って、bit 深度を下げて変換したときに自動的に値の尺度も変更してくれるようです。従って 32bit 画像は常にディスプレイ範囲がどうなっているのかを気にしながら、計算加工を行う必要があるということです。

 とにかく、新しい ImagePlus を作るたびに必ず、setDisplayRange を掛けるとともに、画像に対し乗算や除算、さらに場合によっては加算、減算を行うときも、必ず同時に setDisplayRange を設定してください。そうしないと処理途中で値が勝手にずれます。これで非常に苦しみました。imageCalcurator に掛ける際も、DisplayRange が不適切に設定されていると、Display 状態に合わせてシフトした値で計算を行ってしまいます。まだ、ImageJ の 32bit 画像の挙動が完全に分かっている訳ではありませんが、おそらく ImageJ の中で形式変更なく扱われている限り、ディスプレイ表示はともかく、データ自体は動いていないのではないかと思います。しかし、bit 深度変更などの形式変更やファイル出力保存時、imageCalcurator で他の画像と計算を行う際に、ディスプレイ範囲が適切に設定されていないと、意図せずに、データがシフトしてしまうようです。

 なお、32-bit 画像を新規に作成した場合はディスプレイ範囲は、ディスプレイ範囲を指定しない限り、最初に与えられている最大値と最小値がディスプレイ範囲になるようです。

 なお、bit 深度が浅い方から深いほう (32bit) へ ImageConverter を使って変換する場合は、尺度変更は自分で計算しなければなりません。ImageConverter に setDoScaling というメソッドがありますが、これは、深いほうから浅いほうに変える時のみ有効です。

 また現在設定されているディスプレイ範囲がどうなっているのかを確認するメソッドは、

imp.getDisplayRangeMax()

および

imp.getDisplayRangeMin()

で現在のディスプレイ範囲の上限と下限を取得することができます。

 また、ImagePlus の最大値と最小値は、

stat = imp.getStatistics()

で、一旦統計値を格納する変数を取得し、

stat.max, stat.min

で、知ることができますので、デバッグ作業を行う時は、以上を組み合わせて常時、ディスプレイ範囲と、最大値、最小値を逐一 print 文で出力させ、どのように値が変化しているのかをモニタする必要があると思います。

 例えば...

stat = imp.getStatistics()
print "DisplayRange: ", imp.getDisplayRangeMin(), " - ", imp.getDisplayRangeMax()
print "Value Range: ",stat.max, " - ", stat.min, " Mean:  ", stat.mean

また、メニューから確認するには Show Info... コマンドを使って下さい。

[image] → [Show info] コマンド

 ここまでで、ImageJ における 32bit 画像の挙動について 8 割ぐらいは解明できたように思いますが、まだ若干挙動不審な部分が残っています。

----------------------

[関連記事]

yasuo-ssi.hatenablog.com

yasuo-ssi.hatenablog.com

yasuo-ssi.hatenablog.com