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

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

ImageJ / Python: 対話型インターフェースのお勉強 (バックグラウンドスレッドで動かす)

 今回取り上げるのは「Managing UI-launched background tasks (ユーザインターフェースから起動するバックグラウンドタスクのマネージング)」というサンプルプログラムです。このプログラムは、前回と全く同様、プレビュー画面で画像を拡大したり縮小することができ、外見も変わりません。どこが違うかというと、バックグラウンドでプレビュー画面の制御を行う点です。画像の拡大・縮小プレビューぐらいだとさほどタスクの負荷は大きくありませんが、負荷の高いプレビューを行うと、プレビューの画像処理を行っている間、ユーザの入力を受け付けなくなります。これをユーザ入力と、プレビュー画面の作成を別々のタスクで行うことで、そのようなことをなくそうということのようです。

 ただこのサンプルプログラムはなかなか手ごわいです。Javaの知識とPythonの知識がかなり要求されます。前回のサンプルプログラムは、このチュートリアルに出てくるその前のサンプル群に比べて、いきなり30~40度の坂道になった感じですが、今回のプログラムは、さらにいきなり、6, 70度ある坂道を鎖を頼りに登るような難易度になっています。私の解釈もこれで正しいのか自信がありません。間違いがありましたらご指摘いただけると幸いです。

 まず、前回取り上げたサンプルプログラム今回のプログラムの大きな違いについて説明します。前回のプログラムは、イベントリスナーを継承した、ユーザが定義した、ScalingPreviewer というオブジェクトがプレビュー画面の拡大・縮小などの動作を行っていました。具体的には、

 

ユーザがユーザインターフェースを動かすイベントが発生

    ↓

イベントを受けて、ScalingPreviewer の adjustmentValueChangedメソッドもしくは、itemStateChanged メソッドが起動

    ↓

これらのメソッドが、scale メソッドを呼び出す

    ↓

scaleメソッドが、まず元の画像から拡大・縮小した画像データ (imageProcessor) を作成

    ↓

さらに作成した画像データをプレビュー画面である imagePlus オブジェクトに適用

 

あるいは、

 ダイアログボックスがキャンセルボタンを押されて終了するとき、ScalingPreviewer が resetメソッドを明示的に指定されて呼び出され、resetメソッドがプレビュー画面であるimagePlusオブジェクトにオリジナル画像(imageProcessor)を適用して終了する

のいずれかでした。

 今回取り上げるサンプルプログラムでは、adjustmentValueChanged、itemStateChangedが scaleメソッドを呼び出すまでは一緒ですが、scaleメソッドやresetメソッドは直接 imageProcessorやimagePlusを操作することはありません。scaleやresetはputStateメソッドを呼び出して、ScalingPreviewer コンストラクターで定義をしておいた、プロセスの状態を指示もしくは表示する self.stateリスト変数の内容を書き換えるだけです。

 

 そして、実際に imageProcessorやimagePlus書換動作は Runnableインターフェースに対応した、run メソッドが self.state 変数の状態を監視していて、その値が変化していれば、プレビュー画面の拡大・縮小操作やリセット操作を行うという、前回のプログラムに比べて遠回りな動作を行っています。

 さらに、ScalingPreviewer自体が、コンストラクターで、javaの Executors ライブラリの newSingleThreadScheduledExecutor メソッドを使って0.3秒ごとに起動されます。それがRunnableインターフェースを設置していますので、それぞれが別のスレッドとして起動するようです。さらに、イベントを感知する各メソッドにJythonのsynchronizeライブラリのmake_synchronizedでデコレートすることで、各スレッドが1つのタスクを担うようにしているようです。これによりユーザの入力を感知してself.state変数を書き換えるスレッドと、画像の書き換えを行うスレッドを分離するということのようです。

 さらに、プレビュー画面の書き換えも、javaのSwingUtilitiesライブラリの invokeAndWaitメソッドを通して行うことで、複数のスレッドが同時にプレビュー画面の書き換えを行わないよう制御しているようです。

 ただ、そうだとすると、ScalingPreviewer が起動している間は、どんどんスレッドが増殖してしまうはずです(この解釈でいいのか?)。そこでダイアログを閉じる際に、destroyメソッドを指定してScalingPreviewerを呼び出し、スレッドを生み出す newSingleThreadScheduledExecutor を停止してスレッドの生産と実行を停止させ、ScalingPreviewerを止める、ということをやっているようです。

 以下具体的にコードを見ていきます。

---------

 まず前回に比べて次のようなライブラリがロードされます。

from java.util.concurrent import Executors, TimeUnit
from java.lang import Runnable
from javax.swing import SwingUtilities
from synchronize import make_synchronized

このうち、Runnableというライブラリが、複数のスレッドでプログラムを動かすためのライブラリのようです。またスレッドをどんどん作っていくために、Executors, TimeUnitが、一つのイベントに一つのスレッドを対応させるために、make_synchronized*1、複数のスレッドが同時にプレビュー画面の書き換えを行わないために、SwingUtilitiesが呼び出されているようです。 

 メインプログラム部分が1行なのは前回と変わらず。また、def scaleImageUI(): も基本的には一緒ですが、最後に、 previewer.destroy() という1行が追加されています。これは ScalingPreviewer (イベントリスナー & Runnable) を停止するコードのようです。

 問題は、 ScalingPreviewer の定義です。ここが大きく変わっています。
まず、AdjustmentListener, ItemListenerに加え, Runnableが継承されています。

中身も、コンストラクターに加え、def getState、def putState、def adjustmentValueChanged、def itemStateChanged、def reset、def scale、def run、def destroy と関数が増えています。スレッドを別々に動かすのに def run が大きい役割を果たしているようです。

 以下にJavaにおけるRunnableの使い方が説明されています。このサンプルプログラムは、以下の説明のうち、Runnableインターフェイスjava.langパッケージ)を実装したサブクラスを用意する方法をJythonに書き換えたものだと思います。

java-code.jp

 で、まずコンストラクターから見ていきます。

def __init__(self, imp, slider, preview_checkbox):
"""
imp: an ImagePlus
slider: a java.awt.Scrollbar UI element
preview_checkbox: a java.awt.Checkbox controlling whether to
dynamically update the ImagePlus as the
scrollbar is updated, or not.
"""
     self.imp = imp
     self.original_ip = imp.getProcessor().duplicate() # store a copy
     self.slider = slider
     self.preview_checkbox = preview_checkbox
# Scheduled preview update
     self.scheduled_executor = Executors.newSingleThreadScheduledExecutor()
# Stored state
     self.state = {
          "restore": False, # whether to reset to the original
          "requested_scaling_factor": 1.0, # last submitted request
          "last_scaling_factor": 1.0, # last executed request
          "shutdown": False, # to request terminating the scheduled execution
     }
# Update, if necessary, every 300 milliseconds
     time_offset_to_start = 1000 # one second
     time_between_runs = 300
     self.scheduled_executor.scheduleWithFixedDelay(self,
     time_offset_to_start, time_between_runs, TimeUnit.MILLISECONDS)

 

  前回のサンプルプログラムとの異なる点だけ取り上げます。

     self.scheduled_executor = Executors.newSingleThreadScheduledExecutor()

この、newSingleThreadScheduledExecutor というのは、指定期間おきに実行するスレッドということらしく、その時間が コメント文 # Update, if necessary, every 300 milliseconds 以下に定義されています。このメソッドは、プレビュアーを停止させる指令を出すために使われているようです。これをインスタンス化して self.scheduled_executor と名付けています。

 

# Stored state
     self.state = {
          "restore": False, # whether to reset to the original
          "requested_scaling_factor": 1.0, # last submitted request
          "last_scaling_factor": 1.0, # last executed request
          "shutdown": False, # to request terminating the scheduled execution
     }

こちらは、プログラム稼働状態の示す辞書型リスト変数です。画像の拡大・縮小を中止し原状回復するかどうかを示す "restore"、拡大・縮小率を格納する "requested_scaling_factor"、最後の拡大・縮小率を格納する、"last_scaling_factor"、プレビュアーを終了するかどうかを決める、 "shutdown" の四つの項目が定義されています。

そして上で書いたように、以下は self.scheduled_executor に与える時間指定のためのパラメータです。 

     time_offset_to_start = 1000 # one second

     time_between_runs = 300

以下のコードで時間パラメータを与え self.scheduled_executor を実行しています。

     self.scheduled_executor.scheduleWithFixedDelay(self,
     time_offset_to_start, time_between_runs, TimeUnit.MILLISECONDS)

以上のコードにより、1秒経過したところから0.3秒間隔で別々のスレッドとして何度も ScalingPreviewer が再起動されるということかと思います。

 

 次に、ユーザが定義する getStateメソッドです。geetStateは要するに、プログラムの状態を示す、self.state リスト(配列)変数から値を読みだすメソッドです。

@make_synchronized
def getState(self, *keys):
""" Synchronized access to one or more keys.
Returns a single value when given a single key,
or a tuple of values when given multiple keys. """
     if 1 == len(keys):
          return self.state[keys[0]]
     return tuple(self.state[key] for key in keys)

まず、@から始まる良く分からない表現が目につきます。これは何かと言うと、シンタックスシュガーという記法で記述されたデコレータです。要は、getStateメッソドの返し値として、本来の返し値をさらにmake_synchronizedという関数を通した値を、返すということのようです*2make_synchronized は、ライブラリとして呼び出されています。このライブラリはJython用のライブラリのようです。役割としては、getStateまたはputStateメソッドがself.stateにアクセスしているときに他のスレッドがself.stateにアクセスできないようにするらしいです(getSteteがself.stateを読み込んでいるときにputStateが割り込んでself.stateを書き換えるようなことを防止する)。

 さらに引っかかるのは、def getState(self, *keys) で引数に指定されている *keys です。これは数が不定の引数を受け付ける表現で、引数が複数ある場合、リスト変数の形で受け付けるようです。ここでの keys は具体的には self.state変数を読みだすキーが想定されています。つまり、 "restore",  "requested_scaling_factor",  "last_scaling_factor", "shutdown"のいずれかが想定されているわけで、この中のどれか一つが充てられるかもしれないし、複数が充てられるかもしれない、ということです。

 で、keysの数が1つなら、引数の一番目のkeyに対応する値が返り値として返され、複数あるなら、タプル(値を変更することができない配列定数)として、self.stateの値を読みだして返り値とするということです。

 

次のputStateメソッドはよりシンプルです。

@make_synchronized
def putState(self, key, value):
     self.state[key] = value

デコレータがかかっているのは、上と同じ、そしてこれはself.state変数を書き換えるための関数で、書き換えるキーとその値を引数として取ります。

 

adjustmentValueChanged、adjustmentValueChangedは、前に解説したものと同じなので省略します。

reset、scaleメソッドは上で述べたように前のサンプルプログラムでは、実際に画面の書き換えを担当しましたが、今回のサンプルプログラムでは putStateメソッドを呼び出し、書き換えを指示するために self.state 変数を書き換えるだけとなっています。scaleメソッドは拡大・縮小比の値をself.state変数に書き込むだけです。

def reset(self):
     """ Restore the original ImageProcessor """
     self.putState("restore", True)

def scale(self):
     self.putState("requested_scaling_factor", self.slider.getValue() / 100.0)

 

 

そして runメソッドです。これはRunnableインターフェイスに対応する動作部分です。ここで規定される動作は、呼び出されるたびにそれぞれ別のスレッドとして作動するはずです。

def run(self):
""" Execute the in-place scaling of the ImagePlus,
here playing the role of a costly operation. """
     if self.getState("restore"):
          print "Restoring original"
          ip = self.original_ip
          self.putState("restore", False)
     else:
          requested, last = self.getState("requested_scaling_factor", "last_scaling_factor")
          if requested == last:
               return # nothing to do
          print "Scaling to", requested
          new_width = int(self.original_ip.getWidth() * requested)
          ip = self.original_ip.resize(new_width)
          self.putState("last_scaling_factor", requested)

          # Request updating the ImageProcessor in the event dispatch thread,
          # given that the "setProcessor" method call will trigger
          # a change in the dimensions of the image window
          SwingUtilities.invokeAndWait(lambda: self.imp.setProcessor(ip))

     # Terminate recurrent execution if so requested
     if self.getState("shutdown"):
          self.scheduled_executor.shutdown()

 

まず run メソッドが呼び出されると、getStateメソッドに "restore" キーを代入して状態を見ます。True だったらプレビュー画像を原状復帰し、そのあとself.stete変数の restoreキーを Falseに戻します。

restoreキーがFalseであれば、self.state変数の"requested_scaling_factor", "last_scaling_factor"の二つのキーの値を読み出し、この二つのキーの値が等しければ何も行いません。

異なっていれば、"requested_scaling_factor"の値をオリジナル画像の幅に掛けて、拡大・縮小を行った画像(image processor)を作成します。そして、self.state変数の"last_scaling_factor"の値を、先ほどの"requested_scaling_factor"の値に書き換えます。

 

そして、作成した拡大・縮小画像を今のプレビュー画像(imagePlus)に適用しますが、SwingUtilotiesの invokeAndWait メソッドを使ってプレビュー画像の更新を行います。このメソッドはAWTコンポーネント (ユーザインターフェース用の様々な部品)が引き起こすイベントがすべて終了したら、引数にある実行可能な命令を実行するというメソッドのようです。つまり、ダイアログ上で操作をしていないときにプレビュー画面の更新を行うようです。この時引数にラムダ式を使っています。オリジナルの解説によるとJython上で、invokeAndWaitを 使う場合はラムダ式を経由して実行したい命令を引数にしないといけないようです。

 

最後に、self.stateのshutdownの値がTrueであったときは、 self.scheduled_executor を停止します。

 

scalingPreviewerの最後のメソッド destroyは、scalingPreviewerを停止させるために putStateメソッドを呼び出し、self.state変数の"shutdown"キーを True に書き換えます。実際のシャットダウン過程はrunメソッドの if self.getState("shutdown") 以下のコードが担います。

def destroy(self):
     self.putState("shutdown", True)

 

 当座は、書きたいプログラムの中にはバックグラウンドスレッドまで使わないと書けないようなものはないので、当面、お勉強しました、ということで終わりそうです。


 

 

*1:次の文書も参照。

jython.readthedocs.io

*2:例えば以下を参照。

qiita.com