にせねこメモ

はてなダイアリーがUTF-8じゃないので移ってきました。

長方形で厚くて硬い万引き防止タグを分解してみた

よく電気屋などで小型商品にくっつけてある万引き防止タグ、検知方式によっていろいろな形があるのだが、長方形で立体的に厚みがある硬いタグを分解してみた。
f:id:nixeneko:20170809134741p:plain

このタグは使い捨てで、粘着テープが裏についていて、商品に貼りつけられる。このタグがゲートを通ると検知され、警報が鳴る。会計時に無効化処理を行うことで、会計を済ませた商品がゲートで反応することを防いでいるらしい。

分解

f:id:nixeneko:20170809134948p:plain
f:id:nixeneko:20170809135112p:plain
f:id:nixeneko:20170809135422p:plain
さて、分解してみると、底面に固定された金属板1枚と、金属の薄いフィルム2枚が入っている。フィルムの方は固定されていなくて、プラスチックで囲われたタグの内側の空間の中で動くことができるようになっている。タグの厚みはこのフィルムが動く空間を作るためのもので、タグが固いのも同じ理由だろう。

さて、随分シンプルな構造であるが、いったいどういう原理で動作するのだろうか。

原理

調べてみると、このタグはAcousto-magnetic systems (音響磁気方式)という方式のものらしい。Wikipediaに解説がある。

それによると、防犯ゲートの送信アンテナから送信されるパルスによって中に入っている金属フィルムが共振し、その共振によって発生する電波をゲートが受信するとアラームを起動するということらしい。

また、一番下の金属板が磁化されていると有効になるが、磁化が解除されると無効化され、ゲートに反応しなくなるとのことである。
つまり、解除機の無効化処理とは、実際には磁化の解除を行っているのだろう。

もしかして、解除されたタグについても、磁石を隣に置いておけばゲートに反応するようになるのかもしれない。


しかし、こんなに簡単な仕組みで防犯タグが実装されてるのを見て感心してしまった。解除もできる様になってるし。

イオニア数字変換TeXマクロ

イオニア数字ネイティヴではないので、イオニア数字を書く場合にはいちいち調べて書かないといけないのだけれど、実際面倒なので、(La)TeXマクロにしてLaTeXなどで書く際に簡単に変換できるようにしようというのが今回の目的。

イオニア数字というのは、古代ギリシャで使われた数値の書き方であって、ギリシャ文字を使って数値を表現するものである。一般にギリシャ数字とも呼ばれるとのこと。
詳しくはWikipediaなどを参照: ギリシアの数字 - Wikipedia

サンプル

f:id:nixeneko:20170809031334p:plain

コード

Githubに上げた。XeLaTeXでコンパイルできる。マクロの出力結果について網羅的に確認したわけではないので不適切な部分があるかもしれない。
GitHub - nixeneko/ionicnumber: A TeX macro that does a conversion from a number into Ionic numeral system.

使用法

ionicnum.texの中で\ionicnumマクロが定義されている。これを使うには、数字を与えて

\ionicnum{1234}

のようにする。1234の部分は任意の数字が入る。

メモ

イオニア式の記数法には年代や学者等によりいくつかの変種があり、特に10000以上の書き方はさまざまであるらしい。
この実装で用いたものは次のようなものである:

  • 数字にはギリシャ文字の小文字を用いた。
  • 数字はギリシャ文字列の右肩にʹを付けることによって表現した。
  • 6にはスティグマϛを、90のコッパはϟを、900のサンピはϡを用いている。
  • 10000以上はmyriad Μにより、Μの上に一万~一千万の位の数字を書くことによって表現した。
  • 最大で対応している数は一億(100,000,000)であり、ΜΜʹで表す。

The largest number named in Ancient Greek was the myriad myriad (written MM) or hundred million.

https://en.wikipedia.org/wiki/Myriad

実装について

まず、一~千の位の数字を変換することを考える。
これは、元の数をnとすると、各位の数字は

  • 一の位:  \displaystyle n \mod 10
  • 十の位:  \displaystyle \left\lfloor \frac{n}{10} \right\rfloor \mod 10
  • 百の位:  \displaystyle \left\lfloor \frac{n}{100} \right\rfloor \mod 10
  • 千の位:  \displaystyle \left\lfloor \frac{n}{1000} \right\rfloor \mod 10

となる。次にこれを\ifcaseを使ってギリシャ数字の各文字に変換する。


次に一万以上の数値について。
元の数値を10000で割ったものについて、その値をΜの上に積み重ねて表示する。数値の表示は一~千の位の数字に変換するマクロを共用している。
また、このとき、10000がΜとなるように条件分岐を行っている。


一億以上の数に対応するために再帰っぽい感じで書いてしまったのだが、どうせ処理系が 2^{31}-1 = 2147483647までしか対応してないので、ごちゃごちゃするだけであまりメリットなかったように思う。

20180328追記

気が向いたら修正する……。

Chainerでアニメキャラの目からハイライトを消す

pix2pix

pix2pixというモデルがある。入力画像と、それと一対一対応する変換ターゲットの画像を用意すると、その間の変換を自動で学習してくれるというものである。
元論文はこれ:

この論文内では、モノクロ写真に着色するだとか、線画に色を付ける、航空写真から地図を生成する、ラベリング画像から写真を復元する、などが行われている。要するに、何らかの画像のペアを用意すれば、ペア間の画像変換が学習できるっぽい。


それで、入力画像としてアニメキャラの顔の画像を用意し、出力画像として入力画像から目のハイライトを消したものを用意すれば、アニメキャラの目からハイライトを消すモデルが学習できないだろうか。

幸い、pix2pixのChainer実装が公開されている:

ので、これを改変することによって作成していく。

環境

  • Windows 10 64-bit
  • Python 3.5.3 (Anaconda 64-bit)
  • Chainer 2.0.1
  • CuPy 1.0.1
  • OpenCV 3 (demo.pyの結果表示で使っている)

データセットの用意

まず、データセットがなければ話にならないので用意する。

2017年1~3月に放送されていた*1アニメのキャプチャから、OpenCVのカスケード分類器とlbpcascade_animeface.xmlを利用してアニメ顔を抜き出した。元動画が1280×720 pxとした時に256×256 px以上の解像度をもっているものだけ抽出した。

(20210223追記: データセットを公開しました。次のブログ記事をご参照ください: アニメキャラの目のハイライト消しデータセット - にせねこメモ)

それから、切り出された画像を目で見て、誤検出やブラーがひどいものなどを取り除いた*2

さらに、そこからランダムに500枚抜き出し、それらのコピーに対してSAIを使って手でハイライトを塗りつぶした画像を用意した*3。中には塗りつぶす必要のない画像もあったが、だいたい一枚あたり1~10分程度かかった。特に、目にグラデーションがかかっていたり、タッチ線が入っていたり、ハイライトに大きなグローがかけられていたりするなどの複雑な目の処理をしている場合は難しく、時間がかかった。
用意したデータセットに含まれる画像の例は、下にある結果セクションの入力とground truth(正解画像)を参照。

コードの変更

先ほど挙げたpix2pixの実装においては、Facade datasetから、入力用のラベリング画像を12 chとして、変換ターゲットの画像を3 chのnumpy.arrayとして読み込み、これらによってトレーニングを行っている。これを変更し、入力・出力ともに3chのカラー画像を扱えるものにする。

画像をデータセットとして読み込むプログラムをimg_dataset.py、このデータセットの上で訓練するコードをtrain_dehighlight.pyとして用意した。

ここで、先ほどデータセットとして用意した画像500組のうち、9割(450枚)を訓練用として、残り1割(50枚)を評価用として分割している。

コード

GitHubに上げた。
github.com

デモ

python demo.py -g <GPU番号> -i <画像ファイルのパス>

レーニン

python train_dehighlight.py --epoch 400 --gpu <GPU番号> 

などとして実行する。GTX 1060使って18時間とかだった気がする。GPU使用しないと凄く時間がかかる。実際トレーニングをしてみて感じたことであるが、50,000 iter以上繰り返してもほとんど結果が改善しているようには見えなかった。

結果

176,000 iter目のトレーニング結果を次に挙げる。

入力

f:id:nixeneko:20170801195435p:plain

出力 (変換結果)

f:id:nixeneko:20170801195606p:plain

Ground truth (正解画像)

f:id:nixeneko:20170801195542p:plain
上手くいっているところを選んだとはいえ、割といい感じにハイライトが消えてる気がする(たのしい)。

その他変換結果

訓練セットにも評価セットにも含まれない画像についての変換結果を載せる。トレーニング176,000 iter目のモデルを用いた。左が入力、右が出力である。絵は自作なので絵柄にバリエーションがないのはご愛敬。
f:id:nixeneko:20170801205608p:plain
f:id:nixeneko:20170801210236p:plain
f:id:nixeneko:20170801205943p:plain
何となく塗りつぶした跡が見えるが、そこそこうまく動いてる様に思う。

改善点など

現状、入力画像のうち顔の領域の大きさが256×256 px程度である場合にはうまくいくが、大きさが大きかったり小さかったりするとあまりうまくいかない。つまり、スケール不変性がない。
これは、カスケード分類器で切り出した顔画像を256×256に変形してそれを用いてトレーニングしているためであると考えられ、data augmentationなどで訓練画像として様々なスケールの入力が与えられるようにすることである程度は対応できるのではないかと思う。

また、元画像で目のハイライトが小さい場合にはあまり効果が感じられない。これについてもdata augmentationで様々にスケールを変更したものを入力とすることで多少改善するとは思うが、どうなんだろう。

更に、訓練画像を抽出したカスケード分類器の特徴からか斜めや横向きの顔の画像が少なく、正面向きではうまく働くが、横向くとハイライトが消えなかったりする様である。

後、訓練されたモデルはほとんど顔しか知らないので、画面に文字などが被さってる場合も文字のあたりを目のハイライトとして勘違いするらしく、暗くなったりした。

他にも、訓練に使った画像の作品以外の画像だとハイライトが消えづらいということがあった。目の描き方は作品によって千差万別で、様々な絵柄に対応するためには訓練画像のバリエーションを増やすしかないように思う。何にせよ訓練データセットの作り方次第で改善の余地は大きそうな気がする。

*1:一部再放送を含む。

*2:つらい

*3:とてもつらい

ChainerCVでFaster R-CNNを使って顔検出してみる

(この記事は手探りで書いてるので大いに勘違いを含んでいる可能性があります。ご了承ください。)

VGG16をベースとしたFaster R-CNNを使って顔検出を行うというテクニカルリポートがあった。

ここでは、WIDER FACEという顔検出用のデータセットの上でトレーニングを行っている。

これを再現してみたい*1

手法

ChainerCVにFasterRCNNVGG16というそのものズバリのものがあるので、これを使ってトレーニングをしてみて、顔検出をやってみる。
今回はChainerCVの、Faster RCNNのサンプルコードchainercv/examples/faster_rcnn/を改変することによって行った。サンプルコードでは、PASCAL VOCデータセットに従ってトレーニングをし、20種類の物体が認識できる様だった。

ここでは、要するに、顔だけを検出するので、物体認識において検出できるクラスの数を1とすれば、とりあえずは動くはずである。
あんまりよくわかってないので、ハイパーパラメータはサンプルコードのものをほぼそのまま利用している。

環境

依存ライブラリ

  • Chainer 2.0.0
  • CuPy 1.0.0.1
  • ChainerCV 0.5.1
  • SciPy

ソース

github.com
なお、トレーニング済みのモデルはデータ容量が大きいのでリポジトリに含んでいないので、以下のリンクからダウンロードして trained_model/snapshot_model.npz に置くか、あるいは demo.py などを実行時に自動でダウンロードされるようになっている。


(2018-04-04追記)
chainercv v0.8.0ではトレーニング済みのモデルデータとして上の代わりに次のリンクのものを使う必要がある。

これは、chainercvがv0.6.0より後、v0.8.0までの間に、FasterRCNNVGG16の構造が変化してしまったからのようである。ついでにdemo.pyとtrain.pyをv0.8.0で動く様に修正した。
(追記終)

デモ

python demo.py [--gpu <gpu>] [--pretrained_model <model_path>] <image>.jpg

などとすれば、次のように顔と予測された矩形領域とスコアが表示される。
f:id:nixeneko:20170724160758p:plain
なお、faceと表示されてるのはPASCAL VOCの多クラス検出のコードを流用したためで、冗長になっている。

トレーニング手順

まず、顔検出用のWIDER FACEデータセットをダウンロードする。

ここから

  • Wider Face Training Images
  • Wider Face Validation Images
  • Face annotations

をダウンロードしてきて展開する。

そして、次のようにトレーニング向け/評価向けデータセットの、画像ファイルの含まれるフォルダとアノテーションデータのパスを指定して train.py を実行する。

python train.py --gpu=0 --train_data_dir="WIDER_train" --train_annotation="wider_face_split/wider_face_train.mat" --val_data_dir="WIDER_val" --val_annotation="wider_face_split/wider_face_val.mat"

やや細かい説明

chainercv/examples/faster_rcnn/のサンプルでは、Pascal VOCデータセットで物体認識をトレーニングしているので、ここで使われいてるVOCDetectionDatasetと互換性のある出力をするデータセットを、WIDER FACEデータセットを読み込ませるに際して用意している。
これが wider_face_dataset.py である。chainercv/chainercv/datasets/voc/voc_detection_dataset.pyを改変して作成した。


さて、このデータセットの上でトレーニングしたところ、RuntimeErrorが出たので、エラーが出た画像ファイルをトレーニング時に使用しない様に抜くようにしている。エラーがでたファイルは blacklist.txt にリストアップしてある。抜かない場合、トレーニング途中でlossがnanになってしまい失敗した。

なお、ライブラリのchainercv/links/model/faster_rcnn/utils/bbox2loc.pyの

    dh = xp.log(base_height / height)
    dw = xp.log(base_width / width)

を、

    _dh = xp.fmax(base_height / height, xp.finfo(xp.float32).eps)
    _dw = xp.fmax(base_width / width,  xp.finfo(xp.float32).eps)
    dh = xp.log(_dh)
    dw = xp.log(_dw)

などと変更して、logに与える値が0以下にならないようにしたらnanにならなくなった。

検出結果

緑がground truth、青が検出結果である。スコアが低いほど色が薄くなっている。

割とうまく行った例

f:id:nixeneko:20170724162340p:plain

うまく行かない例

f:id:nixeneko:20170724162432p:plain
ブラーのかかった顔、小さい顔などは2重に検出されていたり、検出されなかったりする。また、検出されてもずれがおおきかったりしているようだ。
見たところ、検出できる領域の最小があまり小さくなく(あまり小さい領域は検出できない)、動かす幅にも制限がある気がする。パラメータの設定次第では改善するのかもしれない? 割と大きなサイズをもった顔はちゃんと検出されているようだ。

*1:再現できているとは言っていない。

WindowsでChainerCVのサンプルを動かしFaster R-CNNをトレーニングしてみる

先日のChainer Meetupにて、ChainerCVというライブラリを知った。

Chainerの上で動作する、コンピュータビジョンタスクのためのディープラーニングライブラリとのことである。

このChainerCVで、Faster R-CNNによる物体検出が簡単に利用できるらしく、exampleを動かすことで試してみた。

環境

記事時点でのChainer, ChainerCVのバージョンは以下の通り。

  • Chainer: 2.0.0
  • ChainerCV: 0.5.1

ディープラーニングWindows使うとつまらない所でひっかかって割とつらいのでLinux使うべきだと思う。

インストー

  1. Chainer (Version 2.0.0)とCuPyをインストールする。
  2. 必要なライブラリが入ってなければpipで入れる
    • 必須: Cython, Pillow
    • 任意: Matplotlib, OpenCV
  3. ChainerCV (Version 0.5.1)をインストールする
    • Anacondaプロンプトを開き、次を実行する:
pip install chainercv

これでインストールは完了。


あと、(v0.5.1の) examplesを動かしてる時に、データをダウンロードするところでZeroDivisionErrorで止まったので、
chainercv/utils/download.py (C:\Anaconda3\Lib\site-packages\chainercv\utils\download.py などにある)の25行目

    speed = int(progress_size / (1024 * duration))

がdurationが0での時でも動くように書き換える:

    try:
        speed = int(progress_size / (1024 * duration))
    except ZeroDivisionError:
        speed = float('inf')

これは最新のリポジトリでは直ってるので、新しいバージョンがリリースされれば解決される問題であると思う。

examplesを動かしてみる

ソースコードをとってくる

git clone https://github.com/chainer/chainercv.git
cd chainercv
git checkout v0.5.1
cd examples/faster_rcnn

demo.py

demo.pyは物体検出を行い、結果を表示する。デフォルトでは、トレーニング済みのモデルをネットからダウンロードしてきてそれを利用する。

適当な画像hoge.jpgを用意して

python demo.py hoge.jpg

とすると、検出結果が表示される🐱
f:id:nixeneko:20170623002835p:plain

train.py

これはモデルのトレーニング用プログラムで、次のようにして実行する。0番目のGPUを使用する設定にしている*1

python train.py --gpu 0

しかし、Windowsでは実行すると次のようなエラーが発生する。

AttributeError: Can't pickle local object 'main.<locals>.transform'

このサイトによると、multiprocessingがサブプロセスを生成する方法がUnixWindowsで異なっていることがエラーの原因らしい。

Unixではデフォルトでforkによりサブプロセスを作るが、Windowsではforkは使えず、spawnによってサブプロセスを生成する。
このspawnでサブプロセスを生成する際、targetはpickableである(pickleでシリアライズできる)必要があるとのこと。特に、関数内に定義された関数はpickableでないので、モジュールのトップレベルに持ってくる必要がある。

エラーメッセージを見ると、train.pyのmain関数内に定義されたtransform関数が引っかかっているようである。なので、transform関数をトップレベルまで持ってくれば良さそうだ。

train.pyのfaster_rcnnの定義とtransform関数の定義をmain関数の定義の前に持ってくると、とりあえず動くようになった。faster_rcnnはtransformの中で参照されているため、これもトップレベルにもってくる。

faster_rcnn = FasterRCNNVGG16(n_fg_class=len(voc_detection_label_names),
                              pretrained_model='imagenet')
def transform(in_data):
    img, bbox, label = in_data
    _, H, W = img.shape
    img = faster_rcnn.prepare(img)
    _, o_H, o_W = img.shape
    scale = o_H / H
    bbox = transforms.resize_bbox(bbox, (H, W), (o_H, o_W))

    # horizontally flip
    img, params = transforms.random_flip(
        img, x_random=True, return_param=True)
    bbox = transforms.flip_bbox(
        bbox, (o_H, o_W), x_flip=params['x_flip'])

    return img, bbox, label, scale
    
def main():
    #(略)

この様に変更した後に、

python train.py --gpu 0

を実行し、トレーニングループを回し、終了するまで待つ。

トレーニングが終わると、デフォルトではresult/snapshot_model.npzにトレーニング後のモデルが保存される。
なので、それを指定してdemo.pyを実行する:

python demo.py --pretrained_model "result/snapshot_model.npz" hoge.jpg

すると、先ほどと同様の結果が出力された。
f:id:nixeneko:20170624093754p:plain

*1:GTX 1060で10時間ほど、CPUだと60日程度かかるらしい。

アウトラインがぶれるフォント

TrueType命令で遊ぶシリーズ。

  1. 初めてのTrueType命令: Windowsでは見えないフォントをつくる - にせねこメモ
  2. フォントサイズに合わせて回転するフォントを作る(1) - にせねこメモ
  3. PPEM・ポイントサイズを表示するフォント - にせねこメモ
  4. TrueType命令で三角関数(sin, cos)を計算する - にせねこメモ
  5. フォントサイズに合わせて回転するフォントを作る(2) - にせねこメモ
  6. TrueType命令で擬似乱数: 線形合同法 - にせねこメモ
  7. TrueType命令でビット演算 - にせねこメモ

擬似乱数を使って何か

前に擬似乱数を生成する関数を作ったので、それを使って何かやってみようというのが今回の目的である。
アウトラインをぶれさせてみたら、ランダムさが効果的に使えるのではないかと思うので、それをやってみる。

完成品

f:id:nixeneko:20170606181753p:plain
M+フォント(mplus-1p-regular.ttf)のアウトラインをガタガタにしている。フォントサイズによって文字の形が変わっているところに注目してほしい。

イラレとかで大きさを変えていくのを見ても面白い。


ダウンロード

なお、例によってTrueType命令を利用しているのでMacでは動かない。

実装

やっていることは単純で、各制御点の座標を読み出し、それに対して乱数生成関数によって生成した乱数を(適当にスケールして)足し合わせ、その座標に制御点を動かす。これをX, Y軸、および全ての制御点に対して行う。

さて、実際に実装してみる。

初期化

各種値の初期化は'prep'で行う。

まず、擬似乱数関数に与える初期seedを適当に初期化する。できるだけ乱雑になってほしかったのでMPPEM命令でPPEMを取得してそれを初期seedとした。これにより、フォントサイズによって乱数列が異なり、最終的に得られるアウトラインもフォントサイズ依存になる。
このseedをStorage Area 0番地に保存する。

次に、乱数のスケールに使用するscale factor sを準備する。
XまたはY軸方向に移動する距離の最大値(つまり、スケール後の乱数の絶対値の最大)をCVTテーブルの0番に書いておく。ここでは30 (FUnits)とした。
これをRCVTで読みだすとpixel単位になるので、それを pとする。
乱数の最大値は0x7FFFFFFFであるので、 s = \mathrm{0x7FFFFFFF} / pとする。
このとき、 p < 1の場合に、 sを計算する際にオーバーフローしてしまうため、それを防ぐために p := 1.0 \mbox{ (if } p < 1 \mbox{)}という処理を入れている。

読みだした乱数を sで割ることで、乱数(の絶対値)が0~pの範囲に含まれる様になる。
最後にscale factor  sをStorage Areaの1番地に保存している。

#seed初期化
PUSHB_1
 0
MPPEM   # 初期seed (>0)
WS      # StorageArea[0] = PPEM

#スケール係数の初期化
PUSHW_7 #0xFF|256.0|0xFF|256.0|0xFF|256.0|0x7F|
 255   # 0xFF
 16384 # 256.0
 255   # 0xFF
 16384 # 256.0
 255   # 0xFF
 16384 # 256.0
 127   # 0x7F
MUL     #0xFF|256.0|0xFF|256.0|0xFF|0x7F00|
ADD     #0xFF|256.0|0xFF|256.0|0x7FFF|
MUL     #0xFF|256.0|0xFF|0x7FFF00|
ADD     #0xFF|256.0|0x7FFFFF|
MUL     #0xFF|0x7FFFFF00|
ADD     #0x7FFFFFFF| #2147483647 = 0x7FFFFFFF
PUSHB_1
 0
RCVT    #0x7FFFFFFF|CVT[0]|
DUP     #0x7FFFFFFF|CVT[0]|CVT[0]|
PUSHB_1 #0x7FFFFFFF|CVT[0]|CVT[0]|1.0|
 64
LT      #0x7FFFFFFF|CVT[0]|CVT[0]<1.0|
IF      #0x7FFFFFFF|CVT[0]|  #if CVT[0]<1.0:
 POP     #0x7FFFFFFF|
 PUSHB_1 #0x7FFFFFFF|1.0|
  64
EIF
DIV     #0x7FFFFFFF/CVT[0]|
PUSHB_1 #0x7FFFFFFF/CVT[0]|1|
 1
SWAP    #1|0x7FFFFFFF/CVT[0]|
WS    #StorageArea[1] = 0x7FFFFFFF/CVT[0]

Storage Areaへの割り付け

番号 value
0 seed (PPEMで初期化)
1 scale factor

関数群

関数0: 乱数の生成

前に作った乱数関数(改良版の関数1)をもってきて、関数0とする。
関数番号以外は同一なので詳しくは前の記事を参照。

実行する度に1~0x7FFFFFFFの乱数列が返ってくる。

関数1: スケールした乱数の取得

関数0を呼び出して乱数を取得し、'prep'でStorage Areaの1番地に保存したscale factor  sで割ることでスケールする。
この際、取得した乱数を1ビット左シフトし、取得した乱数の上から2ビット目を符号ビットとして扱うことで、負の数も得られる様にしている。
最終的に得られる乱数は、最大動き幅pについて-p~pの範囲となるはず。

/* Function 1: returns scaled random value */
PUSHB_1
 1
FDEF          /* ..|      *//* ← initial stack */
PUSHB_1
 0
CALL          /* ..|rand| */
DUP
ADD           /* ..|rand<<1| */ /* to make it signed */
PUSHB_1
 1
RS            /* ..|rand<<1|s| */ /*s = StorageArea[1], scaling factor */
DIV           /* ..|(rand<<1)/s| */
ENDF

関数2: 指定された制御点をランダムな大きさだけ動かす

指定された制御点番号kに対応する制御点のX座標、Y座標を取得し、それに対して関数1で取得したスケール済み乱数を足しあわせ、計算された座標にSCFSで移動している。

/* Function 2: moves the control point k         */
/* initial stack ..| k |       k: 編集する制御点番号                           */
/* final stack   ..|                                                      */
PUSHB_1
 2
FDEF     /* ..|k| */
DUP      /* ..|k|k| */
DUP      /* ..|k|k|k| */
DUP      /* ..|k|k|k|k| */
SVTCA[x-axis]                                  /* X座標に設定 */
GC[cur]  /* ..|k|k|k|x_k| */
PUSHB_1
 1
CALL     /* ..|k|k|k|x_k|rand| */ /* get scaled random value*/
ADD      /* ..|k|k|k|x_k+rand| */
SCFS     /* ..|k|k| */        /* 制御点kのX座標を x_k + rand に */

SVTCA[y-axis]                                  /* Y座標に設定 */
GC[cur]  /* ..|k|y_k| */
PUSHB_1
 1
CALL     /* ..|k|y_k|rand| */
ADD      /* ..|k|y_k+rand| */
SCFS     /* ..| */            /* 制御点kのY座標を y_k + rand に */

ENDF

関数3: カウントアップしつつ関数2を呼び出す

LOOPCALLで呼び出される用の関数。スタックトップの番号(カウンター)を引数として関数2を呼び出し、最後にカウンターを1増やす。

/* function 3: call func2 with value n and increment n */
/* initial stack: ..|n|   */
/* final stack:   ..|n+1| */
PUSHB_1
 3
FDEF      /* ..|n| */ /* repeat n times */
DUP       /* ..|n|n| */
PUSHB_1
 2
CALL      /* ..|n| */ /* moves the point n */
PUSHB_1   /* ..|n|1| */
 1
ADD       /* ..|n+1| */
ENDF

関数4: LOOPCALLで関数3を指定回数実行する

グリフから呼び出され、指定回数(=制御点の個数回)関数3を実行する。関数3では呼び出す度にスタックトップの値(カウンター)を1ずつ増やしていくので、すべての制御点に対して操作が行われることになる。

/* function 4: call func3 n times */
/* initial stack: ..|n|  n: number of repetition */
/* final stack:   ..|  */
PUSHB_1
 4
FDEF      /* ..|n| */ /* n: num of points in the glyph */
PUSHB_1   /* ..|n|0| */ /* initialize the counter by 0 */
 0
SWAP      /* ..|0|n| */
PUSHB_1   /* ..|0|n|3| */
 3
LOOPCALL  /* ..| */ /* call function 3, n times */
ENDF

グリフ

関数4を、グリフから次のように呼び出す。
例えば、グリフに含まれる制御点の個数が10個であれば、

PUSHB_2
 10
 4
CALL

となる。

すべてのグリフへの適用

'cvt ', 'fpgm', 'prep'を設定して書き出したフォントAmovepointsrandom.ttfをttxを使ってXMLファイル(.ttx)にダンプする。

ttx Amovepointsrandom.ttf

その後、次のPythonプログラムを実行し、Amovepointsrandom.ttxのすべてのグリフに対して関数を呼び出すTrueType命令を適用した結果(Amovepointsrandom-out.ttx)を得る。
やっていることは、グリフ毎に制御点の個数を調べ、その値を組み込んだ関数呼び出しの命令を付加するという感じ。

#!/usr/bin/env python3
# conding: utf-8

import xml.etree.ElementTree as ET

INFILE = "Amovepointsrandom.ttx"
OUTFILE = "Amovepointsrandom-out.ttx"
xmltree = ET.parse(INFILE)
xmlroot = xmltree.getroot()

for glyph in xmlroot.find('glyf').findall('TTGlyph'):
    cnt = 0
    for contour in glyph.findall('contour'):
        cnt += len(contour.findall('pt'))
    if cnt > 0:
        prog ="""
          PUSHB[ ]      /* 2 values pushed */
          {} 4
          CALL[ ]       /* CallFunction */
          """.format(cnt)
        glyph.find('instructions').find('assembly').text = prog

with open(OUTFILE, 'w') as w:
    w.write('<?xml version="1.0" encoding="UTF-8"?>\n')
    xmlstr = ET.tostring(xmlroot, method='xml', encoding="unicode")
    #xmltree.write(OUTFILE)
    w.write(xmlstr)

最後に得られた.ttxファイルをttxで.ttfに変換する。

ttx Amovepointsrandom-out.ttx

これによって生成されるAmovepointsrandom-out.ttfが完成品である。

コード

'cvt '

number value
0 30

'fpgm'

PUSHB_1
 0
FDEF
PUSHB_1
 0
RS
DUP
PUSHW_2
 30000
 14488
ADD
DUP
ROLL
SWAP
PUSHW_1
 4096
MUL
DIV
ROLL
ROLL
PUSHB_1
 3
CINDEX
PUSHW_1
 4096
MUL
MUL
SUB
PUSHW_3
 4096
 30000
 18271
ADD
MUL
MUL
SWAP
PUSHW_2
 3399
 4096
MUL
MUL
SUB
DUP
PUSHB_1
 0
GT
IF
ELSE
PUSHW_7
 255
 16384
 255
 16384
 255
 16384
 127
MUL
ADD
MUL
ADD
MUL
ADD
ADD
EIF
DUP
PUSHB_1
 0
SWAP
WS
ENDF
PUSHB_1
 1
FDEF
PUSHB_1
 0
CALL
DUP
ADD
PUSHB_1
 1
RS
DIV
ENDF
PUSHB_1
 2
FDEF
DUP
DUP
DUP
SVTCA[x-axis]
GC[cur]
PUSHB_1
 1
CALL
ADD
SCFS
SVTCA[y-axis]
GC[cur]
PUSHB_1
 1
CALL
ADD
SCFS
ENDF
PUSHB_1
 3
FDEF
DUP
PUSHB_1
 2
CALL
PUSHB_1
 1
ADD
ENDF
PUSHB_1
 4
FDEF
PUSHB_1
 0
SWAP
PUSHB_1
 3
LOOPCALL
ENDF

'prep'

PUSHB_1
 0
MPPEM
WS
PUSHW_7
 255
 16384
 255
 16384
 255
 16384
 127
MUL
ADD
MUL
ADD
MUL
ADD
PUSHB_1
 0
RCVT
DUP
PUSHB_1
 64
LT
IF
POP
PUSHB_1
 64
EIF
DIV
PUSHB_1
 1
SWAP
WS