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

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

ImageJ / Python プログラミング Tips: 極力単純化した ROI を対話的に設定するダイアログ サンプルプログラム

 以前、以下のページで、イギリスのMRC分子生物学研究所で公開している、ROIを対話的に設定するサンプルプログラムの解説を載せました。

yasuo-ssi.hatenablog.com ただ、このサンプル、結構凝っていて、初学者には分かりにくいです。そこで、このサンプルプログラムを極力単純化してアレンジしてみました。こうすると必要最小限の骨格のみ見えますので、初学者にもより分かりやすいのではないかと思います。

 オリジナルプログラムは、イベントリスナーを各種使っていましたが、私がアレンジしたプログラムでは、2つだけで、ROIを監視して、テキストボックスを書き換えるのに使う、RoiListener (ImageJ) と、ボタンを押したときに (ボタンの動作を監視して) テキストボックスの値からROIを書き換えるのに使うActionListener (Java) の2つだけです。

 そして、ROIを監視してROIの変化に応じて自動的にテキストボックスを書き換えますので、逆にテキストボックスを監視してそれに応じてROIの自動書換えをやると、無限ループに入ってしまいます。そのため、ボタンを押したときのみROIを書き換えるようにしています。このボタンの動作を監視するのにActionListener (Java) を使います。またボタンを押したときにテキストボックスの入力内容をチェックし、整数でない文字が入っていた場合は何も行いません。

 なお、以前に記したとおり、ROIを直接マウスで操作しようとすると、ImageJのジェネリックダイアログを使うことはできません (ImageJのジェネリックダイアログ表示中はプレビュー画面のマウス入力を受け付けなくなるため)。そのためJavaのJFrameを使う必要があります。但し、テキストボックスに入れたパラメータからROIを描くだけなら、ジェネリックダイアログを使っても差し支えありません。

では、コードを提示します。

# Simplest ROI interactive dialog sample
# Ohnishi, Yasuo 2022-08-11  
  
from ij import IJ, ImagePlus
from ij.gui import RoiListener, Roi  
from java.awt.event import WindowAdapter  
from java.awt import Color, Rectangle, GridBagLayout, GridBagConstraints as GBC  
from javax.swing import JFrame, JPanel, JLabel, JTextField, BorderFactory, JOptionPane  
from java.awt.event import ActionListener
from javax.swing import JButton

class RoiMaker(ActionListener):  
    def __init__(self, textfields):  
        """ textfields: the list of 4 textfields for x,y,width and height. 
        index: of the textfields list, to chose the specific textfield to listen to. """  
        self.textfields = textfields  
        self.values = [0, 0, 0, 0]  
        try:
            for index in range(4):
                self.values[index] = int(self.textfields[index].getText())
        except:
            return

    def actionPerformed(self, event):
        """ event: a textValueChanged with data on the state of the numeric field. """
        try:
            for index in range(4):
                self.values[index] = int(self.textfields[index].getText())
            # check scripts in text fieldd are integer
            imp = IJ.getImage()  
            if imp:  
                imp.setRoi(Roi(*[int(tf.getText()) for tf in self.textfields]))  
        except:      
            return

class TextFieldUpdater(RoiListener):  
    def __init__(self, textfields):  
        self.textfields = textfields  
    
    def roiModified(self, imp, ID):  
        """ When the ROI of the active image changes, update the textfield values. """  
        if imp != IJ.getImage():  
            return # ignore if it's not the active image  
        roi = imp.getRoi()  
        if not roi or Roi.RECTANGLE != roi.getType():  
            return # none, or not a rectangle ROI  
        bounds = roi.getBounds()  
        if 0 == roi.getBounds().width + roi.getBounds().height:  
            bounds = Rectangle(0, 0, imp.getWidth(), imp.getHeight())  
        self.textfields[0].setText(str(bounds.x))  
        self.textfields[1].setText(str(bounds.y))  
        self.textfields[2].setText(str(bounds.width))  
        self.textfields[3].setText(str(bounds.height))  
  
  
class CloseControl(WindowAdapter):  
    def __init__(self, roilistener):  
        self.roilistener = roilistener  
    
    def windowClosing(self, event):  
        answer = JOptionPane.showConfirmDialog(event.getSource(),  
                                          "Are you sure you want to close?",  
                                          "Confirm closing",  
                                          JOptionPane.YES_NO_OPTION)  
        if JOptionPane.NO_OPTION == answer:  
            event.consume() # Prevent closing  
        else:  
            Roi.removeRoiListener(self.roilistener)  
            event.getSource().dispose() # close the JFrame  

  
def specifyRoiUI(roi=Roi(0, 0, 0, 0)):  
    # A panel in which to place UI elements  
    panel = JPanel()  
    panel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10))  
    gb = GridBagLayout()  
    panel.setLayout(gb)  
    gc = GBC()  
    
    bounds = roi.getBounds() if roi else Rectangle()  
    textfields = []  
  
  # Basic properties of most UI elements, will edit when needed  
    gc.gridx = 0 # can be any natural number  
    gc.gridy = 0 # idem.  
    gc.gridwidth = 1 # when e.g. 2, the UI element will occupy  
                     # two horizontally adjacent grid cells   
    gc.gridheight = 1 # same but vertically  
    gc.fill = GBC.NONE # can also be BOTH, VERTICAL and HORIZONTAL  
    
    for title in ["x", "y", "width", "height"]:  
        # label  
        gc.gridx = 0  
        gc.anchor = GBC.EAST  
        label = JLabel(title + ": ")  
        gb.setConstraints(label, gc) # copies the given constraints 'gc',  
                                 # so we can modify and reuse gc later.  
        panel.add(label)  
        # text field, below the title  
        gc.gridx = 1  
        gc.anchor = GBC.WEST  
        text = str(getattr(bounds, title)) # same as e.g. bounds.x, bounds.width, ...  
        textfield = JTextField(text, 10) # 10 is the size of the field, in digits  
        gb.setConstraints(textfield, gc)  
        panel.add(textfield)  
        textfields.append(textfield) # collect all 4 created textfields for the listeners  
        gc.gridy += 1  

# add Button
    gc.gridx = 0  
    gc.anchor = GBC.WEST  
    listener = RoiMaker(textfields)  
    button = JButton("Renew ROI")
    button.addActionListener(listener)  
    gb.setConstraints(button, gc)  
    panel.add(button)  
    gc.gridy += 1  

  # User documentation (uses HTML to define line breaks)  
    doc = JLabel("Click on a field to activate it, then:"  
            + "Type in integer numbers")  
    gc.gridx = 0 # start at the first column  
    gc.gridwidth = 2 # spans both columns  
    gb.setConstraints(doc, gc)  
    panel.add(doc)  
    
  # Listen to changes in the ROI of imp  
    roilistener = TextFieldUpdater(textfields)  
    Roi.addRoiListener(roilistener)  
  
  # Show panel in a closable window  
    frame = JFrame("Specify rectangular ROI")  
    frame.getContentPane().add(panel)  
    frame.pack() # make UI elements render to their preferred dimensions  
    frame.setLocationRelativeTo(None) # center in the screen  
  # Prevent closing the window directly from clicking on the 'x' window icon  
    frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE)  
    frame.addWindowListener(CloseControl(roilistener)) # handles closing the window  
    frame.setVisible(True)  
  
  
# Launch: open the window  
imp = IJ.getImage()
if imp:
    IJ.selectWindow(imp.getTitle())
    specifyRoiUI(roi=imp.getRoi() if imp else None)

まず、読み込むライブラリーですが...

from ij import IJ, ImagePlus
from ij.gui import RoiListener, Roi  
from java.awt.event import WindowAdapter  
from java.awt import Color, Rectangle, GridBagLayout, GridBagConstraints as GBC  
from javax.swing import JFrame, JPanel, JLabel, JTextField, BorderFactory, JOptionPane  
from java.awt.event import ActionListener
from javax.swing import JButton

ImageJ から、基本的なライブラリと、RoiListener 関連のライブラリを呼び出します。

Java からは、ダイアログを作る、JFrame とその部品に関するライブラリ、そしてイベントリスナーとして ActionListener を呼び出します。なお、新たに追加・変更したコードは青字で示します(以下同)。削除したコードについては、オリジナルのサンプルコードと比べてみてください。

 

 次は、クラス、RoiMakerです。ボタンを押したときに、テキストボックスからパラメータを読み込んで ROI を描きます。

class RoiMaker(ActionListener):  
    def __init__(self, textfields):  
        """ textfields: the list of 4 textfields for x,y,width and height. 
        index: of the textfields list, to chose the specific textfield to listen to. """  
        self.textfields = textfields  
        self.values = [0, 0, 0, 0]  
        try:
            for index in range(4):
                self.values[index] = int(self.textfields[index].getText())
        except:
            return

 使うリスナーはボタンの動きを感知する ActionListenerのみ、そしてコンストラクタでは、テキストボックスである、textfields を引数として設定します。それから、self.values という配列も初期化し、さらにダイアログ上のテキストボックスから値を読み込んでいますが、この配列はテキストボックスの内容が整数かどうかを確かめるために使います。

 そしてこのクラスのメソッドとして、actionPerformed を定義しています。これはActionListenerのイベント、actionPerformed に対応するものなので、名称は変えられません。これはボタンが押されたときに発動します。

 なお、MRC研究所のオリジナルサンプルコードでは、ここはかなり複雑な役割を担っていましたが、ここでは必要最小限の機能に絞っていますので、大幅に簡略化されています。

    def actionPerformed(self, event):
        """ event: button is pushed """
        try:
            for index in range(4):
                self.values[index] = int(self.textfields[index].getText())
            # check scripts in text fieldd are integer
            imp = IJ.getImage()  
            if imp:  
                imp.setRoi(Roi(*[int(tf.getText()) for tf in self.textfields]))  
        except:      
            return

 テキストボックスの内容を整数化して、self.valuesに入れます。問題がなければ、開かれている画像を取得して、画像上にテキストボックスに入れられたパラメータに従ってROIを描き、もし整数化に失敗したら何もしません。

 つぎは、クラス TextFieldUpdater ですが、ROIの動きを監視し、それに応じた座標値をテキストボックスに書き込みます。RoiListenerを使っています。これは MRC 研究所のサンプル通りなので、解説は以前の解説をご参照ください。

class TextFieldUpdater(RoiListener):  
    def __init__(self, textfields):  
        self.textfields = textfields  
    
    def roiModified(self, imp, ID):  
        """ When the ROI of the active image changes, update the textfield values. """  
        if imp != IJ.getImage():  
            return # ignore if it's not the active image  
        roi = imp.getRoi()  
        if not roi or Roi.RECTANGLE != roi.getType():  
            return # none, or not a rectangle ROI  
        bounds = roi.getBounds()  
        if 0 == roi.getBounds().width + roi.getBounds().height:  
            bounds = Rectangle(0, 0, imp.getWidth(), imp.getHeight())  
        self.textfields[0].setText(str(bounds.x))  
        self.textfields[1].setText(str(bounds.y))  
        self.textfields[2].setText(str(bounds.width))  
        self.textfields[3].setText(str(bounds.height))  

 

 次は、クラス CloseControl です。これも MRC研究所のサンプル通りですので説明は省略します。ただ一点だけ、ウィンドウを閉める時に RoiListener を画像から外すために、RoiListener を引数として取ります。

class CloseControl(WindowAdapter):  
    def __init__(self, roilistener):  
        self.roilistener = roilistener  
    
    def windowClosing(self, event):  
        answer = JOptionPane.showConfirmDialog(event.getSource(),  
                                          "Are you sure you want to close?",  
                                          "Confirm closing",  
                                          JOptionPane.YES_NO_OPTION)  
        if JOptionPane.NO_OPTION == answer:  
            event.consume() # Prevent closing  
        else:  
            Roi.removeRoiListener(self.roilistener)  
            event.getSource().dispose() # close the JFrame

 

 次はダイアログを定義する、specifyRoiUIです。変えたところのみ解説します。まず、オリジナルのサンプルにあった、テキストボックスにつけていた、マウスやキー動作に関するリスナーはすべて外しています。

def specifyRoiUI(roi=Roi(0, 0, 0, 0)): 
    # A panel in which to place UI elements  
    panel = JPanel()  
    panel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10))  
    gb = GridBagLayout()  
    panel.setLayout(gb)  
    gc = GBC()  
    
    bounds = roi.getBounds() if roi else Rectangle()  
    textfields = []  
 
  # Basic properties of most UI elements, will edit when needed  
    gc.gridx = 0 # can be any natural number  
    gc.gridy = 0 # idem.  
    gc.gridwidth = 1 # when e.g. 2, the UI element will occupy  
                     # two horizontally adjacent grid cells   
    gc.gridheight = 1 # same but vertically  
    gc.fill = GBC.NONE # can also be BOTH, VERTICAL and HORIZONTAL  
    
    for title in ["x", "y", "width", "height"]:  
        # label  
        gc.gridx = 0  
        gc.anchor = GBC.EAST  
        label = JLabel(title + ": ")  
        gb.setConstraints(label, gc) # copies the given constraints 'gc',  
                                 # so we can modify and reuse gc later.  
        panel.add(label)  
        # text field, below the title  
        gc.gridx = 1  
        gc.anchor = GBC.WEST  
        text = str(getattr(bounds, title)) # same as e.g. bounds.x, bounds.width, ...  
        textfield = JTextField(text, 10) # 10 is the size of the field, in digits  
        gb.setConstraints(textfield, gc)  
        panel.add(textfield)  
        textfields.append(textfield) # collect all 4 created textfields for the listeners  
        gc.gridy += 1  

# add Button
    gc.gridx = 0  
    gc.anchor = GBC.WEST  
    listener = RoiMaker(textfields)  
    button = JButton("Renew ROI")
    button.addActionListener(listener)  
    gb.setConstraints(button, gc)  
    panel.add(button)  
    gc.gridy += 1  

  # User documentation (uses HTML to define line breaks)  
    doc = JLabel("<html><br>Click on a field to activate it, then:<br>"  
            + "Type in integer numbers</html>")  
    gc.gridx = 0 # start at the first column  
    gc.gridwidth = 2 # spans both columns  
    gb.setConstraints(doc, gc)  
    panel.add(doc)  
    
  # Listen to changes in the ROI of imp  
    roilistener = TextFieldUpdater(textfields)  
    Roi.addRoiListener(roilistener)  
  
  # Show panel in a closable window  
    frame = JFrame("Specify rectangular ROI")  
    frame.getContentPane().add(panel)  
    frame.pack() # make UI elements render to their preferred dimensions  
    frame.setLocationRelativeTo(None) # center in the screen  
  # Prevent closing the window directly from clicking on the 'x' window icon  
    frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE)  
    frame.addWindowListener(CloseControl(roilistener)) # handles closing the window  
    frame.setVisible(True)  

 ダイアログの部品上でリスナーがついているのは、新規につけた [Renew ROI] というボタンのみで、こちらにテキストボックスを引数として、RoiMaker という上で定義しておいたリスナー(中身はアクションリスナー)をつけています。なお、MRC研究所のオリジナルサンプルでは、RoiMakerというリスナーは各テキストボックス(における、マウスやキーボード入力)を監視するリスナーとして定義されていましたが、今回はボタンの動作を監視するリスナーに変えています。そのため、大幅に簡略化できました。

 なお、ここではRoiMaker等、リスナーをクラスとして定義し、それをインスタンス化したものを、各部品に、add~Listener で付けています。しかし、リスナーが event 以外の引数を取る必要がない場合は、部品によっては、クラスとして定義せず、単なる関数 (def で始まる) として定義し、さらに add~Listener 文でリスナーすら設置しなくても済みます。

 例えば、ボタンだったら

button = JButton("Area", actionPerformed=measure) 

この一文だけですみ (actionPerformed イベントが発生したら、measure関数を実行する、と定義)、イベントリスナーをライブラリで読み込んでおく必要すらありません。その具体例は、以下の MRC研究所のサンプルをご覧ください。

syn.mrc-lmb.cam.ac.uk