俳句プログラミング投稿作品について

f:id:takemaruhirai:20150308035618p:plain

ブログをつくってみました。いろいろ燻っているので、IT関連のテーマでこちらに書いていこうと思います。

まずは、「プロ生ちゃん 俳句プログラミング プチコンテスト」に応募したコードの解説から。ちょっと分かりにくかったかな、という反省を込めて。コンテストの詳細は、以下のとおりです。楽しそうだったので、なんとなくノリで参加してしまいました。

応募作品概要

応募作品は、こんな感じです。

ソースコードはこちらで公開しています。

プログラミング生放送のサイトでも取り上げて頂いています。

何をやっているのか

作品の説明に日本語をUTF-8で表すと3バイトなので、RGBのカラーコードから変換と書いたのですが、これだけだと何をやっているのか、いまひとつ伝わりにくいですね。なので、ここでもう少し詳細に説明しようと思います。

UTF-8では、日本語は3バイト

ウェブではすっかり標準化した文字コード UTF-8 ですが、Windows標準のShift_JIS等と違い、日本語に使われる漢字や仮名は2バイトではなく3バイト(24ビット)のコードで表されます。

UTF-8 - Wikipedia

たとえば、「あ」という文字をエンコードすると「%E3%81%82」になります。つまり、E38182という3バイトの16進数で表現可能ということです。

f:id:takemaruhirai:20150307222948p:plain

【みんなの知識 ちょっと便利帳】URLエンコード/デコードツール = UTF-8

カラーコードも3バイト

ところで、ウェブやPC上で扱われる「色」は、通常RGBつまり、赤・緑・青の光の三原色で表されます。とくに一般的なトゥルーカラーでは、赤・緑・青それぞれの要素が10進数で0~255の1バイト(8ビット)で表されます。3つの色要素を合わせて3バイト(24ビット)です。

RGB - Wikipedia, フルカラー - Wikipedia

たとえば、CSSなどで文字色や背景色を赤にしたい場合、#FF0000と表記しますが、これはそのRGBを16進数で表現したものです。

f:id:takemaruhirai:20150307224202p:plain

WEBカラーピッカー(Color Picker)

日本語を色に置き換えた画像を用意する

つまり、

  • 日本語は3バイトのコードで表現できる
  • 色は3バイトのコードで表現できる

ということです。ここで、「文字」⇒「3バイトのコード」⇒「色」と変換できるということを思いつきました(逆ではありません)。

では、早速、俳句を色に直してみます。例として芭蕉の句である

さまざまの 事思ひ出す 桜かな

なんてどうでしょう? 末尾に作者名も付けましょう。

文字 コード 文字 コード 文字 コード
E58FA4 E38284 E38284
E6B1A0 E38284 E38284
E38284 E38284 E38284
E38284 E38284
E38284 E38284 E88AAD
E38284 E89589

おお!!見事に桜の雰囲気を表すピンク色のイメージとなりました。ついでに空白文字「 」をコード化すると、%20 となります。

文字 コード
000020

それでは、これらの文字(色)を各所に散りばめた画像を作ってみます。せっかくなので、桜をイメージした画像にしました。プロ生ちゃんにも入ってもらいました。

f:id:takemaruhirai:20150307232928p:plain

呼び出したい文字(色)の座標をコード化する

さて、次は、上で用意した画像から俳句を抽出するのですが、各文字に対応する色は、桜の花びらとしてランダムな位置に散りばめてしまいました。なので、必要とする色が画像のどこにあるのかを座標を使って指定する必要があります。

たとえば、最初の「さ」という文字(%E5%8F%A4)は、画像の左上を原点とした座標上で x=450px, y=40px の位置にある花びらの色(#E58FA4)で表されます。つまり、「さ」という文字を、450, 040 という2つの数値で置き換えることができます。

f:id:takemaruhirai:20150307235937p:plain

同様にして、俳句を構成する文字列をすべて座標に置き換えると、

文字 座標 文字 座標 文字 座標
(450, 040) (320, 060) (122, 044)
(260, 040) (030, 030) (234, 122)
(060, 170) (160, 160) (170, 120)
(240, 020) (180, 180)
(044, 090) (444, 144) (270, 070)
(030, 130) (260, 100)
文字 座標
(026, 192)

というようになります。プログラム中でこの座標を利用して画像のピクセルデータを取得し、そのカラーコードを解析すれば、該当する文字が復元可能なはずです。

JavaScriptで画像解析を行うためには、HTML5 Canvasの機能を利用します。ごく簡単に述べると、以下のメソッドを利用して、(x, y) の座標から 任意の [width, height] のピクセルデータを配列として取得できるのです。

var id = context.getImageData(x, y, w, h);

取得した配列は、以下のような1ピクセル分のデータの繰り返しになっています。

添字 0 1 2 3
赤(R) 緑(G) 青(B) アルファ

配列の各要素の値は10進数ですが、次の方法で16進数に変換することができます。

var dec = 10;
var hex = dec.toString(16);

JavaScriptによる画像解析の方法は、wata_htn様の記事に分かりやすく説明されています。


[3][HTML][canvas]canvasで画像解析 - wataメモ

座標コードも画像に埋め込む

これで、「座標」⇒「色」⇒「文字」という順で復元が可能になりました。この座標データを、数値の配列などの形でプログラムコードの中に保持すれば、その値を走査することで元の俳句の文字列が取得できるという算段です。

しかし、それでは、「結局のところコード中に俳句と等価なデータを持っている」ことになり、なんだか片手落ちな気がしました。できれば、俳句データはすべて画像が保持しており、(理論上は)コードを変更しなくても画像を差し替えれば別の俳句が表示されるようにしたかったのです。その無意味なこだわりのため、座標データも画像中に埋め込んでしまいました。

座標データは、画像の y=0 の行に埋め込みます。桜の画像の左上部分を拡大すると、下のように1ピクセルごとの色が並んでいます。

f:id:takemaruhirai:20150308005628p:plain

この色の並びを左から順にカラーコード、さらに座標に該当する文字に直すと、以下のようになっています。

ピクセル カラーコード 座標 文字
R 225 450
G 020 040
B 130 260
A 255
R 020 040
G 030 060
B 085 170
A 255
R 120 240
G 010 020
B 022 044
A 255
R 045 090
G 013 026 (空白)
B 096 192
A 255

この調子で最後まで走査します。

ただし、やむを得ずいくつかの制約を設けてあります。

  • 画像のサイズが 256×256 を越えているため、プログラムでは実際にはRGBの各値×2を実際の座標値として解釈しています。そのため座標は偶数でなければなりません。
  • アルファ値は意図通りにデータとして拾い難いので、座標として採用せずスキップしています。
  • 値が0の場合は無視します。x=0 あるいは y=0 の位置は、文字情報として使わないことにします。

こうして、走査したピクセルデータを基にプログラムで「座標」⇒「色」⇒「文字」と復元すれば、もとの俳句を得ることができます。あとは、得られた文字列を適当に加工して、Canvas 上に配置すればいいだけです。

JavaScriptコード解説

canvas の準備

まず変数の宣言、そしてHTML5 Canvasを準備します。

var haiku, id;
var canvas = document.createElement("canvas");
var ctx    = canvas.getContext('2d');

次に、Imageオブジェクトで画像をCanvasに読み込みます。

var img = new Image();

// JSFiddleに画像を添付するのは難しそうなので、画像は Dropbox から読み込む
img.crossOrigin = "Anonymous";
img.src  = "https://dl.dropboxusercontent.com/u/7521936/pronamachan_sakura.png";

// 画像が完全に読み込まれたら、画像をCanvasに取り込む
img.onload = function(){

    canvas.width  = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0);
    document.body.appendChild(canvas);
    
    // 以降の処理
    
};  

座標⇒色⇒文字の変換

Canvasに画像が読み込まれたら、先ほど登場した getImageData メソッドを使って、画像の先頭1行のピクセルデータを取得します。

id = ctx.getImageData(0,0,img.width,1).data;

目的である色情報は、取得した ImageData オブジェクトの data プロパティに格納されています。ただし、この配列は Uint8ClampedArray という特殊な配列であるため、map, reduce等で扱いやすいように普通の配列に変換しています。

haiku = Array(id.length).join(" ").split("")

さらに、値×2を座標として解釈する制約のため、すべての要素の値を2倍しています。

.map(function(e,i){ return id[i] * 2 })

アルファ値は座標値として採用しないため、4番目、8番目、・・・の要素を除外します。また、値が 0 となった場合も切り捨てます。

.filter(function(e,i){ return i % 4 < 3 && e > 0 })

この配列を [ [x1,y1], [x2,y2], ... ] という形の二次元配列に直しています。

.reduce(function(a,b){

    if (a[a.length-1].length > 1) a.push([]);
    a[a.length-1].push(b);
    return a;

}, [[]])

最後に、この二次元配列を座標と解釈し、各座標にあるピクセル色情報を取得、そのカラーコードをデコードしてUTF-8の文字列に変換します。

.map(function(e){

    // 座標 (e[0], e[1]) の1ピクセル分のデータを取得
    var coord = ctx.getImageData(e[0], e[1], 1, 1).data;

    try {
        //           R         G         B
        var code = [ coord[0], coord[1], coord[2] ]

            // の値を 16進数 に変換したうえで、UTF-8としてデコード可能な形式に変換
            .reduce(function(a,b){
                return a || b+0>0? a + "%"+b.toString(16) : a;
            },  "");

 
        // 各要素を、文字と解釈してデコード
        return decodeURIComponent(code);
    }

    // 文字と解釈できないコードは無視
    catch (e) {
        return null;
    }
});

Canvas上に俳句を表示

こうして得られた文字列は、俳句として「一句め 二句め 三句め 作者」という形になっているものとします。最後に、この文字列を空白文字で分割し、位置を適度に調整してCanvas上に時間差で配置しています。

haiku.forEach(function(s, i){ setTimeout(function(){

    ctx.font = "bold 30px '游明朝','ヒラギノ明朝 ProN W3','HG明朝'";
    ctx.fillText(
        haiku[i],
        20 + 80 * Math.pow(i, 1.45),           // x座標
        i==haiku.length-1 ? 180 : 35 + i * 40  // y座標
    );

}, 1000 + i * 1200)});

これで、画像から俳句を読みって文字として表示するプログラムが出来ました。

f:id:takemaruhirai:20150308035618p:plain

もう一句!

mapやreduceやfilterといったメソッドの引数である無名関数を外出しし、わざわざ複雑なオブジェクト内に定義して、名前ををつけてみました。こうすることで無理矢理、コードの中に別の俳句をゴリ押しすることができました。

花の雲 鐘は上野か 浅草か

ちなみに、こちらも芭蕉の詠んだがテーマの俳句です。


haiku = Array(id.length).join(" ").split("")
        
    .map   ( Hana.No.Kumo              )
    .filter( Kane.Wa                   )
    .reduce( Ueno.Ka, Asakusa.Ka       )                
    .map   (              Matsuo.Basho ).join("").split(" ");

コードの可読性からすると、完全に蛇足でしかないのですが・・・

最後に

桜をRGBに変換すると桜色っぽくなるよ!

(ネタバレ)

仰るとおりです(笑)

どういうことかというと、そうですね、たとえば「古池や 蛙飛び込む 水の音」を変換してみましょうか。まずは、「古池や」から・・・

文字 コード
E58FA4
E6B1A0
E38284

ああ、残念ながらどれもピンクっぽいですね(笑)。 どう頑張っても、蛙や池のイメージである青や緑は出てきません。 そんなわけで、必然的にテーマは「」となった訳でした(梅でもよかったのですが)。