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

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

ImagePlus, ImageProcessor, ImageStackの関係 - ImageJ

 いままで、ImageJにおける、ImagePlus, ImageProcessor, ImageStackの関係の理解が不十分でした(個人的に)。今回この関係を整理したいと思います。

 まずImagePlusですが、これはImageJ上における、ほぼファイルに相当する概念です。GIMPで言えば、Image概念に相当します。それに対して、ImageProcessorとは画像データそのもので、GIMPで言えば drawable にほぼ相当します。

 そして、GIMPの1つのImageが複数のdrawable (レイヤーやマスク等)で構成されている場合があるのと同じように、一つのImagePlusは複数のImageProcessorで構成されることがあります(もちろん1枚の場合もあります)。その構造がImageStack です。ImageStackは、例えば、R, G, Bチャンネルの複数の画像で構成されている場合もありますし、あるいは同じサイズの別々の画像で構成されている場合もあります。例えば異なった細胞の写真が大量にスタックとして含まれた、1つのImagePlusというものも考えられます。概念を図式化すると下図のようになります。

f:id:yasuo_ssi:20211217091148p:plain

ImagePlus, ImageProcessor, ImageStackの関係

 ImageProcessorは、1pixel当たり、32bit (2進数32桁) で構成されているようです。但しすべてのImageProcessorが32birの領域をフルに使っているのではなくて、使っている領域幅によってImageProcessorは、以下のような下位分類があります。つまり、8bit、256種類の区分(0-255の整数)しかない ByteProcessor, 16bit、65536種類の区分(0-65535の整数)のある ShortProcessor、32bitフルに使う FloatProcessor (この場合は、区分は0~1.0の浮動小数点小数で表される)、RGBを1チャンネル当たり8bit x 3で表し、24bit使う、ColorProcessorの4種類です。

 

f:id:yasuo_ssi:20211217094701p:plain

ImageProcessorとその仲間

■ImageProcessorのピクセル値に対するアクセス

 ここで、今、特定のImagePlusオブジェクトを imp、ImageProcessorを ip、ImageStackを stk と仮に名付けます。

 そして、ImagePlusもしくはImageProcessorオブジェクトから、ある特定のPixelの値を変数 a に取得することを考えます。Pythonの場合式は以下のようになります。

a = imp.getPixel (x, y) (  または a = imp.get (x, y)  )

 or

a = ip.getPixel (x, y) (  または a = ip.get (x, y)  )

 or

a = imp.getProcessor().getPixel (x, y) (  または a = imp.getProcessor().get (x, y)  )

 

 この時、変数 a は、a[0], a[1], a[2], a[3]という配列(リスト変数)になります。これは原則、ImageProcessorの32bitデータを、下位8bitから上位8bitまで4段階、8桁ずつ一つの配列要素として読み込むためです。但し、1つのImageProcessorに1つのチャンネルしか割り当てられていない場合、例えばグレースケールだったり、R, G, Bの1つ分のチャンネルしか表していない場合は、値はindex 0番目に、つまり a[0]にまとめて入ります。そして、a[1]~a[3]は存在しますが、常に0の値が入ります。また ColorProcessorである場合は、a[0]がR, a[1]がG, a[2]がBとなり、a[3]は存在しますが常に0になります。データを取得した変数を配列ではない通常の変数だと錯覚するとエラーになりますので注意が必要です。なお、以前の解説では、カラーデータがスタック構造で収容されるため配列で読み込まれるのではないか、と書いていましたが、配列のインデックスとスタック構造は関係なく誤解でした。なお、以前の解説の誤りは訂正しておきました。スタック構造で読み込まれる場合のピクセルデータアクセスに関しては、この後説明します。

 また、ピクセルの値を書き換える場合は、putPixelメソッドを使い、以下のようになります。

ip.putPixel (x, y, value)

 or

ip.putPixel (x, y, [r, g, b])   ※ColorProcessorの場合

 この時は、getPixelとは異なり、直接ImagePlusオブジェクトに対して使うことはできません。やるとすればgetProcessorメソッドをはさんで、

imp.getProcessor().putPixel (x, y, value)

等と書く必要があるかと思います。また与えるvalueがリスト構造でない場合は、indexの0番目に値が代入されます。

■カラー画像の2つの構造

 カラー画像の場合、以前にも指摘しましたが、1チャンネル当たり各ピクセル 8bit以下と16bit以上で、次の2つの構造があります。

f:id:yasuo_ssi:20211217000744p:plain

COLOR_RGB
Color画像が単独のImageProcessor (ColorProcessor) として扱われている

 上の図は1ピクセル、1チャンネル当たり8bit以下の画像のケースです。RGBデータが、1枚のImageProcessor (ColorProcessor) にまとめられています。なお、Index3はアルファチャンネル用に予約されているのではないかと思いますが、はっきりしません。

 

f:id:yasuo_ssi:20211217000058p:plain

R, G, Bが別々のスタックとして扱われている状態

 こちらの図は、16bit以上の画像の場合です。ImageProcessorは32bitが限界なので、RGB全データを1枚のImageProcessorに収めることができず、スタック構造を使って3枚のImageProcessorに収めなければなりません。そして当然この両者でピクセルへのアクセス方法が異なってきます。

 RGBが1枚のImageProcessorに収まる場合は、上記のように、

a = imp.getPixel (x, y) (  または a = imp.get (x, y)  )

 or

a = ip.getPixel (x, y) (  または a = ip.get (x, y)  )

ip.putPixel (x, y, [r, g, b])  

等でOKです。

 なお、元画像が8bitであっても、bio-format importerを通じてスタック形式で読み込むと、16bit以上の画像と同じになります。その場合は、各ImageProcessorはByteProcessorになることになります。アクセスの方法は以下を参照してください。

■スタック構造のカラー画像へのピクセルアクセス

 しかし、スタックになっている場合は変わってきます。

 まず、非常に要注意なのは、スタック番号のindexが、通常Pythonの場合 0 からindex番号が始まるのが常識なのに、なぜか 1 から始まります。つまり、Rが1番目、Gが2番目、Bが3番目というindex番号になります。0番目のスタックは存在しません。

 それから直接ImageProcessorにアクセスできません。例えばGチャンネル (スタックの2番目) のピクセルにアクセスしようとして以下のように書くと失敗してしまいます。

 a = imp.getProcessor(2).getPixel(x, y)

 これにアクセスするには以下のように行う必要があります。

            stk = imp.getStack()
            a = stk.getProcessor(2).getPixel(x, y)

 つまり、ImagePlusから一旦ImageStackオブジェクトを取得しておきます。これに対しgetProcessorメソッドを使う時に、インデックス番号が使えます(以前この部分記述が間違っていました。ダイレクトに上の失敗例のようにアクセスできるように書いていました)。またこのようにピクセル値を取得した a は、やはり要素4つの配列になり、またこの場合、1つのImageProcessorあたり一つしかチャンネルがありませんので、[0]番目の要素しか値が入らないのも、既述の説明と同じです。

 因みにgetProcessorのインデックス番号に0および4以上の値を入れて呼び出そうとすると失敗しますので、これで、スタックのインデックス番号が1~3 であることが確認できます。なおやってみましたが、stk(i) というような呼び出しもできませんでした。

 スタックに何枚のImageProcessorが含まれているかは以下の式を使って、スタックサイズを入れる変数にその数を取得して確認できます。

            stacksize = stk.getSize()

 なお、これも以前記した通り、このようなスタック構造を持ったImagePlusオブジェクトをsaveAsTiffコマンドなどを使ってファイルに保存すると、R, G, BバラバラのマルチページのTIFFファイルとして保存されてしまうので、通常の画像ビュアーなどで見ると、Rチャンネルだけのグレースケール画像、もしくはRGBがばらばらのページにある、グレースケール画像に見えてしまいます。これはスタックがRGBデータ保存のためだけでなく、一般に複数の画像を収める構造であることを考えると仕方のない仕様だと思います。従って、保存したファイルをGIMPなどのチャンネル合成機能のある画像編集ソフトを使ってチャンネル合成を行い、通常のフルカラーTIFFファイルを作成しなければなりません。誰かが、フルカラーTIFFファイルを出力するプラグインを開発すればこの問題は解決すると思いますが、ちょっと探し当てられていません (bio-formatプラグインでは単独ページの16bit以上のフルカラーTIFFは出力できません)。

 なお、ImageJでこのようなRGBデータを格納したマルチページTIFFを bio-formatプラグインで読み込んで見るときは、ImageJだけが識別できるTIFF上の独自のタグを読み込んで識別しますので、ちゃんとしたRGB合成画像に見えます。