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

戦前型旧型国電および鉄道と変褪色フィルム写真を中心とした写真補正編集の話題を扱います。写真補正技法への質問はコメント欄へどうぞ

ImageJ / Pythonプログラミングの盲点: 画像の代入

 現在ここのサイトで公開しているImageJ用プラグインスクリプトは、基本的にImageJでマニュアル操作を行ってマクロを記録し、それをPython (Jython)で書き換え、さらに、ファイル選択などのダイアログなどを書き加えて作成しています。

 ただ、この方式だと常にアクティブなウィンドウに表示されている画像に対してしか操作できません。例えば、次のようなコードです。

from ij import IJ →ImageJ用ライブラリを使用する宣言

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

IJ.selectWindow(filename +"_G.tif")   →アクティブ・ウィンドウの選択
IJ.run("Copy") → アクティブ・ウィンドウの画像をクリップボードへコピー
IJ.run("Internal Clipboard") クリップボードから新しいウィンドウへ画像を貼り付け
IJ.run("Multiply...", "value=0.5") →新しく開いたウィンドウの画像データに0.5を掛ける

IJ.run("マクロコマンド") でマクロを実行します

 これだと、複雑なことをやらせようとするときに、制約が出ます。もうちょっと複雑なことをやる勉強もかねて、ウィンドウを開かなくても動くよう、なるべくウィンドウを開かず、マクロ・コマンドを使わない書き換えを試みています。

 ところがその最中に、はまりました。

 

例えば a と b という変数があるとします。通常プログラミングでは、

b = a

と書くと、a という変数のデータが b という変数に代入されます。そのあと、

b = b + 1

と計算してbの内容を書き換えても a のデータが書き換わることはありません。

 

ところで、ImageJでは画像データは imagePlusというクラスのオブジェクトとして扱われています。仮に今、imp0とimp1というimagePlus画像データ変数があるとします。

imp1 = imp0

こうするとimp0 の内容が imp1という別の画像データ変数に代入されるものとてっきり思っていました。従って、この場合、imp1に対しても何らかの操作を加えてもimp0の内容は書き換わらないと思っていました。

   .... ところが、違うことがわかりました...OLZ

 imp1 = imp0 は結局、imp1という変数に、imp0という変数のデータを代入したのではなく、imp0にimp1というエイリアス(別名)をつけた、というだけなのですね。従って、imp1に対して操作を加えてデータ内容を書き換えると、imp0のデータ内容も書き換わってしまうという... これって画像データプログラミングでは常識なのでしょうか? いままで画像データを扱うプログラミングをやったことがなかったんで知らなかった... あるいは imageJだけの常識?

 で、imp0の内容を別変数imp1に代入したい場合は次のように行う必要があるようです。

from ij.plugin import Duplicator  →Duplicatorというライブラリを使う宣言

imp1 = Duplicator().run(imp0) →Duplicatorを実行し、imp0の内容をimp1に代入

もしくは、

imp1 = imp0.duplicate()

これは、ちょうどマクロコマンドで

IJ.run("Copy") 
IJ.run("Internal Clipboard")

とやるのと同等な操作を明示的に指定する必要があるということです。今、公開しているプラグインスクリプトは基本的にこのクリップボード経由でコピーするコマンドを使って,画像データの複写を行っているので正しく動作していた訳です。

 これって非常に重要な情報だと思いますが、こういう対策情報は日本語では一切見つからなくて、下記のサイトを見てやっとわかりました。とはいえこのページでも imp1 = imp0 というような式を立てるかわりに、imp1 = imp0.duplicate() を使えと、明示的に指南しているわけではありません。

wiki.cmci.info

 プログラムの動作がおかしい原因を突き止めるところから、その対策を探し出すのに、これだけでまる一日かかりました... ふぅ....

 これってひょっとすると業界の語られない暗黙知の一つかもしれませんので、ここに記しておきます。

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

 それと問題にimagePlusとウィンドウの関係があります。上に記したように、ImageJのマクロコマンドはIJ.run()というコマンドで呼び出して使うことができます。そして今仮に、imagePlusの変数を impとしておくと、その場合、

IJ.run(imp, "コマンド")

で、呼び出すと、ウィンドウを開かなくてもimpに対してマクロコマンドを実行できるようですが (ただし、コマンドによってはうまくいかない)、このimpを一旦ウィンドウで開いておいて(例えば、imp = IJ.getImage() で、アクティブなウィンドウの画像データをimpに取得しておくとか、imp.show()でウィンドウ表示をさせるなど)、その後そのウィンドウを閉じると、ウィンドウを閉じた後に、IJ.run(imp, "コマンド")でimpを呼び出そうとすると、 Null Pointer でエラーになります。impを一度もウィンドウに表示しなければ問題がない様なのですが... ウィンドウを閉じるとimpもリセットされてしまいます。

 結局、どうやら、imp = IJ.getImage() も imp0 = imp1 と同様、別のポインタにデータを複写したのではなく、同じポインタに別のエイリアスをつけただけなので、一方に変更を加えると、他方も変わる、と理解しておくのが良さそうです。

 つまり、仮に ウィンドウに表示されている画像データを imp に入れた場合、impの内容を変更すればウィンドウの内容も変更され、ウィンドウの内容を変更すると imp の内容も変更され、ウィンドウを閉じれば imp のデータも消える、ということのようです。

 この場合、imp.hide() を使うと imp のデータを消さずにウィンドウを閉じることができます。またウィンドウを開いておいて、データを変更し、imp.close() で閉じようとすると、Save するかと聞いてきます。これも imp.hide() を通してから imp.close() を走らせると、聞かれずに imp を閉じる (消す) ことができます。

 とりあえず、今はなるべくマクロコマンドを使わなくて済む場合は他のライブラリのコマンドで代替するよう、なるべく書き換えていますが、この代替コマンドを探し出すのがまた一苦労です。いまいち、この畑の「土地」勘がないので、探すのが大変なうえ、例え、それっぽいものが見つかっても使い方がよくわからずに、エラーが出たり試行錯誤の連続です。

 Pythonもにわか勉強ですが、Pythonの文法自体が分からなくて苦労するよりは、ライブラリの使い方や、その動作確認で9割以上のエネルギーが割かれる感じです。

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

  これで、ウィンドウの開かないサイレントバージョンのプラグインスクリプトができても公開はしないかもしれません。というのはウィンドウが開かずに実行されるとユーザから見て実行されているのかいないのかが分かりにくいですし、つまりません。今のバージョンはウィンドウが開いて、いろいろ画像が加工されるのがよく分かるので見ていて楽しいです。いかにもImageJが一所懸命仕事しています、という感じがしますので...

------

※お断り
 当記事は、ImageJ - Fijiディストリビューションの記事執筆時点での最新バージョンを前提とした記事です。ImageJのオリジナルバージョンだと、環境設定をご自分で整備しない限り、同様に動作しない可能性があります。

-------

追記:

 この話、Pythonのリスト変数 (配列) の扱いと同じでした。Pythonの教科書を見ていたら出てきました。仮に、リスト名を list_1 とすると、list_2 = list_1 とやっても、単に新たな名前のポインタを作っただけで、ポインタの示す先は同じのようです(cf. 辻真吾, 2018, Pythonスタートブック[増補改訂版], pp. 314-5 )。

 リスト変数を全く別の変数としてコピーを作る場合は import copy とライブラリを呼び出して、さらに list_2 = copy.copy(list_1) とやらないといけないようですが、imagePlus オブジェクトもこれと同じですね。