点描画とピクセル化との出会い

Java 2D API を使って絵画をアニメーション化する

この記事では Paul Reiners が、Java™ 2D API とセル・オートマトンを使って、画像を思いもかけぬ芸術的なアニメーションにする方法を説明します。その説明のなかでは、Java コードによる画像処理プログラムの実装と、2 次元セル・オートマトンの一種である巡回空間 (cyclic space) について説明します。この記事で説明する概念を応用すると、独自の画像処理によって芸術的な結果を生成するプログラムを Java 技術を使って作成することができます。

Paul D. Reiners, Software Engineer, WSO2 Inc

Paul ReinersPaul Reiners は Sun 認定の Java プログラマーであり Java 開発者です。彼は Automatous Monk や Twisted Life、Leipzig などいくつかのオープンソース・プログラムの開発者です。彼は 1991年5月に University of Illinois at Urbana-Champaign で応用数学 (計算理論) で修士号を取得しています。彼はミネソタに住み、時間がある時にはエレキベースを練習し、同僚と組んでいるジャズ・バンドで演奏しています。



2008年 11月 18日

この記事では、BufferedImageOp インターフェースを実装することで Java 2D によるカスタムの画像処理クラスを作成する方法を説明します。その説明のなかでは 2 次元の CA (cellular automaton: セル・オートマトン) である巡回空間を使って画像処理アプリケーションを作成します。CA が画像 (例えば JPEG ファイルなど) に対して「演算」を行うと、その画像が時間と共に興味深い形で変化していきます。皆さんがこの記事を読むことでまったく新しい種類の画像処理アプリケーションを作成する可能性を見出せることを祈っています。

2 次元セル・オートマトン

2 次元セル・オートマトンは、一般的に宇宙 (universe) と呼ばれる 2 次元格子のセルで構成されます。各セルには状態があり、状態は 0 と n の間の整数と考えることができます。リスト 1 は Java コードでセル・オートマトンの宇宙を宣言する方法を示しています。

リスト 1. TwoDCellularAutomaton.universe の定義
protected int[][] universe;

仮想的なクロックのティックごとに、セル群は一斉にセルの状態を更新します。あるセルの新しい状態は、そのセルの現在の状態と、そのセルに隣接するセル群の現在の状態により、特定のルールに従って決まります。リスト 2 はクロック・ティックごとに宇宙を更新します。

リスト 2. TwoDCellularAutomaton クラス (リストの一部)
public void update() {
    int[][] newUniverse = new int[rowCount][colCount];
    for (int row = 0; row < rowCount; row++) {
        for (int col = 0; col < colCount; col++) {
            newUniverse[row][col] = updateCell(row, col);
        }
    }
    for (int row = 0; row < rowCount; row++) {
        for (int col = 0; col < colCount; col++) {
            universe[row][col] = newUniverse[row][col];
        }
    }
}

protected abstract int updateCell(int row, int col);

各セルをどのように更新するかを決めるルールは CA のタイプによって異なります。ルールの定義はサブクラスに委譲されます。

巡回空間

巡回空間はウィスコンシン大学マディソン校 (University of Wisconsin at Madison) の数学科の David Griffeath によって発見され、また雑誌 Scientific American に掲載された A. K. Dewdney のコラムで一般に知られるようになりました。

巡回空間では、各セルは n 個の状態のうちの 1 つの状態を取ることができます。通常、各セルの初期状態はランダムに指定され、0 と (n-1 を含む) n-1 の間の乱数になります。あるセルに隣接するセル (そのセルの上下左右にある 4 つのセル) はフォン・ノイマン近傍と定義されます。

リスト 3 は、あるセルの隣接セルとそのセル自体の座標の差を指定することによって、そのセルのフォン・ノイマン近傍を定義しています。

リスト 3. TwoDCellularAutomaton.VON_NEUMANN_NEIGHBORHOOD の定義
protected static final int[][] VON_NEUMANN_NEIGHBORHOOD = { { -1, 0 },
        { 1, 0 }, { 0, -1 }, { 0, 1 } };

巡回空間は以下のルールで定義されます。

k という状態のセルの隣に k + 1 という状態のセルがある場合、そのセルは次のクロック・ティックで k + 1 という新しい状態になる。それ以外の場合には、そのセルの状態は同じままに保たれる。

このルールは巡回的です。つまり、あるセルが n - 1 という状態にあり、その隣に 0 という状態のセルがある場合には、そのセルの状態は次のクロック・ティックで 0 になります。

ConvolveOp はセル・オートマトンに非常に似ていますが・・・

Java 2D API の ConvolveOp クラスは空間畳み込みを表します。つまり ConvolveOp クラスで処理された後の各セルの色は、対応するソース・ピクセルとその隣接ピクセルの色の組み合わせで決まります。

どこかで聞いた話だと思いませんか? これは 2 次元セル・オートマトンとほとんど同じですが、完全に同じではありません。例えば、状態 (色) は離散的ではなく連続的です。(これは完全には正しくありません。RGB の値の数は有限だからです。しかしここでは連続的と言っても差し支えありません。) つまり ConvolveOp クラスの場合は 2 次元セル・オートマトンというよりもむしろ「連続オートマトン」に似ています。またセル・オートマトンとは異なり、あるセルとその隣接セルの現在の状態に基づいて新しい状態をきめ細かく制御することができません。

こうした理由から、ConvolveOp を使ったとしても、巡回空間を定義することはできません。とは言え、ConvolveOp を使う方法も興味深い試みと言うことができます。またこれは ConvolveOp に対する別の見方でもあります。

この単純なルールから、思いもよらない複雑な振る舞いが生まれます。リスト 4 はこのルールを巡回空間でのセルの更新に実装しています。

リスト 4. CyclicSpace.updateCell(int, int) の定義
protected int updateCell(int row, int col) {
    int[] neighborStates = getNeighborStates(row, col, neighborhood);
    int currentState = universe[row][col];
    for (int i = 0; i < neighborStates.length; i++) {
        int neighborState = neighborStates[i];
        if (neighborState == (currentState + 1) % n) {
            return neighborState;
        }
    }

    return currentState;
}

先ほど触れたように巡回空間の宇宙の初期状態はランダムです。セルは、「より大きな」セルに「食べられ」、最終的には状態 0 に再び戻ります。こうしたセルの動きの間、いくつもの区画が自分で形を整えて広がり、波のようになります。そして最終的には、安定した波のパターンが生まれます。これらの波は宇宙を斜めに移動し、ちょっとした回転花火のように見えます。


画像処理プログラムを作成する

java.awt.image.BufferedImageOp インターフェースを使うと、独自の画像処理プログラム (フィルターとも呼ばれます) を作成することができます。この記事では BufferedImageOp の 1 つのメソッド (下記) のみに注目します。

BufferedImage filter(BufferedImage src, BufferedImage dest)

srcdest はピクセルで構成される 2 次元の格子です。このメソッドを実装すると、src からお望みの方法で dest を構成することができます。通常の方法としては、src のピクセルに対して繰り返し処理を行い、対応する dest のピクセルを何らかのルールに従って作成します。これを私の画像処理アプリケーションの中で行います。私はこの画像処理アプリケーションを、有名なフランスの画家 Georges-Pierre Seurat (ジョルジュ・ピエール・スーラ) の名前にちなんで Seurat と呼ぶことにします (完全なサンプル・コードは「ダウンロード」セクションにあります)。

Seurat アプリケーション

画像のピクセルと CA のセルの間にしかるべきマッピングがあるはずだと考えた人がいるかもしれません。ピクセルも CA のセルも 2 次元の格子の中にあり、それぞれが状態を持っています (ピクセルの場合には、RGB (赤、緑、青) の値です。このしかるべきマッピングを filter(BufferedImage src, BufferedImage dest) の実装に利用します。src の各ピクセルに対して、そのピクセルの RGB 値と、対応する CA のセルの状態とを一定のルールに従って組み合わせ、対応する dest のピクセルの新しい RGB 値を作成します。このルールによってフィルターを定義します。

リスト 5 は src のすべてのピクセルに対して繰り返し処理を行い、dest のピクセルを作成する方法を示しています。抽象メソッド getNewRGB(Color) は個々のフィルターによって定義されます。このメソッドは入力される色をフィルタリングした RGB 値を計算して返します。

リスト 5. CellularAutomataFilter クラス (リストの一部)
public BufferedImage filter(BufferedImage src, BufferedImage dest) {
    if (dest == null)
        dest = createCompatibleDestImage(src, null);

    int srcHeight = src.getHeight();
    int srcWidth = src.getWidth();
    for (int y = 0; y < srcHeight; y++) {
        for (int x = 0; x < srcWidth; x++) {
            // Get the pixel in the original image.
            int origRGB = src.getRGB(x, y);
            Color origColor = new Color(origRGB);

            // Get the new RGB values from the filter.
            int[] newRGB = getNewRGB(origColor);

            // Convert the pixel coordinates to the CA coordinates by
            // scaling.
            int cAY = (int) ((double) twoDCellularAutomaton
                    .getRowCount()
                    / (double) srcHeight * y);
            int cAX = (int) ((double) twoDCellularAutomaton
                    .getColCount()
                    / (double) srcWidth * x);
            // Get the state of the corresponding CA cell.
            int state = twoDCellularAutomaton.getState(cAY,
                    cAX);
            // Determine the weight of the filtered RGB values depending on
            // the state.
            double filterProportion = (double) state
                    / (double) twoDCellularAutomaton.getN();

            // Determine the weighted average between the filtered RGB
            // values and the image RGB values.
            int weightedRed = (int) Math.round(newRGB[0] * filterProportion
                    + origColor.getRed() * (1.0 - filterProportion));
            int weightedBlue = (int) Math.round(newRGB[1]
                    * filterProportion + origColor.getBlue()
                    * (1.0 - filterProportion));
            int weightedGreen = (int) Math.round(newRGB[2]
                    * filterProportion + origColor.getGreen()
                    * (1.0 - filterProportion));

            // Set the pixel in dest with this weighted average.
            dest.setRGB(x, y, new Color(weightedRed, weightedBlue,
                    weightedGreen).getRGB());
        }
    }

    return dest;
}

abstract protected int[] getNewRGB(Color color);

画像のピクセルと CA のセルとの間に 1 対 1 のマッピングを使っていないことに気付いた人がいるかもしれません。CA は (少なくともほとんどの場合に) ピクセルほどきめ細かくないのです。私は元々パフォーマンスの理由からこのようにしたのですが、CA の宇宙にさまざまなサイズを使うと、ピクセル化による興味深い効果を持たせることができます。

リスト 6 は、ある特定の getNewRGB(Color) の実装を示しています。この実装は私が「RGB 補色」と呼ぶ値を計算しますが、「RGB 補色」は真の補色ではありません。(真の補色を計算するフィルターも興味深いフィルターですが、そのコーディングは単純ではありません。)

リスト 6. RGBComplementFilter クラス (リストの一部)
protected int[] getNewRGB(Color c) {
    int red = c.getRed();
    int newRed = getComplement(red);
    int green = c.getGreen();
    int newGreen = getComplement(green);
    int blue = c.getBlue();
    int newBlue = getComplement(blue);

    return new int[] { newRed, newGreen, newBlue };
}

private int getComplement(int colorVal) {
    // 'Reflect' colorVal across the mid-point 128.
    int maxDiff = colorVal >= 128 ? -colorVal : 255 - colorVal;
    // Divide by 2.0 to make the effect more subtle. Could also just use
    // maxDiff for a more garish effect.
    int diff = (int) Math.round(maxDiff / 2.0);
    int newColorVal = colorVal + diff;

    return newColorVal;
}

こうする他に、getNewRGB(Color) を一般化し、変換対象のピクセルの色だけではなく、隣接する 8 つのピクセルの色も渡すようにすることもできます。そうすれば、フィルタリングされたピクセルの色が隣接セルの色に依存するような何らかの他の効果 (ぼかしやエッジ検出など) を得ることができます。これは素晴らしい機能強化になります。

最後に、CA のクロック・ティックに合わせて画像を更新することで画像をアニメーション化します。このためには javax.swing.Timer を使います。(これは変化する画像をアニメーション化するための簡単な方法ですが、最善の方法ではありません。Jonathan Knudsen による著書『Java 2D Graphics』には、アニメーションを作成するための、もっと複雑で優れた方法が紹介されています。「参考文献」を参照。)

Seurat を実行する

図 1 はジョルジュ・スーラ (Georges Seurat) が 1884年に描いた点描画の名作「グランドジャット島の日曜日の午後」の写真です。

図 1. ジョルジュ・スーラの「グランドジャット島の日曜日の午後」
ジョルジュ・スーラの「グランドジャット島の日曜日の午後」

では、スーラの絵に対して RGB 補色フィルターを使って Seurat アプリケーションを実行します。図 2 は、巡回空間がランダムな初期状態にある場合に、この絵に RGB 補色フィルターをかけたものを示しています。

図 2. 巡回空間がまったくランダムな状態で RGB 補色フィルターをかけた「グランドジャット島の日曜日の午後」
巡回空間がまったくランダムな状態で RGB 補色フィルターをかけた「グランドジャット島の日曜日の午後」

図 3 は、重みの異なるいくつかのパターンで巡回空間が秩序の取れたパターンへと自己構成を開始した状態 (ただし大部分はランダムのまま) で RGB 補色フィルターをかけたときの絵を示しています。

図 3. 巡回空間が中間的な状態で RGB 補色フィルターをかけた「グランドジャット島の日曜日の午後」
巡回空間が中間的な状態で RGB 補色フィルターをかけた「グランドジャット島の日曜日の午後」

図 4 は 巡回空間が最終的な安定状態で RGB 補色フィルターをかけたときの絵を示しています。

プロセス・アートとアルゴリズム・アート

私は Seurat を作成するための準備として Jackson Pollock に関する資料を大量に読みましたが (私は最初、このアプリケーションを「Blue Poles」と呼んでいました)、その際、プロセス・アートという言葉に何度も突き当たりました。私は世間知らずなことに、プロセス・アートはアルゴリズム・アートすなわち一定のルールに従って作成されるアートを指すのだと思っていました (アルゴリズム・アートでは、芸術家が一連のルールを考案し、それらのルールを何らかの初期条件で実行することによって絵画のような芸術的な成果物を生成します)。しかしアートの世界では通常、プロセス・アートとアルゴリズム・アートは同じではないことに気付きました。Guggenheim Collection Glossary によると、

「プロセス・アートは (あらかじめ決められた構図や計画に基づくのではなく) アートを作成していくプロセスと、可変性や刹那性の概念を重視するものであり、これは Lynda Benglis や Eva Hesse、Robert Morris、Bruce Nauman、Alan Saret、Richard Serra、Robert Smithson、Keith Sonnier などの作品に見ることができます。

つまりアルゴリズム・アートすなわち一定のルールに従って作成されるアートは、そのアルゴリズム (またはプログラム) の中間的な状態が芸術的に興味深いものであるなら、そのアルゴリズムを実行している最中は一種のプロセス・アートと見なすことができます。

私達はプログラマーとして、プロセス・アートを自ら作成したり、芸術家によるプロセス・アートの作成を支援したりするのに適したユニークな立場にあるのです。

図 4. 巡回空間が安定状態で RGB 補色フィルターをかけた「グランドジャット島の日曜日の午後」
巡回空間が安定状態で RGB 補色フィルターをかけた「グランドジャット島の日曜日の午後」

しかし静止した絵を見ても、フィルターや CA の真価はわかりません。(そもそも、このアプリケーションは静止画をアニメーション化するために作成されたものです。) 皆さんには、実際にこの Java アプレットを実行してフィルターと CA によって絵が変化する様子を見るようにお薦めします (「参考文献」にはライブのデモへのリンクがあります)。

美的観点からの考察

一部の人は「グランドジャット島の日曜日の午後」のような偉大な絵画に対して画像フィルタリング・アプリケーションを実行することは冒とくだと考えるかもしれません。その意見には大いに賛成しますが、私はこの絵を単なる例として使っているにすぎません。私の主たる目的は、単純なセル・オートマトンを使うことによって画像を複雑で興味深いアニメーションにする方法を示すことであり、有名な絵画はそのサンプルとして好都合だったのです。

さまざまなタイプの絵画に対して Seurat を実行したところ、抽象画の場合にも具象画の場合にも興味深い結果が得られましたが、Seurat の効果が最も発揮されたのはモダン・アート (特にポップ・アート) のようでした。例えば Jasper Johns の絵画「Flag」に対して Seurat を実行すると、非常に興味深いパターンが現れました。「Flag」の直線に対して巡回空間による斜めの線はとても効果的でした。また Jackson Pollock のドリップ・ペインティングに Seurat を実行した場合にも興味深い結果が得られました。例えば、巡回空間による CA が Pollock の「Blue Poles」に広がっていくと、複雑な模様のディテールが隠れたり見えたり、また再び隠れたりするため、その変化に応じて見る人の注意がさまざまな部分に注がれることになります。写真にも効果的で、Ralph Eugene Meatyard によるシュールレアリズムの写真に Seurat を実行して楽しむことができました。

Seurat のようなアプリケーションを実行する際には、2 次元セル・オートマトンのタイプ、フィルター、そしてオリジナルの画像という、3 つの側面を変化させることができます。この記事で使用した 2 次元セル・オートマトンは巡回空間のみでしたが、Hodgepodge などの他のタイプの 2 次元セル・オートマトンを使うこともできます。フィルターに関しては十分に想像力を働かせてプログラミングを行ってください。ここでは主に色に対して演算を行うフィルターを使ってみましたが、画像の中で空間の関係を変化させるフィルターを使ってみるのも面白いかもしれません。例えば画像の表面をラップするフィルターをプログラミングすると、Beatles のアルバム Rubber Soul のジャケットに使われたような効果を得ることができます。そしてオリジナルの画像には、どのような画像でも (例えば写真なども) 使うことができます。与えられた画像に対して、フィルターと CA のタイプとの組み合わせを変えることによって、望むような結果が得られたり、得られなくなったりします。この記事をきっかけとして、さまざまな画像でさまざまなアニメーションを試してみる気になってくれたようであれば幸いです。


謝辞

ビジュアル・アートへの私の興味をかき立ててくださった Julia Braswell 氏に感謝いたします。


ダウンロード

内容ファイル名サイズ
Java files for this articlej-j2D.zip19KB

参考文献

学ぶために

  • The Magic Machine: A Handbook of Computer Sorcery』(A. K. Dewdney 著、1990年 W. H. Freeman 刊) は雑誌 Scientific American に掲載された Dewdney によるコラム「Computer Recreations」をまとめたものであり、巡回空間に関する章と Hodgepodge に関する章が含まれています。このコラムが 1980年代に初めて登場した当時、私はそこで紹介されたアルゴリズムをすべて Amiga 500 で AmigaBASIC を使ってプログラミングしていました。
  • Primordial Soup Kitchen を訪れ、巡回空間を発見した David Griffeath からセル・オートマトンの詳細を学んでください。
  • Java アプレット、Seurat を試してみてください。
  • Java 2D Graphics』(Jonathan Knudsen 著、1999年 O'Reilly Media 刊) は Java による 2D グラフィックスを紹介した優れた本です。
  • Java 2D API には Java による 2D に関するドキュメントやサンプル、その他のリソースが豊富に用意されています。
  • Art: The Way It Is, 3rd ed』(John Adkins Richardson 著、1973年 Prentice Hall and Harry N. Abrams 刊) はスーラやその他の芸術家について学ぶ上で必読の本です。
  • Cellular automata and music」(Paul Reiners 著、developerWorks、2004年5月) を読み、Java 言語とセル・オートマトンを使ってアルゴリズムで作曲する方法を学んでください。
  • Introduction to Java 2D」(Mitch Goldstein 著、developerWorks、2002年7月) は GUI プログラミングに Java 2D を活用することで実現される高度な描画、テキスト・レイアウト、画像操作などのメリットを順を追って解説したチュートリアルです。
  • イメージベース・パスを使用した2Dアニメーション」(Barry Feigenbaum と Tom Brunet の共著、developerWorks、2004年1月) は、可逆画像、Swing 技術、および著者自身による Java ベースのアニメーション・エンジンを組み合わせ、固定オブジェクトに対して 2D アニメーションによる動きのシーケンスを生成する方法を示します。
  • Creating Java2D composites for rollover effects」(Joe Winchester と Renee Schwartz の共著、developerWorks、2002年9月) を読み、Java 2D API を使った画像の作成と操作について学んでください。
  • technology bookstore には、この記事や他の技術的な話題に関する本が豊富に取り揃えられています。
  • developerWorks の Java technology ゾーンには、Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=358722
ArticleTitle=点描画とピクセル化との出会い
publish-date=11182008