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

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

ImageJ / Python プログラミング Tips: Gamma 補正と setDisplayRange は必ずセットで使うべし

 以前、ImageJ で、sRGB 式の TRC をエンコード、デコードするプラグインスクリプトを公開しました。その中でちょっと手抜きをしていると書きましたが、今回その手抜きを止めて真面目に取り組むことにしました。ところがその修正作業で大はまり。

 まず sRGB 式 TRC のエンコード / デコード式は以下に出ています。

International Color Consortium (ICC) sRGB 仕様書
https://www.color.org/chardata/rgb/srgb.pdf

 もし、TRC がエンコードされている画像を、リニアにデコードする場合は以下の式に従います。

 L という添え字がついているのがリニアな値です。なお、上の式は誤植があり、R が縦に 3 つ並んでいますが、上から、R, G, B の間違いです。それはともかく、TRC のかかった R, G, B 値 (0.0 - 1.0 の範囲を取るとの前提) に 0.055 を足して、それを1.055 で割ったものを 2.4 上することで、リニアな RGB 値が求められます。

 しかし R, G, B の元の値がホワイトポイントに近い値だと、0.055 を足したときに、ImageJ 上で、8bit もしくは 16bit (byteProcessor, shortProcessor) だとデータクリッピングが起こる可能性があります。

 そこで一旦 32bit に直して計算し、その後、8 または 16bit に直そうとしましたが、なぜか異常に暗い画像になってしまいます。いろいろその理由をつかもうとやってみたのですが、理由がつかめませんでした。

 そこで、bit 深度を変えることなく、クリッピングが起こる可能性のある部分を一旦そうでない部分と分割し、計算を行ってから最後にもう一度合算し、最後に 2.4 上しようとしました。

 ところがなぜか変換後、シャドウ部のデータが消えてしまいます (ブラックポイントになってしまう)。これも理由がわからずさんざん検討したのですが、プログラムに間違いがあるとは思えません。

 数日さんざん悩んだ挙句、ふと気が付いて setDisplayRange の場所を確認しました。  

 以前以下にも書いておきましたが...

yasuo-ssi.hatenablog.com

yasuo-ssi.hatenablog.comこれらの記事に書いておいたように、setDisplayRange によるディスプレイ範囲の設定が計算に影響を与えます。

 で、当然そう書いた本人も分かっていますので、

imp.setDisplayRange(0.0, 65535.0)

などとと入れています。

 問題は setDisplayRange を実行した後、さらに imagePlus に対し計算を加えており、その後、その imagePlus に対し、

imp.getProcessor().gamma(2.4)

とやっていました。つまりディスプレイ範囲を設定した後、画像に対して足し算、引き算などの計算を加え、その後ガンマ補正を掛けているため、ガンマ補正前の画像計算の段階で、ディスプレイ範囲がずれてしまったのではないかと思われました。

そこで見直して、ディスプレイ範囲を設定するのをガンマ補正の直前に変更しました。

imp.setDisplayRange(0.0, 65535.0)
imp.getProcessor().gamma(2.4)

というような感じです。

 すると、ビンゴ。これを入れたことで正しく TRC がデコードされるようになりました。

 つまり、単に setDisplayRange をきちんと適用しましょう、だけではだめで、gamma 補正を掛ける直前に必ずディスプレイ範囲を設定しましょう、ということを確認する必要がある、ということです。gamma メソッドと setDisplayRange メソッドは共に手を取り合って並んでセットとして使う必要があるということが分かりました。

 

 なお、以前にも報告しましたが、ImageJ でガンマ補正などの計算が、16bit 画像 (shortProcessor) において、Ver. 1.53 → 1.54 で変わっています。ただ、それが、16bit 画像においてディスプレイ範囲を参照せずに変換していたのが (8bit 画像、byteProcessor と同じ挙動)、ディスプレイ範囲を参照して変換するように変わったのか、それとも、16bit 画像においても、それまで明示的に指示しない限りディスプレイ範囲が変わらなかったのが、32bit 画像のように計算によって自動的にディスプレイ範囲が変わるようになったためなのかは掴めていませんが(どうも後者のような気がします)、併せてご注意ください。