以前、以下のページで、イギリスの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研究所のサンプルをご覧ください。