強火で進め

このブログではプログラム関連の記事を中心に書いてます。

漢字動画


「第0回 HTML5プログラミング&クリエイティブ・コンテスト」に応募したけど選ばれなかった作品その2です。

動画の画像を毎フレーム取得し、グレースケール化したときの色の濃淡に合わせて表示する漢字を選択して色ではなく漢字の濃さで表現するアスキーアートの的な作品です。文字なので選択やコピーが出来ます。

漢字動画
http://tsuyobi.heteml.jp/html/html5/video/AA/

※動画を作成すると動作が開始します。
※フォントのロードが終わるまではデフォルトのフォントで描画されます。
※「ループ再生」にチェックを付けると動画が最後まで再生されると先頭に戻ります。

漢字の選択処理の説明

今回の作品は動画の画像を漢字に変換するというものです。

対象の漢字は「第一、第二水準の全漢字」それとこれは漢字ではないけれど「全角スペース」。

この漢字の中から濃淡を考慮し、256個の漢字をグレースケールの0〜255の256個に割り当たテーブルを作りました。このテーブルを元に動画をグレースケール化したときの色の濃淡(0〜255)に対応する漢字を表示しています。

テーブル作成の処理もJavaScriptで作れた良かったのですが流石にそれやっちゃうといつまでも起動出来そうに無いのでそこはPythonで抽出プログラムを作りました。

まずは漢字に該当する文字の取得。これはunicode.orgでサイトの「JIS X 0208」のファイルを使いました。詳しく下記参照。

Unicodeの中のJIS X 0208に当たる文字を取得 - 強火で進め
http://d.hatena.ne.jp/nakamura001/20110108/1294453032

そして、こんなプログラムを作って「JIS X 0208」から漢字だけを抽出しました。抽出のルールはコメントに が付いている物。

#! /usr/bin/env python
# coding: utf-8
# coding=utf-8
# -*- coding: utf-8 -*-
# vim: fileencoding=utf-8

import codecs

ch_list = []
f = open('JIS0208.TXT', 'r')
for row in f:
    if row[0] != '#':
        if len(row.split("\t")) >= 4 and row.split("\t")[3] == "# <CJK>\n":
            c = row.split("\t")[2]
            ch_list.append(unichr(int(c, 16)))
f.close()

f = codecs.open('unicode.txt', 'w', 'utf-8')
#txt = "".join(ch_list)
txt = "\n".join(ch_list)
f.write(txt)
f.close()

漢字の抽出はこんな感じで簡単に準備出来たのですが次の段階の256個の漢字の選択を行うプログラムの作成が中々厄介でした。

まずはこんな感じのプログラムを作ってみました。
1. 漢字を実際にグレースケールで描画
2. ソート
3. 256等分で抽出
ソート後の漢字は後半に濃いものが偏っていました。今回は黒地に白い文字で描画したのですが後半は白が多いものばかり、という状況でした。

次に考えたのが「基準値*n」で行う方法です。
1. 漢字を描画した時のRGB値の合計を求める。これを全ての漢字に行い、その合計値を求める。
Pythonはメモリが許す限り無限の桁数に対応なのでこの様な力技も簡単に行えます。
2. その合計値を256で割る。ここでは分り易い様に256で割って求めた値が 50 だったとします。
仮にソート後の漢字の先頭3文字をA〜Cとし、その時の漢字を描画した時のRGB値の合計を記載すると以下の様になったとします。

漢字 RGB値の合計値
A 0
B 30
C 60

この時、最初は 50*0 = 0 で0以上(x >= 0)になる漢字を選びます。Aが該当するのでAを選択。
次は 50*1 = 50 で50以上(x >= 50)になる漢字を選びます。50以上はCなのでCを選択します。

この様なルールで作成したところ、 2. の所の合計値を256で割って求めた値が大きいため前半部分のRGB値の合計値が少ない漢字が一気にスキップされ(Aの次にKが選択される感じ)、後半には逆にスキップされずに位置の近い漢字ばかり選択されました。

次に試したのは「漢字全体を256のグループに分割+1グループの中から漢字1つ選択」という方法です。
1. 濃淡順でソートした漢字を256のグループに分割。
2. 先ほどの「基準値*n」のルールと同じルールで漢字を選択。しかし、今回は 1. で分割したグループをまたぐ場合はそのグループの中の最後の漢字を使う。

例を挙げます。例えばグループと漢字が以下の様になっているとします。

グループ 漢字 RGB値の合計値
1 A 0
1 B 20
2 C 30
2 D 40
3 E 70

基準値は 50 だったとします。最初はグループ1の中から 50*0 = 0 で0以上(x >= 0)を選択、Aが選ばれます。次の選択の時はグループを+1し、2のグループから選択します。 50*1 = 50 で50以上(x >= 50)を選択、2のグループの中には存在しないみたいです。しかし、「分割したグループをまたぐ場合はそのグループの中の最後の漢字を使う」というルールが有るのでDを選択します。

これが一番マシだったのですがそれでもまだ偏りが有るなぁ。と感じました。この分野に詳しい人ならもっと綺麗に解けるんでしょうが自分の今の実力だとこれ位が限界でした。うーむ、もっと勉強せねば。

もしも自分でも試してみたい人が居たら最後のバージョンのプログラムを書いておきますので頑張ってみて下さい。そして、改善されたものをブログなどに解説付きで公開して貰えると、とても嬉しいですw

※PILモジュールがインストールされている必要が有ります。
※同じフォルダにフォントファイルを置き、プログラムの先頭の font_file にそのフォントファイル名に置き換えて下さい。自分はこちらの「衡山毛筆フォント行書」を使用しました。

#! /usr/bin/env python
# coding: utf-8
# coding=utf-8
# -*- coding: utf-8 -*-
# vim: fileencoding=utf-8

# 文字のピクセル数を取得後、
# ソートをし、256等分して文字をチョイス
import codecs
from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw

#font_file = 'mika.ttf'
font_file = 'KouzanGyoushoOTF.otf'

ch_list = []
f = codecs.open('unicode.txt', 'r', 'utf-8')
count = 0
for row in f:
    if row[-1] == "\n": row = row[:-1]
    ch_list.append(row)
    count += 1

all_pix_data = []
font_size = 12;
img_size = (font_size, font_size)
font = ImageFont.truetype(font_file, font_size, encoding='utf-8')
img = Image.new('RGB', img_size, (0,0,0))
draw = ImageDraw.Draw(img)
img_w, img_h = img_size
all_pix_num = 0L
for ch in ch_list:
    draw.rectangle([0, 0, img_w, img_h], fill=(0,0,0))
    draw.text((0, 0), ch, fill=(255,255,255), font=font)
    col_count = 0L
    for y in range(img_h):
        for x in range(img_w):
            c = img.getpixel((x,y))
            col_count += c[0]
            col_count += c[1]
            col_count += c[2]
    pix_data = {'uni':ch, 'num':col_count}
    print pix_data
    all_pix_data.append(pix_data)
    all_pix_num += col_count
#    img2.show()
all_pix_data.sort(cmp=lambda x,y: cmp(x["num"], y["num"]))
for pix_data in all_pix_data:
    print pix_data["uni"],pix_data
print "min:",all_pix_data[0],"max",all_pix_data[-1]
max_pix_num = all_pix_data[-1]['num']
str = ""
div_num = 256
pix_step = float(max_pix_num)/div_num
idx_step = float(len(all_pix_data)-1)/div_num
n = 0
now_pix_num = 0
idx = 0
no = 0
print "all_pix_num:",all_pix_num,"len:",len(all_pix_data), \
    "pix_step:",pix_step,"idx_step:",idx_step
for i in range(div_num):
    while(i*pix_step > all_pix_data[idx]["num"]):
        idx += 1
        if idx >= (i+1)*idx_step: # 次のレンジ(グループ)に入ってしまった場合は打ち切る
            idx -= 1
            break
    print "no:",no, \
            "idx:",idx,"limit:",i*(idx_step+1),"uni:",all_pix_data[idx]["uni"], \
            all_pix_data[idx]["num"]
    no += 1
    str += '"%s",' % all_pix_data[idx]["uni"]
    idx += 1
print str