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

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

ImageJ / Python プログラミング Tips: インターアクティブな UI 作成のポイント

 ImageJ における Python (Jython) を使ったインターアクティブなダイアログボックスUIの書き方について、何となく要領が分かってきました。ここでポイントをまとめてみます。

 まず説明の前提として、スライダーが一つ、数値入力ボックス (numeric field) が一つ、チェックボックスが一つのダイアログを作ることにします。そしてこのダイアログで入力した数値によってプレビュー画像が随時変化するというUIを考えます。このダイアログボックスは、dialogUI というユーザ定義関数で定義します。

 これらのオブジェクトを監視して、プレビュー画像を随時変更させるには、Java のAdjustmentListener, TextListener, ItemListener を使います。これで各オブジェクトを監視し、これらのオブジェクトの入力の変化をプレビュー画像に反映させるパーツとして、 imagePreviewer という名のクラスを設置します。

 そうするとおおむね以下のような構成になります。

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

# ライブラリの呼び出し記述

from ij import IJ
from ij import WindowManager as WM
from java.awt.event import AdjustmentListener, ItemListener, TextListener
from ij.plugin import Duplicator
from ij.gui import GenericDialog
import os
from org.python.core import codecs
codecs.setDefaultEncoding('utf-8')

 

#オブジェクトの入力変化を監視する class

class imagePreviewer(AdjustmentListener, ItemListener, TextListener):
    def __init__(self, imp,  nfield_variableA,  nfield_variableB, checkbox_variableC):

        #コンストラクタ直下にclass内で使う変数定義を記述
        #極力定義のみにし、実行コマンドはなるべく書かない
        #但し、画像や変数の初期値を保存しておく必要がある場合はここで取得し、保存する。

 

        self.imp = imp
        self.impPreview = Duplicator().run(imp) #プレビュー表示用の imagePlus
        self.impPreview.show() # プレビュー画面を表示
        WM.getActiveWindow().toFront()
        self.nfield_variableA = nfield_variableA
        self.nfield_variableB = nfield_variableB
        self.checkbox_variableC = checkbox_variableC

 

    def adjustmentValueChanged(self, event):

        #スライダー変更時に呼び出す関数
        #ここに書く実行コマンドは極力関数呼び出しの判断分岐のみにする
        #以下、イベント対応関数のすべてにおいて同じ

        if (IJ.isMacOSX() == False) and event.getValueIsAdjusting():
            return
            # スライダーが調整中の場合は再描画を行わない (execution を呼出さない)
            # このメソッドは Mac OS X では無効
       self.execution()

 

 

    def textValueChanged(self, event):

        #テキストボックス変更時に呼び出す関数

        self.execution()

 

    def itemStateChanged(self, event):

        #オブジェクト(チェックボックス)の状態変更時に呼び出す関数

        if event.getStateChange() == event.SELECTED:
            #Listener を設置したオブジェクトが選択されたときのみ再描画
            self.execution()

 

    def execution(self):        #描画実行関数

        #以下にイベント発生時に行うプレビュー画面の描画編集を記述
        #実行コマンドは分岐判断以外はここに集中させたほうが良い
        #パラメータの読み込みも、初期値読み込み以外は原則ここで行う
        #プレビュー画像を描画する際は、必ずオリジナル画像を保存しておいて、そのオリジナル画像を複写した画像に対して編集・加工して描画し、パラメータが変わった場合は、前の編集結果を廃棄し、オリジナルからもう一度編集・加工しなおすように注意!

        ip = self.imp.getProcessor()

        (この間に、 ip に対する画像編集コマンドを記述)

        self.impPreview.setProcessor(ip)

        # プレビュー画面のキャンバスとなる impPreview に編集済みの ip をセットする
        self.impPreview.updateAndDraw()

        # プレビュー画面を更新

 

def dialogUI(imp1):

    imp1.show()
    WM.getActiveWindow().toFront()
    gd = GenericDialog(" dialog title")

    # ダイアログ要素の設置
    gd.addSlider("variable A", 0.0, 1.0, 0.5, 0.01)   # Slider 0 & NField 0

    # 注: スライダーオブジェクトには、slider と numeric field の双方が含まれる

    gd.addNumericField("variable B", 1.0, 1)  #NField 1
    gd.addCheckbox("variable C", True) # CBox 0

   #インターアクティブなUIにかかわるダイアログ要素の定義
    slider_variableA  = gd.getSliders().get(0) # slider 0
    nfield_variableA  = gd.getNumericFields().get(0) # NField 0

    # 注: スライダーオブジェクトには、slider と numeric field の双方が含まれる

    nfield_variableB  = gd.getNumericFields().get(1) # NField 1
    checkbox_variableC = gd.getCheckboxes().get(0)  # CBox 0

   #イベントリスナーの定義と設置
    previewer =  imagePreviewer(imp1, nfield_variableA,  nfield_variableB, checkbox_variableC)
    slider_variableA.addAdjustmentListener(previewer) # set listener for upper slider
    #スライダーに関しては、numeric field にリスナーをつけない
    nfield_variableB.addTextListener(previewer)  # set listener for textbox  
    checkbox_variableC.addItemListener(previewer)  # set listener for preview checkbox

    #ダイアログボックスの表示
    gd.showDialog()

 

    #ダイアログボックスからの変数の取得
    variableA =  gd.getNextNumber() # NField 0
    variableB =  gd.getNextNumber() # NField 1
    variableC = gd.getNextBoolean() # ChkBox 0

   #イベントリスナーの取り外し
    slider_variableA.removeAdjustmentListener(previewer) # set listener for upper slider
    nfield_variableB.removeTextListener(previewer) # set listener for lower slider
    checkbox_variableC.removeItemListener(previewer)  # set listener for preview checkbox

    #取得した変数を戻り値として呼び出したプロセスに戻す
    return variableA, variableB, variableC

 

#---- 以下メインプログラム ------

imp0 = WM.getCurrentImage()

 

""" プレビュー用画像 (横幅 1280 pixls) の作成 """
imp1 = Duplicator().run(imp0) # imp1: preview imp
scale = 1280.0 / imp0.getWidth()
                    # preview image scaling value (width = 1280px fixed)
new_height =int(imp1.getHeight() * scale) # preview image height
imp1 = imp1.resize(1280, new_height, "none")

 

     #変数入力ダイアログボックスの呼び出し
variableA, variableB, variableC = dialogUI(imp1)

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

上を実行した場合のダイアログ

プレビュー画面

 

ImageJ でインターアクティブなUIをつくるポイント

■全般
    def __init__(self, ... ) コンストラクタ以下には、なるべく変数の定義のみを書きます。実行コマンドは極力書かないほうがよいです。但し、通常プレビューを作成する元画像を保管しておく必要がありますので、それだけはここで取得しておいてください。


 また、プレビュー画像は、原則オリジナル画像から作成し、パラメータを変えるごとに、前の作成結果は破棄します。一旦作成したプレビュー画像に更にパラメータ変更を追加すると、おかしくなります。

 

 パラメータの読み込みは、原則描画実行関数の中で読み込み、それ以外では読み込まないほうが良いです。但し、パラメータの初期値を保管しておく必要がある場合のみ (例えば、上で述べたプレビュー作成の基となる元画像の読み込みなど)、コンストラクタの直下で読み込んでおきます。

 

■各イベント発生に対応する関数
・名称は決まっており、任意の名前は付けられません。

    def adjustmentValueChanged(self, event):
    def itemStateChanged(self, event):
    def itemStateChanged(self, event): など...

 以下のページの各リスナー・インターフェースの項目を見ると、各リスナーに対するイベント対応関数の名前が確認できます。

docs.oracle.com

ここに書く実行コマンドは、描画関数を呼び出す分岐判断のみ書き、それ以外の実行コマンドは書かないのがおすすめです。

・各イベントで使えるプロパティやメソッドは Java のマニュアルをご参照ください (ImageJ の API マニュアルではなく)。


■スライダー

 スライダーは、スライダー自体と、数値ボックス (Numeric Field) から構成されます。プレビューアーは、スライダー本体に設置し(AdjustmentListener を使う)、スライダー付属の数値ボックスに設置すべきではありません。数値ボックスに TextListener をつけると、プレビュー画面の描画更新が頻繁に起こり表示が非常に重くなります。

 また、スライダーを動かすと、adjustmentValueChanged のイベントが発生しますが、event.getValueIsAdjusting() を取得したときはプレビューの書き換えを行わないようにすれば、スライダーを動かしたときの不必要なプレビュー画面の書き換えが抑制され、軽くなります。ただし、Mac OS では、おそらく Java のバグのため、event.getValueIsAdjusting() メソッドを使っても、スライダーが動作中かどうか検出できないので、どうしてもプレビュー画面の書き換えが重くなってしまいます。この場合、プレビューオン/オフチェックボックスを設置し、不要な時はプレビューの書き換えをマニュアルで抑制させるようにします。

 一方、値を取得するのは、slider 本体ではなく、Numeric Field から取得してください。slider 本体から取得もできますが、インターアクティブ UI で使う場合、Java のオブジェクトとして扱わなければならず、その場合整数しか取得できません。また換算しなければならないので、面倒です。

 

■描画実行関数
 (上のサンプルでは def execution(self) の部分)
・プレビューを描くキャンバスとなる imagePlus を用意しておきます(上のサンプルでは impPreview)。
・プレビューを書き直す場合は、作成元画像から ip を取得し、それに対して加工を行い、加工した ip をキャンバスである imagePlus にセットすることで書き換えます。書き換えた後、updateAndDraw() メソッドの実行をお忘れなく。

・パラメータは原則ここで描画実行直前に読み込んでおきます。

 

■プレビューアー中のImageJ Numeric Field からのパラメータの取得について

 ImageJ のダイアログ要素における Numeric Field は、数値入力ボックスです。そして ImageJ 上では原則そのまま数値とした入力したパラメータを取得することができます。

 しかし、プレビューアーの中ではすべてのダイアログ要素が Java のオブジェクトとして扱われます。そのため、ImageJ の Numeric Field はプレビューアーの中では、Java のテキストボックス・オブジェクト (java.awt.TextField クラス) として扱うことになります。そのためダイレクトにパラメータを数値として取得することができません。一旦テキストとしてパラメータを取得し、int() や float() でテキストの文字列を数値に変換するという手続きが必要になります。

 例えば...

        variableB = float(self.nfield_variableB.getText())

 self.nfield_variableB からは、getText() を使った、文字列の形でしか値を取得できないので、それを float() 関数を使って数値に直します。

 因みに、ImageJ の slider は、プレビューアーの中では、java.awt.Scrollbar クラス、もしくは、javax.swing.JSlider として、checkbox は、java.awt.Checkbox クラスとして扱われます。この時、ImageJ 上のオブジェクトの性質と、Java 上のオブジェクトの性質が同一であれば、問題はないのですが、上の Numeric Field のように性質が異なる場合があると要注意です。Numeric Field 以外の例としては、ImageJ の slider からは直接実数の値を取得できますが、java.awt.Scrollbar や javax.swing.JSlider としては整数しか取得できない、というような違いが出てきます。

 ImageJ で通常の値の取得の仕方と、プレビューアーでの値の取得の仕方で、取得の方法が異なる、一方は ImageJ のオブジェクトとして、他方は Java のオブジェクトして扱わないければならない、というような点には、プログラミング上非常に当惑させられますので、ご注意ください。

 

■プレビューアー・クラスで引数として取るべきダイアログ要素について

 イベントを監視するだけなら、そのダイアログ要素を引数として参照する必要はありません。単にダイアログ要素に対してプレビューアーを設置するだけで OK です。引数として取る必要があるのは、クラス内でそのダイアログ要素から値を読み取る場合だけです。

 例えばスライダーの場合、スライダーから値を読み取るより付属の Numeric Field から値を読み取ったほうが良いので、プレビューアーのクラスでは、Numeric Field を引数として指定する必要はありますが、スライダーは引数として指定する必要はありません。

 一方、スライダーに対してはプレビューアー (AdjustmentListener) を掛けます。しかし、スライダー付属の Numeric Field に対してはプレビューアー (TextListener) を掛けない方が良いと思います。Numeric Field にプレビューアーを掛けると、値が変わる度に描画しなおすので、動作が重くなってしまいます。

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

[関連情報]

yasuo-ssi.hatenablog.com

yasuo-ssi.hatenablog.com

yasuo-ssi.hatenablog.com

yasuo-ssi.hatenablog.com

yasuo-ssi.hatenablog.com