2010年1月25日

Threadの割り込みハンドラが失敗することがある(その3)

この問題は先日のアップデートでパッチがあたりました。

最新のtrunkのThreadではこの問題は起きないようです。


前回の続きです。。

前回のパッチをSparkProjectにコミットしたところ、nobuさんから割り込み後もイベントを受け取ってしまうという問題を指摘してもらえました。この事についてちょっと考えてみます。


おそらくThreadの仕様的には割り込み後の振る舞いというのはThreadのサブクラスにまかされていて、スーパークラスであるThreadでは特に定義されていないんだと思います。ところがこれまで見てきたように、割り込みとイベント配信が続けて行われた場合、サブクラスでは、タイミングによっては自分が割り込まれたかどうか知る事ができないという状況が発生します。


前回のパッチではこれをふまえ、Threadの仕様を「イベント待ちの時に割り込まれたThreadはイベントを処理しない(イベントハンドラを実行しない)」ように変更しています。ただ、2つあるテストのうち、後者のテストである[割り込み]〜[イベント配信]では割り込み後も1度イベントを受け取ってしまっていました。これは割り込み後実際に割り込みハンドラが割り当てられるまでに1サイクルのインターバルがあるためその隙に飛び込んできたイベントを受け取ってしまっていたためです。

まず、テストの、割り込まれた後のイベントチェック部分がおかしかったので修正します。

//300mSec後にテスト

setTimeout(

    async(

        function():void

        {

            //InterruptHandlerTestThreadの割り込みハンドラが実行されたかどうかテスト

            //assertEquals("interrupt event interrutHandler finalize ", Static.log);

            assertEquals("interrupt interrutHandler finalize ", Static.log);


テストを実行します。
予想通り割り込まれた後もイベントを受け取ってしまうためテストに失敗します。


..E
Time: 0.66
There was 1 failure:
1) inturruptHandlerExecution2(org.libspark.thread::InterruptHandlerTest)
Error: expected:<interrupt []interrutHandler fina...> but was:<interrupt [event ]interrutHandler fina...>


そこで、Threadのコードにもう一つだけ処理を加えこの問題を回避する事にします。
ここではイベントを受け取ったとき割り込みが予約されていれば何もしないようにしています。

private function eventHandler(e:Event, handler:EventHandler):void

{

    //割り込みが予約されていれば何もしない

    var me:Thread = this;

    if(

        _reserveInterrupt.some(

            function(thread : Thread, ...param):Boolean

            {

                return thread == me;

            }

        )

    ) return;

                

    //実行関数が割り込みハンドラならイベントハンドラをリセットして何もしない

 

テストを実行すると無事にパスします。


CYCLE 0--------------
  @ internalExecute (RUNNABLE
    # run() (RUNNABLE
  @ register event  (RUNNABLE

CYCLE 1--------------
  @ internalExecute (WAITING

〜〜 省略 〜〜

CYCLE 8--------------
  @ internalExecute (WAITING

 [DISPATCH]

  @ eventHandler  (WAITING
  @ internalExecute (RUNNABLE
    # received event (RUNNABLE

!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!

  @ interrupt  (RUNNABLE

CYCLE 9--------------
  @ internalExecute (RUNNABLE
    # run() (RUNNABLE
  @ register event  (RUNNABLE
  @ internalExecute (WAITING

 [DISPATCH]

  @ eventHandler  (RUNNABLE

CYCLE 10--------------
  @ internalExecute (RUNNABLE
    # INTERRUPTED


CYCLE 11--------------
  @ internalExecute (TERMINATING
    # finalize() (TERMINATING
    
    

CYCLE 0--------------
  @ internalExecute (RUNNABLE
    # run() (RUNNABLE
  @ register event  (RUNNABLE

CYCLE 1--------------
  @ internalExecute (WAITING

〜〜 省略 〜〜

CYCLE 6--------------
  @ internalExecute (WAITING

!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!

  @ interrupt  (WAITING

 [DISPATCH]

  @ eventHandler  (WAITING

CYCLE 7--------------
  @ internalExecute (WAITING
  @ internalExecute (WAITING

 [DISPATCH]

  @ eventHandler  (RUNNABLE

CYCLE 8--------------
  @ internalExecute (RUNNABLE
    # INTERRUPTED


CYCLE 9--------------
  @ internalExecute (TERMINATING
    # finalize() (TERMINATING
..
Time: 0.654

OK (2 tests)

 

シーケンス図です。

fixed


今回の修正をもう一度SparkProjectにコミットしました。

2010年1月20日

Threadの割り込みハンドラが失敗することがある(その2)

この問題は先日のアップデートでパッチがあたりました。

最新のtrunkのThreadではこの問題は起きないようです。


前回の続きです。

問題をもう一度整理してみます。
問題点は2つあります。

1・ ステートがWAITINGの状態でイベントハンドラが実行され即座にRUNNABLEになる。
このステートが再びWAITINGになるのは次のサイクル。
つまりサイクルとサイクルの間でステータスがRUNNABLEになってしまう事があり、このとき割り込まれると割り込みハンドラは失敗する。

2・ WAITINGの状態で割り込まれると割り込みハンドラは次の実行関数として正しく設定される。
しかしこの実行関数が次のサイクルで実行される前にイベントを受け取ってしまうとイベントハンドラ内に書かれた処理内容によっては実行関数を上書きしてしまう。


まず2から取りかかります。


割り込みハンドラとイベントハンドラは排他的な関係のように思えます。
なぜならイベント待ちをしているThreadに割り込みをかけるという事は、もうイベントを受け取りたくないという事だからです。
これをヒントに次のような方針でパッチを当てます。

「イベントハンドラがイベントを受け取ったとき、実行関数が割り込みハンドラであれば(このThreadは割り込まれたので)、そのイベントは無視する。」

以下がイベントハンドラの部分のパッチコードです。(1026行目)

private function eventHandler(e:Event, handler:EventHandler):void

{

    //実行関数が割り込みハンドラならイベントハンドラをリセットして何もしない

    if (_interruptedHandler != null && _runHandler == _interruptedHandler) 

    {

        // イベントハンドラをリセット

        resetEventHandlers();

        return;

    }

    // 既にイベントが起こっていれば何もしない

 

つぎに1のほうですが以下のような方針でパッチを当てる事にします。

ステータスがRUNNABLEになる瞬間を避けて、WAITINGの時に確実に割り込みの内部処理を実行する

割り込み処理はinterrup()メソッド内に書かれています。interrupt()が呼ばれた時にすぐにこの処理を実行してしまわないで、ステータスがWAITINGの時にこの処理を行いたい。ではステータスが確実にWAITINGなのはどんなタイミングでしょうか?サイクルが終わった直後なら確実にWAITINGのはずです。サイクルが終わった直後に、イベントに先を越されないようすぐに割り込み処理を実行するようにします。


割り込み処理の遅延をするために、キュー用の変数を用意します。(138行目)

public class Thread extends Monitor

{

    private static var _executor:IThreadExecutor;

    private static var _threadIndex:uint = 0;

    private static var _currentThread:Thread = null;

    private static var _toplevelThreads:Array = [];

    private static var _uncaughtErrorHandler:Function = null;

    private static var _defaultErrorHandlers:Dictionary = null;

    private static var _reserveInterrupt : Array = [];


 

interrupt()が呼び出された時、呼び出されたThreadインスタンスをこのキューに格納します。
また実際の割り込み処理の部分を、新しいメソッドinternalInterrupt()を作って丸ごと移動します。
以下がinterrupt()部分のパッチコードです。(791行目)

public function interrupt():void

{

    //割り込みを予約し、次のサイクルの最後に実行する

    _reserveInterrupt.push(this);

}


internal function internalInterrupt():void

{

    if (_state == ThreadState.WAITING || _state == ThreadState.TIMED_WAITING) {

 

最後に、サイクルの最後に割り込み処理を実行するように、executeAllThreads()メソッドの最後に以下のコードを追加します。(350行目)

public static function executeAllThreads():void

{

    // 全てのトップレベルスレッドを呼び出す

    

〜〜 省略 〜〜

    

    //予約された割り込みハンドラがあれば実行する

    _reserveInterrupt.forEach(

        function(thread:Thread, ...param):void

        {

            thread.internalInterrupt();

        }

    );

    _reserveInterrupt = [];

}

 

テストを実行すると無事にテストをパスします

CYCLE 0--------------
  @ internalExecute (RUNNABLE
    # run() (RUNNABLE
  @ register event  (RUNNABLE

〜〜 省略 〜〜

CYCLE 8--------------
  @ internalExecute (WAITING

 [DISPATCH]

  @ eventHandler  (WAITING
  @ internalExecute (RUNNABLE
    # received event (RUNNABLE

!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!

  @ interrupt  (RUNNABLE

CYCLE 9--------------
  @ internalExecute (RUNNABLE
    # run() (RUNNABLE
  @ register event  (RUNNABLE

 [DISPATCH]

  @ eventHandler  (RUNNABLE

CYCLE 10--------------
  @ internalExecute (RUNNABLE
    # INTERRUPTED


CYCLE 11--------------
  @ internalExecute (TERMINATING
    # finalize() (TERMINATING




CYCLE 0--------------
  @ internalExecute (RUNNABLE
    # run() (RUNNABLE
  @ register event  (RUNNABLE

〜〜 省略 〜〜

CYCLE 6--------------
  @ internalExecute (WAITING

!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!

  @ interrupt  (WAITING

 [DISPATCH]

  @ eventHandler  (WAITING
  @ internalExecute (RUNNABLE
    # received event (RUNNABLE

CYCLE 7-------------- (※このサイクルで割り込みハンドラが割り当てられる)
  @ internalExecute (RUNNABLE
    # run() (RUNNABLE
  @ register event  (RUNNABLE

 [DISPATCH]

  @ eventHandler  (RUNNABLE

CYCLE 8--------------
  @ internalExecute (RUNNABLE
    # INTERRUPTED


CYCLE 9--------------
  @ internalExecute (TERMINATING
    # finalize() (TERMINATING
..
Time: 0.659

OK (2 tests)



シーケンス図です

fixed the problem

今回のテストと合わせて既存のすべてのテストをパスしたので、このパッチを当てる事で大きな問題は多分起きないんじゃないかと思いますが、何か問題が起きるようであれば教えてください。オフィシャルなパッチじゃないのでクレームはSparkProjectではなくimajuk@mac.comまで

追記:sparkProjectのThreadのbranchesにcommitしました。http://www.libspark.org/svn/as3/Thread/branches/imajuk

Threadの割り込みハンドラが失敗することがある(その1)

[追記]この問題は先日のアップデートでパッチがあたりました。

最新のtrunkのThreadではこの問題は起きないようです。


Threadを使っているとある問題に気づく事があります。
それは「イベントをリスンするThreadでinterrupt()を呼び出した際に割り込みハンドラが実行されないことがある」問題です。
event()を実行してイベントのリスンを開始したThreadは待機状態となり、その状態でinterrupt()を実行すると割り当てられた割り込みハンドラが実行されます。が、場合によっては期待する割り込みハンドラが実行されない事があるのです。

上記の問題は、サイクルとサイクルの狭間、つまり一つのThreadの実行関数が終わり次のThreadの実行関数が始まるまでの間に、イベントハンドラとinterrupt()の両方が実行された時におこります。

(※ここでいうサイクルとはThread.initialize()で渡すIThreadExecutorの実装によって定義されるThreadの実行の単位のことです。例えばEnterFrameThreadExecutorはユーザが定義したフレームレート、30fpsなら1/30秒に一回Threadが実行され、IntervalThreadExecutorならIntervalThreadExecutorのコンストラクタに渡したインターバルで一回Threadが実行されます。)

この問題に気づく典型的な例は、マウスクリックに反応するようにイベントをリスンしているThreadをinterrupt()するケースやステージのリサイズを監視するThreadをinterrupt()するケースなど、短い間隔でイベントをキャッチするThreadに割り込むケースです。
(参考:Threadの割り込みがうまくいかないことがある

このエントリはここから普段ユーザが意識する必要のないThread内部の振る舞いを見ていきます。
もしこの問題の解決方法だけを知りたい場合は「Threadの割り込みハンドラが失敗することがある(その2)」を見てみてください。

問題が再現するテストを書く

さて、この問題が再現するテストを書いてみて何がおきているかを調べてみます。
以下がそのテストコードです。ThreadライブラリのリポジトリにあるtestディレクトリにTestSuiteが用意されているので、ここに今回のテストInterruptHandlerTest.asを追加しました。

package org.libspark.thread

{

    import flash.utils.setTimeout;

    import flash.display.Sprite;


    import org.libspark.as3unit.after;

    import org.libspark.as3unit.assert.*;

    import org.libspark.as3unit.before;

    import org.libspark.as3unit.test;

    

    use namespace test;

    use namespace before;

    use namespace after;


    /**

     * 割り込みハンドラのテスト

     *   イベントをリスンするThreadインスタンスがあり、

     *   Threadの実行タイミング間でイベントの配信と割り込みが両方行われた時に

     *   割り込みハンドラが正しく実行されるかテスト

     */

    internal class InterruptHandlerTest extends Sprite

    {

        private var dispatcher : Dispatcher;

        private var thread : InterruptHandlerTestThread;


        before function setup() : void

        {

            Thread.initialize(new EnterFrameThreadExecutor());

            Static.log = "";

            

            //イベント配信オブジェクト

            dispatcher = new Dispatcher();

            

            //テスト用Thread

            thread

                new InterruptHandlerTestThread(dispatcher);

        }


        after function teardown() : void

        {

            Thread.initialize(null);

            Static.log = "";

        }


        //イベント配信直後に割り込みするケースのテスト

        test function inturruptHandlerExecution() : void

        {

            //200mSec後に [イベント配信] -> [割り込み]

            setTimeout(

                function():void

                {

                    //Threadに向けてイベントを配信

                    dispatcher.dispatch();

                    

                    //Threadinterrupt()を呼び出し

                    trace("\n!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!\n");

                    thread.interrupt();

                    

                }, 200

            );


            //250mSec後にもう一度イベント配信.このイベントはキャッチしないはず

            setTimeout(

                function():void

                {

                    dispatcher.dispatch();

                },

                250

            );

            

            //300mSec後にテスト

            setTimeout(

                async(

                    function():void

                    {

                        //InterruptHandlerTestThreadの割り込みハンドラが実行されたかどうかテスト

                        assertEquals("event interrupt interrutHandler finalize ", Static.log);

                        //割り込み後のステータスをテスト

                        assertEquals(ThreadState.TERMINATED, thread.state);

                    }

                ), 

                300

            );

                

            //テスト用Thread開始

            thread.start();

        }

    }

}


import org.libspark.thread.Thread;

import flash.events.Event;

import flash.events.EventDispatcher;


//テスト用Thread

class InterruptHandlerTestThread extends Thread 

{

    private var dispatcher : Dispatcher;

    

    public function InterruptHandlerTestThread(dispatcher:Dispatcher)

    {

        super();

        this.dispatcher = dispatcher;

    }

    

    override public function interrupt():void

    {

        Static.log += "interrupt ";

        super.interrupt();

    }

    

    override protected function run() : void

    {

        trace("    # run()", printStatus(state));

        

         //割り込みハンドラを生成

        var interruptHandler : Function

            function():void

            {

                trace("    # INTERRUPTED\n");

                Static.log += "interrutHandler ";

            };

        

        //割り込みハンドラを設定

        interrupted(interruptHandler);

        

        //イベントをキャッチしたら再びイベントをリスンして待機状態へ

        event(

            dispatcher,

            InternalEvent.COMPLETE,

            function():void

            {

                trace("    # received event", printStatus(state));

                Static.log += "event ";

                //イベントを受け取ったら再びイベントをリスン

                next(run); 

            }

        );

    }


    override protected function finalize() : void

    {

        Static.log += "finalize ";

        trace("    # finalize()", printStatus(state));

    }

    

}


//イベント配信オブジェクト

class Dispatcher extends EventDispatcher

{

    public function dispatch():void

    {

        trace("\n [DISPATCH]\n");

        dispatchEvent(new InternalEvent(InternalEvent.COMPLETE));

    }

}


//テスト用イベント

class InternalEvent extends Event

{

    public static const COMPLETE : String = "COMPLETE";

    

    public function InternalEvent(type : String)     

    {

        super(type, false, false);

    }

}


class Static

{

    public static var log:String;

}


さらに、Thread内の動きを観察するために、org.libspark.thread.Threadとorg.libspark.thread.EnterFrameThreadExecutorにtrace()を仕込みます。

Threadにデバッグ用メソッドを追加

//Threadのステータスコードを文字列に変換するユーティリティ

public static function printStatus(s : int) : String

{

    switch(s)

    {

        case ThreadState.NEW:

            return "(NEW";

        case ThreadState.RUNNABLE:

            return "(RUNNABLE";

        case ThreadState.TERMINATED:

            return "(TERMINATED";

        case ThreadState.TERMINATING:

            return "(TERMINATING";

        case ThreadState.TIMED_WAITING:

            return "(TIMED_WAITING";

        case ThreadState.WAITING:

            return "(WAITING";

    }


    throw new Error(s);

}

Threadの以下のメソッドにtrace()を追加
event()メソッド

public static function event(dispatcher:IEventDispatcher, type:String, func:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void

        {

            trace("  @ register event ", printStatus(getCurrentThread().state));

            getCurrentThread().addEventHandler(dispatcher, type, func, useCapture, priority, useWeakReference);

eventHandler()メソッド

private function eventHandler(e:Event, handler:EventHandler):void

        {

            trace("  @ eventHandler ", printStatus(_state));

            //実行関数が割り込みハンドラならイベントハンドラをリセットして何もしない

interrupt()メソッド

public function interrupt():void

        {

            trace("  @ interrupt ", printStatus(_state));

            //割り込みを予約し、次のサイクルの最後に実行する

internalExecute()メソッド

private function internalExecute(error:Object, errorThread:Thread):Boolean

        {

            trace("  @ internalExecute " + [printStatus(_state)]);

            if (_state == ThreadState.WAITING || _state == ThreadState.TIMED_WAITING) {

EnterFrameThreadExecutorのenterFrameHandlerにtrace()を仕込みます。(カウンタ用の変数を一つ追加しています)

private var time : int = 0;

private function enterFrameHandler(e:Event):void

{

trace("\nCYCLE " + time++ + "--------------");

Thread.executeAllThreads();

}

これで準備完了。
このテストでは、テスト開始100mSec後にイベント配信とThreadの割り込みを行い、その50mSec後にもう一度イベント配信、最後に200mSec後に割り込みハンドラが実行されたか確認しています。


テスト

テストしてみます。
割り込みハンドラが実行されずにテストは失敗します。

以下はその出力結果です。

CYCLE 0--------------
  @ internalExecute (RUNNABLE
    # run() (RUNNABLE
  @ register event  (RUNNABLE

〜〜 省略 〜〜

CYCLE 8--------------
  @ internalExecute (WAITING

 [DISPATCH]

  @ eventHandler  (WAITING
  @ internalExecute (RUNNABLE
    # received event (RUNNABLE

!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!

  @ interrupt  (RUNNABLE

CYCLE 9--------------
  @ internalExecute (RUNNABLE
    # run() (RUNNABLE
  @ register event  (RUNNABLE

 [DISPATCH]

  @ eventHandler  (WAITING
  @ internalExecute (RUNNABLE
    # received event (RUNNABLE

CYCLE 10--------------
  @ internalExecute (RUNNABLE
    # run() (RUNNABLE
  @ register event  (RUNNABLE

CYCLE 11--------------
  @ internalExecute (WAITING
  
.E
Time: 0.341
There was 1 failure:
1) inturruptHandlerExecution(org.libspark.thread::InterruptHandlerTest)
Error: expected:<event interrupt [interrutHandler finalize] > but was:<event interrupt [event] >

何が起きたか見ていきましょう。

@マークのある行はThread(スーパークラス)内部でおこった出来事、#マークのある行はThread(テスト用のサブクラス)で起こった出来事です。
マークの後に何が起こったか、そしてその時のThreadのステータスを出力しています。
例えば、

@ internalExecute (RUNNABLE
はThread.internalExecuteが実行され、その時のステータスはRUNNABLEだったという感じです。
(※このテストコードの出力結果は実行時のフレームレートやCPUの速度などに影響します。もしあなたがこのテストを実行した場合、テストの成否は同じはずですが出力結果は異なる可能性があります。今回は31FPSでこのテストを実行しました。)

まず、CYCLE0を見てみてください。

CYCLE 0--------------
  @ internalExecute (RUNNABLE
    # run (RUNNABLE
  @ register event  (RUNNABLE

run()が実行されイベントハンドラが設定されています。
ここでこのサイクルは終了です。


次に、CYCLE8を見てみます。
このサイクルで割り込みハンドラが実行されない問題が起きています。

CYCLE 8--------------
  @ internalExecute (WAITING
  
[DISPATCH]

  @ eventHandler  (WAITING
  @ internalExecute (RUNNABLE
    # received event  (RUNNABLE

まずイベントが配信されイベントハンドラが実行されるという期待した動作がスムーズに展開していきます...


!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!

  @ interrupt  (RUNNABLE

CYCLE 9--------------
  @ internalExecute (RUNNABLE
    # run() (RUNNABLE
  @ register event  (RUNNABLE

 [DISPATCH]

  @ eventHandler  (WAITING
  @ internalExecute (RUNNABLE
    # received event (RUNNABLE

その後、interrupt()が実行されますが、次のサイクル9で実行されるはずの割り込みハンドラは実行されていません。
また、割り込まれた後イベントのリスンをやめる事が期待されていますが、次のサイクルであるサイクル9でも何事もなかったようにイベントをリスンし続けています。

何が起きたのでしょうか?



もう一度割り込まれた時の出力結果を見てみます。

!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!

  @ interrupt (RUNNABLE

interrupt()が呼び出されたときThreadのステータスはRUNNABLEになっています。
これは、interrupt()される直前にイベントハンドラが実行され、ステータスが変更されたためです。


Threadの仕様によると、割り込みハンドラが実行される条件はイベントハンドラが設定されている事、つまりステータスがWAITINGであることです。しかし割り込み直前でイベントハンドラが実行されたためにステータスがRUNNABLEになってしまいました。イベントハンドラが再度設定されステータスが再びWAITINGになるのは次のサイクルです。次のサイクルが始まる前に割り込みが実行されると、この時点ではまだステータスがRUNNABLEであるため、このエントリで取り上げている問題が起こります。

テキストだけだとわかりにくいので図にしてみました。(前のエントリでも書いてみたシーケンス図です。見方がわからない場合は前のエントリを参照してください)

 

まず、割り込みハンドラが意図する通りに実行されるケースです。

successfuly interruption

つぎに失敗したケースです。

failed interruption

サイクルとサイクルの間でイベントハンドラの実行と割り込みの実行の両方が起きているのがわかります。
イベントのキャッチによりThreadのステータスが一時的にRUNNABLEとなり、そのタイミングで割り込みが実行されると割り込みハンドラは失敗します。

「割り込みハンドラが実行されるのは待機状態に割り込まれた時」とThreadは仕様としているので、これは正しい動作と言えなくもないです。が、ユーザは(このテストのようなケースでは)「イベントを受け取ったら再びイベント待機状態」つまり「割り込まれるまでは常に待機状態」のThreadを意図していると思うのでバグのようにも思えます。
この問題が仕様なのかバグなのか微妙なところです。

運用でカバーできないのか?

ステータスが一時的にRUNNABLEになってしまうのが問題です。しかしステータスがRUNNABLEの時に割り込みが実行されるとすれば割り込みフラグ(isInterrupted)たつはずです。このフラグを利用すれば手動で割り込みハンドラを呼び出す事ができるのではないでしょうか? これを使って運用でカバーできないのでしょうか?

例えば以下のようなコードです。

override protected function run() : void

{

    trace("    # run()", printStatus(state));

    

     //割り込みハンドラを生成

    var interruptHandler : Function

        function():void

        {

            trace("    # INTERRUPTED\n");

            Static.log += "interrutHandler ";

        };

        

    //割り込みフラグがたっていれば割り込みハンドラを実行して終了

    if (isInterrupted)

    {

        interruptHandler();

        return;

    }

        

    //割り込みハンドラを設定

    interrupted(interruptHandler);

        

    //イベントをキャッチしたら再びイベントをリスンして待機状態へ

    event(...


テストコードに上記の処理を追加すればテストは通ります。

  .
  Time: 0.337
  OK (1 test)


うまくいったように思えます。が、まだ2つの問題を抱えています。

1つ目は、待機状態(WAITING)でないのに割り込みハンドラを呼び出すという意味でThreadの仕様を変えてしまっている事です。
2つ目は、実はまだinterrupt()のタイミングによってはテストが失敗するという事です。

後者について詳しく見ていきます。

まず、現段階のテストでは、イベントをキャッチした後割り込みが実行されています。([イベント] →  [割り込み])。しかし、割込みを実行した後にイベントをキャッチするケース([割り込み] → [イベント])をまだテストしていません。

テストコードにテストを追加してみます。

//割り込み直後にイベント配信

test function inturruptHandlerExecution2() : void

{

    //200mSec後に [イベント配信] -> [割り込み]

    setTimeout(

        function():void

        {

            //Threadinterrupt()を呼び出し

            trace("\n!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!\n");

            thread.interrupt();


            //Threadに向けてイベントを配信

            dispatcher.dispatch();

                    

        }, 200

    );


    //250mSec後にもう一度イベント配信.このイベントはキャッチしないはず

    setTimeout(

        function():void

        {

            dispatcher.dispatch();

        },

        250

    );

            

    //300mSec後にテスト

    setTimeout(

        async(

            function():void

            {

                //InterruptHandlerTestThreadの割り込みハンドラが実行されたかどうかテスト

                assertEquals("interrupt event interrutHandler finalize ", Static.log);

                //割り込み後のステータスをテスト

                assertEquals(ThreadState.TERMINATED, thread.state);

                //割り込み後の割り込みフラグをチェック(待機状態での割り込みなのでfalseのはず)

                assertFalse(thread.isInterrupted);

            }

        ), 

        300

    );

                

    //テスト用Thread開始

    thread.start();

}

前は「イベント配信」「割り込み」という順番でしたが、今度のテストは「割り込み」「イベント配信」という順番です。前のテストとはこの呼び出しの順番だけが違います。


このテストを実行してみるとテストは失敗します。

.E.
Time: 0.658
There was 1 failure:
1) inturruptHandlerExecution2(org.libspark.thread::InterruptHandlerTest)
Error: expected:<interrupt event [interrutHandler] > but was:<interrupt event [event] >

これはwonderflでsoundkitchenさんが指摘していることと同じことが起こっています。

図にすると以下のような感じです。

another failure case

このケースでは割り込み後のイベントハンドラの動作で、割り込みフラグと割り込みハンドラが上書きされてしまいます。
こうなるともはやThreadは自分が割り込まれたかどうかを知る手段がありません。運用でカバーするのは無理そうです。
やはりこの問題を解決するにはThreadのソースコードに手を入れる必要があるかもしれません。


次回のエントリでは解決編として、Threadのソースにパッチを当てる方法を書いてみます。


つづく

2009年9月 8日

Threadはええぞ〜

前に一度org.libspark.threadライブラリについてエントリーを書きましたが、このライブラリは本当に素晴らしいです。フレキシブルに使えるし、使っているだけで自然とOOPの作法が身に付いていくような気もします。
なんでこんなに使い心地がいいのかつらつら考えているのですが、いまだうまくエントリーにまとめられそうにありません。が、
とりあえず、今回は「Threadは新しいデザインパターンの実装なのではないか」という大胆な仮説を唱えてみようかと思いますw

まずは 同期処理、非同期処理のおさらい

さて、タスクAのあとタスクBを実行したいという状況があるとします。
以下のようなコードからスタートします。

package  
{
    import flash.display.Sprite;

    public class AboutThread extends Sprite 
    {
        public function AboutThread()
        {
            /**
             * タスクAが終わった後タスクBをすませたい
             */
            doTask();
        }

        //タスク実行
        private function doTask():void
        {
            //タスクAを生成
            var a:A = new A();
            //タスクを実行
            a.execute();

            //タスクBを生成
            var b:B = new B();
            //タスクを実行
            b.execute();
        }
    }
}

とってもシンプルで清々しいコードですね。何の問題もありません。平和な世界。
ただ現実はこのように甘くはないかもしれません。
タスクAは非同期なタスクかもしれないからです。
もし非同期の場合は、タスクの終了を通知してもらう手続きをして、さらに通知を受け取るイベントハンドラも必要になりますね。
あと、一度だけ通知してもらいたい場合(よくある!)は、ハンドラ内で通知解除の手続きも必要です。
コードは以下のような感じです。

package  
{
    import flash.events.Event;
    import flash.display.Sprite;

    public class AboutThread extends Sprite 
    {
        public function AboutThread()
        {
            /**
             * タスクAが終わった後タスクBをすませたい
             */
            doTaskA();
        }

        //タスクAを実行
        private function doTaskA():void
        {
            //タスクAを生成
            var a:A = new A();
            //タスク終了を通知してもらう
            a.addEventListener(Event.COMPLETE, taskACompleteHandler);
            //タスクを実行
            a.execute();
        }

        //タスクAが終了した
        private function taskACompleteHandler(event:Event):void
        {
            //リスナの登録を解除            
            A(event.target).removeEventListener(Event.COMPLETE, taskACompleteHandler);
            //タスクBスタート
            doTaskB();
        }

        //タスクBを実行
        private function doTaskB():void
        {
            //タスクBを生成
            var b:B = new B();
            //タスク終了を通知してもらう
            b.addEventListener(Event.COMPLETE, taskBCompleteHandler);
            //タスクを実行
            b.execute();
        }

        //タスクBが終了した
        private function taskBCompleteHandler(event:Event):void
        {
            //リスナの登録を解除            
            B(event.target).removeEventListener(Event.COMPLETE, taskACompleteHandler);
            
            //...続く
        }
    }
}

突然コード量が増えてしまいました。ざっと倍くらいでしょうか。

何が問題なのか?

単純にコードの量が増えたことを問題にしているわけではありません。
最近の気の利いたエディタを使っていれば半自動でリスナ登録やハンドラの記述をやってくれますもんね。
コードを書くときはそれほど負担にはならないんです。でも、コードを読むときはどうでしょうか?

本来書きたかった処理は、「タスクAのあとにタスクBを実行したい」という物だったはずです。
もちろん残念ながらコンパイラは自然言語で書かれた命令のような抽象的なことを理解することはできないので(死ぬまでにはそんなコンパイラが出てくることを望みますが)、 それなりの手続きを記述していく必要があります。
問題にしているのは手続きの細かさです。
上記のコードは、「タスクAのあとにタスクBを実行したい」ということを記述したいはずのコードなのに、その一段低レベルなイベントシステムの手続きという層がむき出しになっています。
また、これが、「Aのあと、Bのあと、Cのあと、Dのあと、Eしたい」の様なケースではイベントの手続きという低レベルなコードがほとんどを全体のほとんどをしめてしまうでしょう。
まるで、イメージシーンばかりでいつまでたっても本編が始まらないA...

Threadはこれを解決する?

はい。解決します。
このように

package  
{
    import org.libspark.thread.Thread;

    public class AboutThread extends Thread 
    {
        public function AboutThread()
        {
            /**
             * タスクAが終わった後タスクBをすませたい
             */
            doTaskA();
        }

        //タスクAを実行
        private function doTaskA():void
        {
            //タスクAを生成
            var t:Thread = new DoAThread();
            //タスクを実行
            t.start();
            //タスクの終了を待つ
            t.join();
            //終わったらタスクB開始
            next(doTaskB);
        }
        
        //タスクBを実行
        private function doTaskB():void
        {
            //タスクBを生成
            var t:Thread = new DoBThread();
            //タスクを実行
            t.start();
            //タスクの終了を待つ
            t.join();
            //終わったら次の...
            next(doSomething);
        }
    }
}

コード量も減りましたが、何より嬉しいのは上から流れるように処理を記述できる点です。
イベントという仕組みは一見隠蔽され、一段抽象的なレベルで処理を記述できています。直感的とも言えますね。
start()やnext()などとても直感的な記述で、もし初めてThreadを使ったコードを見た人でも何をやっているか想像できるんじゃないでしょうか?
t.join()というちょっと違和感を感じるかもしれない記述がありますが、これは「tの終了を待つ」と読み替えてもらうといいです。


ちょっと脱線
個人的にはjoin()という名前に違和感を感じる。startやnextと意味の階層が違う気がしてて、うまく言えないけど利用者視点じゃなく設計者視点な名前という感じで。
これがもし、waitForFinishing()とか(ださいけど)だと意味がつながる気がする。さらに、join()とnext()をまとめたシンタックスを用意して、 さらにさらに流れるようなインターフェイスで、t.start().andThen(func)とか書けると最高だー。


非同期処理のコード大盛り問題を解決するだけの物ではない

本題です。
最近、自分はコントローラはすべてThreadで書いています。同期、非同期関係なくです。
たとえ同期なタスクでもjoin()してしまいます。そうすると何が嬉しいのかと言うと、 もし、コードを書き始めた当初同期だったあるタスクがあとから非同期になっても、クライアントコードはいっさい変更する必要がありません。
いいかえれば、クライアントはそのタスクが同期か非同期かを意識する必要なく処理を記述できるんです。
「AしてBしてCしてDして...」
それらのタスクが同期なタスクか非同期なタスクかいっさい気にかけることなく流れるように処理を記述でき、後から読むときも流れるようにコードを読めます。

エントリーの最初にあげた大胆な仮説、「Threadは新しいデザインパターンの実装なのではないか」というのはこれです。
「同期か非同期かを意識する必要なく処理を記述する」パターン。

ThreadはCommandパターンとCompositeパターンの応用だとは思うんですが、そのどちらにもない同期、非同期性の隠蔽という機能があるんですよね。
少なくともGoFの23のパターンにはないパターンだという気が。。
こういうパターンに既に名前がついているかどうか知っている人がいたら教えてください〜。

と書いては見たものの

うーん、新しいパターンはちょっと言い過ぎたかも。やっぱりw
ほとんどのCommand系のライブラリは非同期なタスクをサポートしてますもんね。
でも、いろいろあるCommand系のライブラリの中でも、クライアントコードの記述の簡潔さは一歩抜きん出ていると思います。

というわけで、次期バージョン期待してます!

2009年4月11日

最適化してます?

最適化(と言ってもいろいろあるけど)って、ひたすらコードに制限を加えていくことになりますよね。ドラスティックに効果がある最適化は特に。

融通の利かないコードと引き換えに速度を得られるんですね。わかります。

コードの資産化をいつも心がけている自分としては、いままで最適化にはあまり興味がなかったんですけど、城戸さんの公演のビデオを見て少し考え方が変わりました。
曰く、「浮いた分のパフォーマンスを別の表現にまわせる」
たしかに。

というわけで、今回は最適化のプラクティスです。
テーマは魚っぽいノードの群れ。




ノード数1000



ノード数3000

僕の環境では、ノード数1000だと60/60fpsキープできるのですが、
ノード数を3000まで増やすとで32/60fpsどまりでした。
にわかオプティマイザーだとこんなもんですかね。くやしい。
まだまだ早くできる余地がある気がします。

施した最適化は以下のとおり
・オブジェクト複製による残像効果は使わない。
・ノードはaddChild()しない
・ループ内でオンデマンドでやる必要のない計算は全て前もってやっておく
・ループ内で極力newしない
・全てビットマップ化
・flockポイ動きだけどいんちき
・その他、ちまたにあふれている重箱の隅をつつくような最適化Tipsいろいろ

CS4のVector使えばもうちょっと早くなるのかな。

2009年3月23日

絵で見るThread

Flasher各方面で話題のorg.libspark.thread.Threadだけど、最近ようやく使ってみた。
評判がいいのもうなづける素晴らしいライブラリだと思う。
もっと早く使ってみればよかった。。

ドキュメントがとてもよく出来ているので、使い方についてほとんど付け加えることはないんだけど、文章とコードだけだと、処理の実行単位とか実行順がわかりにくいかもしれないと思い(自分がそうだった)シーケンス図でまとめてみた。

ここまちがってるんじゃないの?みたいなつっこみ、絶賛募集してます。


シーケンス図の見方、超投げやりな説明




シーケンス図は、プログラムの実行時におけるインスタンスの処理の流れや、インスタンス間のコラボレーションの様子を表現できる。
インスタンスとインスタンスの寿命はこのように表現する。






時間は上から下へ流れる。
この図では、
1・ENTER_FRAMEイベントが起きた
2・誰かがnew A()を呼び出した
3・インスタンスAが生成された
事を表している。


Threadの実行単位は「実行関数」



Threadの実行単位は「実行関数」。
1フレーム※1に一度、Threadが持っている実行関数が1つだけ実行される。
2つは実行されない。※2
これ大事。
※1(1フレームと書いたが、フレームベースではなくintervalベースにすることもできるみたい)
※2(例外あり。event()参照)

runとfinalizeはデフォルトで用意されている実行関数。
runは必ず最初に呼び出される実行関数。
finalizeはThreadが終了する前に必ず呼び出される実行関数。






実行関数の中でnext()を呼ぶと、次の実行関数を予約できる。
予約された実行関数は、1フレーム1実行関数の原則に従い、次のフレームで実行される






実行関数の中でevent()を呼ぶと、任意のイベントが発生するまで待機状態になる。
イベントが発生すると予約した実行関数を実行する。
その後特に実行関数がなければ、finalize()が実行される
※(1フレーム1実行関数が基本だが、この場合は例外的に2つの実行関数(fとfinalize)が1フレームで実行される)






実行関数内でsleep()を呼ぶと、指定された時間のあいだ待機状態になる。
時間が過ぎると次の実行関数が実行される。


実行関数の定義まとめ


Threadの実行単位は実行関数なので、なにが実行関数なのかを意識した方が良い。
実行関数は、

  • run()

  • next()に渡された関数

  • event()に渡された関数

  • finalize()


上記に含まれないThread内の関数は実行関数ではないので注意。


Threadの親子関係



実行関数の中で新しいThreadを生成すると、Threadの親子関係が出来る。
AはBの親、BはCの親。
親子関係のあるスレッドは、それぞれ独立した時間軸を持ち実行関数を実行していく






子Threadの実行関数は必ず親Threadの実行関数より先に実行される。




Thread.graffle-19.jpg

子Threadがjoin()された時は、親Threadは子Threadのすべての実行関数が終了するまで次の実行関数の実行を待機する(待機状態になる)。
つまり、各Threadの独立した時間軸の一本化


Monitor



実行関数内で、Monitor::wait()を呼び出すと、そのThreadはMonitor::notify()が呼び出されるまで待機状態になる。
nonify()が呼び出された場合は即座に待機状態が解除され、notify()された同じフレームで実行関数が実行される。


待機状態まとめ


待機状態は、「時間指定あり待機状態」と「時間指定なし待機状態」に分けられる。

時間指定ありなし
・join(timeout time)したとき
・sleep()したとき
・join()したとき(引数なし)
・event()したとき
・Monitor.wait()したとき


割り込み



interrupt()を呼び出すと、割り込みフラグが立つ。
割り込み処理はこのフラグを使って実装する。
ちなみにinterrupt()は実行関数ではないので注意。






待機状態のときに割り込まれると、指定した割り込みハンドラが実行される。
割り込みハンドラは、Thread.interrupted ( )をつかって各実行関数ごとに記述する。
待機状態で割り込まれ、割り込みハンドラがない場合はエラーとなる。





たとえ親子関係にあっても、割り込みは子Threadに伝播しない
もし伝播させたい場合はorg.libspark.thread.utils.ParallelExecutorまたはorg.libspark.thread.utils.SerialExecutorを使用する。


2009年3月15日

ConvolutionFilterで遊ぶ その4

ConvolutionFilterを使ったアニメーションの可能性

前回からの続きです。前回までを見てないと今回の話はわからないと思うので、もしまだであれば以下のリンクからど〜ぞ。


ここに黒背景に1pxの画像があります。

img1.jpg

この画像に以下のような行列でConvolutionFilterを適用すると...

k1.jpg

以下のようになります。前回までで見てきたように、ターゲットピクセルの50%の強さで、斜め4方向にピクセルを移動コピーしています。

img2.jpg



さて、このフィルタの適用結果に、さらにフィルタをかけ続けるといったいどうなるのか?とても気になったので、ツールに連続してフィルタを適用する機能を追加しました。

tool.jpg
三角形の再生ボタンを押すと、フィルタを連続して適用していきます。
スライダーを右に動かすと、フィルタをかけるインターバルが長くなっていきます。(注:上の画像はスナップショットなので、機能しませんよ〜)


再生ボタンを押すと、ドットで出来た円が拡大し、画面端で折り返した後、画像全体が白に収束します。
(下の画像どれかをクリックすると、アニメーションが再生されます。)

anim1.jpg
1・ドットで出来た円が拡大していく...
anim2.jpg
2・画面端で折り返す...
anim3.jpg
3・最後は白に収束する

円が拡大していくとこまではなんとなく予想してましたが、その後が想像を超えてました


では、シンプルなグレースケール画像ではなく、フルカラーの画像で同じ事をやると何が起こるのでしょうか?
下の画像に、同じConvolutionFilterを連続して適用してみると...

(下の画像をクリックすると、アニメーションが再生されます。)
anim0.jpg
anim4.jpg

結果は、一瞬で白に収束してしまいます。。
まぁ、さっきのような黒背景に1ピクセルの白があるだけの画像ではなく、
明るさを持ったピクセルが無数にあるフルカラー画像では当たり前の結果ですよね。
ということは、行列の値がまずかったと。


そこで、プリセットにあるガウスぼかしを連続で適用してみました。

(下の画像をクリックすると、アニメーションが再生されます。)
anim5.jpg

なんとなく味わいのあるフェードアウトで、最終的には黒に収束します。
ところで、ガウスぼかしの行列は以下のとおりで、全ての数値を足し合わせると「1」になります。
(0.33*4 + 0.11*4 + 0.44 = 1)
k2.jpg


いろいろやってわかったのですが、ConvolutionFilterを連続して適用する際、
行列の数値を足し合わせて1以下の場合、画像は黒(0)に収束する傾向があり、1より大きい場合は白(255)に収束していくようです。
(あとでみますが、どちらにも収束せずに発散することもあります。)

たとえば、ターゲットセルに対応する値をほんの少しだけ大きくしてみます。
k3.jpg

このように、列の数値を足し合わせた数を1より大きくすると、画像は白に収束していきます。RGBの各チャンネルが255方向へ収束するのでとてもきれいです。
anim6.jpg
anim7.jpg


さきほど、黒にも白にも収束しない場合があると言いましたが、
たとえば、以下のような行列の場合です。
k5.jpg

この行列で、ConvolutionFilterを適用し続けると、黒にも白にも収束せずに不思議なモアレ模様を描き続けます。
anim8.jpg

これがそのまま、何かの表現に使えるとは思いませんが、
このライフゲームのような不思議なアニメーションは面白いです。

ターゲットピクセルの値を少し変えてみると、

k6.jpg

黒に収束してしまいましたが、よりライフゲーム感が出てきました。シマシマの中を何かが泳いでいます(笑。
anim9.jpg



他にも面白いものがいくつか発見できたので、ならべておきます。

(画像をクリックすると、アニメーションが再生されます。)

ガウスぼかしのフェードアウトに似ていますが、終わりがけに一部カラーハーフトーンのような効果が見られます。
anim10.jpg


色面の色彩が荒くなっていき、最終的に点描のような状態で収束します。
anim11.jpg


画像が流れ落ちていく。。
anim12.jpg


コメントしにくいけど綺麗。。
anim14.jpg


これもコメントしにくい。ななめ。。
anim15.jpg

行列作成のこつ

行列の数値を色々変えながら遊んでいましたが、 コントロールしやすいのはピクセルの移動方向ぐらいで、 ほとんどの場合予想がつかない変化となります。。 それでも、いくつかコツと言えるようなものがあったので、あげてみます。
  1. 最終結果を暗くする、明るくする
    上でも書きましたが、行列の足し合わせた数が1を超えると白(255方向)へ収束し、1以下だと黒(0方向)へ収束します。ただしどちらにも収束しない場合もあります。
  2. 方向を変える
    例えば、以下のような行列は上側の数値よりも下側の数値の方が大きいです。このように下に重みをつけた場合、ピクセルは上に移動します。
    k8.jpg
    anim22.jpg

    下に移動させたい場合は、上下反転させて、上に重みをつければればOK。
    k9.jpg
    anim23.jpg

    同じ理屈で、45度回転させれば、斜め右上に重みがつくので斜め移動、さらに45度回転させれば、右の重みがつくので横移動になります。
    k10.jpg
    anim21.jpg


    k11.jpg
    anim20.jpg

    また、 縦横方向の重みは収束結果にピクセルが残りやすく、斜め方向の重みは収束結果にピクセルが残りにくいみたいです。
  3. 面白い行列を発見出来たら細かく微調整
    行列の合計が1以下の黒への収束と、1より大きい白への収束の境界付近が、収束するまでに長く時間がかかるので面白い効果が出来やすい気がします。 面白い行列を発見出来たら、ターゲットピクセルの数値を増減させてこの境界を探してみましょう。 0.01増減させただけで、見た目が大きく変わることもあります。

ConvolutionFilterを使ったアニメーションの利用の可能性

さて、ConvolutionFilterを使ったアニメーションの可能性を探ってみたわけですが、 「で、これなんかに使えるの?」という疑問が聞こえてくる気がします。

さすがに、クライアントワークなどにそのまま使えるとは思いません。ただ、表現の種(素材)として考えたときには一定のポテンシャルを感じます。

たとえば、最初の1pxを拡張していくグレースケールのアニメーションなんかは、
アルファチャンネルマスクとして使えば、スライドショーのトランジションとかに利用できるんじゃないかと思います。(綺麗かどうかはやってみてないのでわからんですが)
もしかしたら、白方向、黒方向へ収束するものは、そのままトランジションとして利用できるものもあるかもしれません。

他には、オリジナルの元画像を残しつつ、BlendMode.SREEN等で合成しながらアニメーションさせてみるとか、アニメーション中に別の画像を重ねて見るとか。。

個人的には、置き換えマップ(DisplacementMapFilter)のソースとして、ConvolutionFilterを使ったアニメーションを利用するとどんな事が可能になるか探ってみたいとこだったりします。

また何か発見があったらレポートします。
とりあえずこのシリーズは今回で終了です。

2009年2月24日

ConvolutionFilterで遊ぶ その3

ピクセル変換の過程を追いかけてみる

前回のエントリーで紹介したツールを使って、ConvolutionFilterの詳細を追いかけてみたいと思います。<<ここ>>にアクセスして、テスト用画像を開いて下さい。

黒い背景の中央に1ピクセルの白いピクセルが表示されていると思います。
この画像を使ってConvolutionFilterによるピクセル変換を見ていきたいと思います。
(Convolutionでどのような計算が行われているかはConvolutionFlterで遊ぶ その1を参照)

さて、いま、行列は以下のようになっています。
0 0 0
0 1 0
0 0 0

この行列を使ってConvolutonFilterを適用する事の意味を考えてみます。

ターゲットとなるピクセル(以下、ターゲットピクセルと表記します)の周辺にある8ピクセルはずべて0です。
周辺にどんな色のピクセルがあったとしても、0倍されるので結果は0です。
言い換えると、ターゲットピクセルの周辺のピクセルは全く考慮しないということになります。
一方、ターゲットピクセルに対応する行列の数値は1です。
自身のピクセルの強さ(色)を1倍するわけですから、ターゲットピクセルは元の色そのままということになります。

つまり、この行列は、なんのピクセル操作もしない行列です。

平行に移動コピー

では、行列を以下のように変更してみます。 何が起こるでしょうか? Kernelの数値を変更して実際に結果を確認してみて下さい。
0 0 0
0 0 1
0 0 0

ピクセルを1倍してそのまま表示するという意味では、先ほどの「何もしない行列」と同じですが、
1倍するピクセルがターゲットピクセルの右隣になっています。
つまり、ターゲットピクセルを「ターゲットピクセルの右隣のピクセルにそのまま置き換える」という意味になり、
その結果、画像全体が1ピクセル左に移動します。

(メインビューにマウスカーソルをのせて、拡大画像で確認できます。)
convEx1.jpeg

両側に移動コピー

次はターゲットピクセルの左の数値も1にしてみましょう。
0 0 0
1 0 1
0 0 0

今度はターゲットピクセルの右隣だけでなく、左隣のピクセルもコピーします。
左方向の平行移動と右方向の平行移動を重ね合わせたようなイメージです。
convEx2.jpeg

4方向に移動コピー

ということは、以下のような行列は、
0 1 0
1 0 1
0 1 0

予想通りこうなります↓。
このように1倍する行列はすごくわかりやすいですね。
convEx3.jpeg

ガウスぼかし

1倍する行列はこのくらいにして、つぎはガウスぼかしについてみていきます。
Presetから「gauss」を選択して下さい。
convEx4.jpg

行列が次のように変更されると思います。
0.03 0.11 0.03
0.11 0.44 0.11
0.03 0.11 0.03
これも、わりと行列から結果が推測しやすいと思います。 行列をみると、ターゲットピクセルの色を44%に薄めて、薄まった分を周辺のピクセルのから少しずつ足してるのがわかります。 イメージとしては、ターゲットピクセルを周りの色と馴染ませる感じですね。

convEx14.jpg

エッジ検出

エッジ検出はややこしいのですこしずつ理解していきます。

<<ここ>>にアクセスして、次のテスト画像を開いて下さい。
黒い背景に白い四角形の画像が表示されます。

行列を次のように変更してみて下さい。
0 0 0
-1 1 0
0 0 0
四角形の左辺だけが抽出されました。おもしろいですね。

convEx6.jpg

何が起こっているのか考えてみます。

メインビューにカーソルを移動して、四角形の左上角のピクセルをポイントして下さい。
以下のようにその部分を拡大してみる事ができます。

convEx7.jpeg

ターゲットピクセルを、四角形の左上角のピクセルにして考えてみます。
まず、行列の数値0は無視していいのを思い出して下さい。(0倍なので計算に影響しない)
そうすると、考慮するのはターゲットピクセル(行列の1)とその左隣のピクセル(行列の-1)です。
また、ターゲットピクセルの左隣のピクセルは黒(0)です。
黒(0)を何倍しても0なのでこれも無視できます。

つまり、この場合、考慮するのはターゲットピクセルのみということになります。
結果、ターゲットピクセルを1倍してそのままです。

次に、今見たピクセルの右隣のピクセルをターゲットピクセルにして考えてみます。

convEx8.jpeg

行列から、考慮するのはターゲットピクセルとその左隣のピクセルです。
今度は先ほどと違い、ターゲットピクセルの左隣のピクセルは白(255)です。
対応する行列の数値は-1。かけ算すると255 x -1 = -255。
これをターゲットピクセル(白255)と足し合わせます。
結果、255 + (-255) = 0 で、もともと白だったターゲットピクセルが黒に変更されました。

ちょっと乱暴にまとめると、
自分(ターゲットピクセル)の左隣がxで、自分もx(x以下)だったら黒にする。そうじゃなければ、なにもしない。
これを言い換えると、ある色が連続して平行に並んでいる場合、その一番左の色を残し他は黒(0)にすると言えます。

この結果、左端の輪郭が抽出されます。

「両側にコピー」でやった事を応用すれば、右辺も抽出できます。
0 0 0
-1 2 -1
0 0 0

convEx9.jpeg

同様に、上辺と下辺を抽出することで、基本的な輪郭検出の行列になります。

0 -1 0
-1 4 -1
0 -1 0

convEx10.jpeg

※ちなみに、この行列を使ったエッジ検出をラプラシアンフィルタというらしい。

エンボス

エンボスは輪郭検出に似ています。 輪郭検出と違うところは、斜め1方向だけにとどめるところです。
0 0 0
0 1 0
0 0 -1

convEx11.jpeg

このままだとただの輪郭ですが、行列の計算時に各値を128加算(オフセット)すると、以下のようになります。
(いまのところ、このツールにはオフセットを設定する機能は省略してあるので、実際には↓のような画面を見ることはできません。。)

convEx12.jpeg


四角形の上辺と左辺に影が落ちてみえるのが不思議ですよね。
なぜ128オフセットしたのに、ここだけ黒(0)なのでしょうか?

四角形の左上角のひとつ斜め左上のピクセルをポイントしてみましょう。

convEx13.jpeg

見た目は黒ですが、計算結果である「new pixel」値が(-255)になっています。
このため、128が加算されてもグレーにはならず黒い影のように見えるというわけです。
それに対し、値が0の黒いピクセルは、オフセットにより128となりグレーになります。
うまいことできてますね。

カラー画像の場合

今回は単純なグレースケール画像で色々見てきましたが、カラー画像になっても理屈は同じだと思います。 R、G、B各チャンネル(グレースケール画像)に対して同じ事が行われるだけです。

ここまでのまとめ

webで検索してもなかなかリソースが少ないConvolutionFilterですが、正直、人文系Flash使いとしては、ロジカルにこれを追いかけるのはこの辺りが限界。数学的バックグラウンドを持っている人は、もっとエレガントに説明してくれるはず。 奇才の登場を待ちましょう。。

次回は、ConvolutionFilterを使ったアニメーションの可能性について探ってみたいと思います。

つづく...

2009年2月21日

ConvolutionFilterで遊ぶ その2

前回の続きです。
ConvolutionFilterの理解を深めながら遊ぶためのツールを作ってみました。

>>ツールを表示する


画面左上の大きな画像がメインビューです。ここにフィルタの適用結果が表示されます。
mainView.jpg


メインビューの下にあるボタンをクリックすると、ローカルまたはネット上にある他の画像と差し替える事もできます。
(ネット上にある画像を読み込むにはFlashからのアクセスを許可するためのcrossdomain.xmlが設置されている必要があります。)

loadgui.jpeg


メインビューにマウスカーソルを移動すると、カーソル付近の拡大画像が表示されます。
左がフィルタ適用後で右がオリジナル画像です。
中央のピクセルが、いまカーソルがポイントしているピクセルになります。
zoomView.jpeg


オリジナルのピクセルがどういう計算を経て新しいピクセルに変換されるかをExpressionで確認できます。
計算方法については前回のエントリーを参照して下さい。
「use Hex」をチェックすると、数値が16進数表記になります。
expression.jpeg


KernelはConvolutionFilterに使う行列です。
いまのところ3x3の固定になっています。(将来拡張予定)
この数値を変更すると、フィルタの適用結果がメインビューの画像に反映されます。
kernel.jpeg


いくつかの代表的な行列が、あらかじめプリセットとして用意されています。
プリセットから何か選択するとKernelとメインビューに反映されます。
preset.jpeg


Kernelの数値を変更して、気に入ったエフェクトが出来たら、「Add」をクリックするとプリセットに追加されます。
これはSharedObjectに保存されるので、次回ツールを起動したときにもプリセットに表示されます。
プリセットからアイテムを削除したい場合は、Controlキー(MacはCommandキー)を押しながら
プリセット全体をデフォルトにリセットしたい時は「Revert」をクリックします。
add_revert.jpeg

ConvolutionFilterを連続して適用するとどうなるかを実験するにはこのインターフェイスを使います。
▶ボタンをクリックすると、連続してフィルタを適用します。
×ボタンをクリックすると、適用されたフィルタをリセットします。
スライダーでフィルタを適用するインターバルを調節できます。
repeat.jpeg


作業中の画像やカーネルの値等は、URLパラメータとして渡すことができます。
パラメータつきURLが画面下部に表示されているので、このURLを保存しておけば作業の続きが再開できます。
また「Save as Bookmark」をクリックするとブラウザのブックマークとして保存できます。
bookmark.jpeg


次回はこのツールを使って、さらにConvolutionFilterの挙動を探っていきたいと思います。

つづく...

2009年2月19日

ConvolutionFilterで遊ぶ その1

Flash8からネイティブに実装されているConvolutionFilterですが、自分はあまり理解していなかったので色々遊んでみました。

ConvolutionFilterで出来る事

ConvolutionFilterを使うとガウスぼかしやエンボス等、いわゆるビットマップエフェクトが可能になるようです。
DisplacementMapFilterと併用すると、Photoshopのフィルターの多くをFlashでシミュレートできるんじゃないかと妄想しています。

ConvolutionFilterとは

ConvolutionFilterは、一言でいうとピクセルの色を変更するフィルターです。
ピクセルの色を変換するという意味ではColorTransformに似ていますが、
ColorTransformは係数のみでピクセルの色を計算、変更するのに対し、
ConvolutionFilterは計算時に、変更対象になるピクセルの周辺のピクセルも考慮します。
この係数は行列を使って定義します。

 ColorTransformConvolution
周辺のピクセル考慮しない考慮する

ConvolutionFilterで行われる計算

話を簡単にするため、7x7pxのグレースケールの画像を例に考えてみます。

以下の図は、7x7pxのグレースケールの画像を拡大したものです。
各ピクセルは0(黒)〜255(白)の強さを持っています。


File.png


図中に示したピクセルeに、以下のような3x3の行列を使ってConvolutionFilterを適用した場合、次のような計算が行われます。

a -.5 b 0 c 0
d 0 e .5 f .5
g 0 h 0 i 0

File-1.png


  1. ピクセルeを含む周囲9ピクセル(a~i)と行列の数値が1:1で対応し、各ピクセルの強さと対応する行列の数値をかけ算します。

    例えば、ピクセルaは白(255)で、行列のaの数値(-0.5)とかけ算します。
    a x -0.5 = 255 x -0.5 = -128

    同様に、bからiまでのピクセルを計算します。
    b x 0 = 255 x 0 = 0
    c x 0 = 255 x 0 = 0
    d x 0 = 255 x 0 = 0
    e x 0.5 = 255 x 0.5 = 128
    f x 0.5 = 255 x 0.5 = 128
    g x 0 = 255 x 0 = 0
    h x 0 = 255 x 0 = 0
    i x 0 = 255 x 0 = 0


  2. 9ピクセル全てについてかけ算が終わったら計算結果を足し合わせます。
    -128 + 0 + 0 + 0 + 128 + 128 + 0 + 0 + 0 = 128

    この足し合わせた数値128が、新しいピクセルの強さです。
    つまり、255(白)だったピクセルeが128(グレー)に変更されます。
    (この計算結果をさらに割ったりオフセットしたりすることもありますが、このエントリーでは詳しくは扱いません。)


  3. この操作を、画像の全てのピクセルに対してそれぞれ行います。
    図は、ConvolutionFilterの最終的な適用結果です。

    before
    before.png

    after
    after.png



理屈は簡単ですよね?

例では3x3の行列を使いましたが、4x4でも3x1でも行列であればどんな行列でも使えるようです。
行列の大きさが変わるとフィルタ適用時に考慮する周辺ピクセルの数が変わるというわけですね。

さて、理屈は非常にシンプルなConvolutionFIlterですが、行列から適用結果を想像する事がすごーく難しいです。

そこで、理解を深めるためにツールを作ってみました。
ツールの使い方を簡単に説明した後、このツールを使ってさらにConvolutionFilterを探求していきます。

つづく...