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

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

ImageJ / Python プログラミング Tips: 画像の bit 深度によって gamma メソッドの動作が変わる

 先日も報告したように トーン再現カーブ (TRC) エンコード / デコードプログラムを作成していて大はまりしましたが、ようやく原因が分かりました。結局分かったことは、画像の bit 深度によって gamma メソッドの動作の仕方が異なるということです。

 以下 Ver. 1.54 以降をベースに動作について論じますが、8bit (byteProcessor) 画像の場合は、元の画像の明度値 (以下、v とする) を255で割って (つまり、v の範囲を 0.0 - 1.0 に変換して)、それに対し、ガンマで指定した値で累乗し、最後に 255 を掛けて、元の 0 - 255 の範囲の値に戻します。

 16bit (shortProcessor) の場合はディスプレイ範囲の値を基に計算します。ディスプレイ範囲の最小値を 0.0 最大値を 1.0 になるように元の v の値を変換し、それに対しガンマを掛けたあと(ガンマ値で累乗した後)、元の値の範囲になるように戻します。また、ディスプレイ範囲は、画像の足し算、掛け算などの演算を行うと変化する可能性がありますので、gamma  メソッドを掛ける前に、setDisplayRange で必ずディスプレイ範囲を明示的に与えないと、思わぬ結果になる場合があります。

 32bit (floatProcessor) の場合は、v をただガンマ値で累乗するだけです。従って元の v が 0.0 - 1.0 の範囲であれば、正しくガンマが掛かりますが、v の最大値がそれ以外、例えば、255 や 65535 だったりすると画像がおかしくなります。この場合は、ユーザ側で v の範囲を 0.0 - 1.0 になるように直してから gamma メソッドを掛けなくてはなりません。floatProcessor ではディスプレイ範囲を参照して gamma を掛けることはありません。

 ですので、先日 gamma メソッドは setDisplayRange メソッドとセットで使えと申し上げましたが、それはあくまで 16bit 画像においてのみの話、ということになります。とは言え読み込む画像の bit 深度が定まらない場合は、セットで使っても問題はないとは言えます。ただ、32bit 画像はまた扱いが違いますので...

 というわけで gamma メソッドの動作は bit 進度によって異なり、一貫性がありません。16bit のこの仕様は、メニューの Process → Math → Gamma 調整の動作に対応しているようです。これも 16bit 画像の時のみディスプレイ範囲の最小値と最大値を参照してガンマ補正をかけるようです。

 なお、自作のプログラムで、ImageJ Ver. 1.54 において、16bit 画像における gamma メソッドの動作が変わりましたが、どうやら、ImageJ Ver. 1.53 までは、16bit 画像においてディスプレイ範囲が、画像計算に従って自動的に動くことがなかったのが、1.54 においては 32bit 画像同様に、自動的に最大値、最小値がディスプレイ範囲として設定されるようになったためのようです。

 なお、この bit 深度による仕様の違いについては、公開されている JavaソースコードでimageProcessor, shortProcessor, floatProcessor での違いが確認できますその、ガンマ補正の計算式は以下のようになっています。

imageProcessor (byteProcessor) の場合

 v = (int)(Math.exp(Math.log(i/255.0)*value)*255.0);

shortProcessor の場合

    if (range<=0.0 || v1==min2)
        v2 = v1;
    else                    
        v2 = (int)(Math.exp(value*Math.log((v1-min2)/range))*range+min2);

 ※ min2 がディスプレイ範囲の下端閾値、range がディスプレイ範囲値 (上端 - 下端値) と思われます。

 

floatProcessor の場合

    if (v1<=0f)
        v2 = 0f;
    else
        v2 = (float)Math.exp(c*Math.log(v1));

 shortProcessor については Ver. 1.54 からソースコードが変更されたものと思われます。

 因みに、chat GPT にもこの仕様変更について聞いてみましたが、分からないとの答えが出てきました。仕様を変更したというリリースノートも公開されていないためだと思います。

 なお、ひょっとすると floatProcessor における gamma メソッドの挙動も、shortProcessor に合わせて今後変わる可能性を考慮する必要があるかと思います。とりあえずどう転んでも大丈夫なようにする対策としては、先日指摘したように、とりあえず gamma メソッドを掛ける時は常に直前に setDisplayRange でディスプレイ範囲を明示するとともに、32bit 画像の場合は、ユーザ側がブラックポイント、ホワイトポイントの範囲が、0.0 - 1.0 になるように明示的に変換するようにプログラムを書く、ということかと思います。