コミケ合わせで新刊作りました。ただし私は今回はサークル参加を申し込んでいないのでここで公開します。
電柱や電線をスケッチしたものです。
本編
よく電気屋などで小型商品にくっつけてある万引き防止タグ、検知方式によっていろいろな形があるのだが、長方形で立体的に厚みがある硬いタグを分解してみた。
このタグは使い捨てで、粘着テープが裏についていて、商品に貼りつけられる。このタグがゲートを通ると検知され、警報が鳴る。会計時に無効化処理を行うことで、会計を済ませた商品がゲートで反応することを防いでいるらしい。
さて、分解してみると、底面に固定された金属板1枚と、金属の薄いフィルム2枚が入っている。フィルムの方は固定されていなくて、プラスチックで囲われたタグの内側の空間の中で動くことができるようになっている。タグの厚みはこのフィルムが動く空間を作るためのもので、タグが固いのも同じ理由だろう。
さて、随分シンプルな構造であるが、いったいどういう原理で動作するのだろうか。
調べてみると、このタグはAcousto-magnetic systems (音響磁気方式)という方式のものらしい。Wikipediaに解説がある。
それによると、防犯ゲートの送信アンテナから送信されるパルスによって中に入っている金属フィルムが共振し、その共振によって発生する電波をゲートが受信するとアラームを起動するということらしい。
また、一番下の金属板が磁化されていると有効になるが、磁化が解除されると無効化され、ゲートに反応しなくなるとのことである。
つまり、解除機の無効化処理とは、実際には磁化の解除を行っているのだろう。
もしかして、解除されたタグについても、磁石を隣に置いておけばゲートに反応するようになるのかもしれない。
しかし、こんなに簡単な仕組みで防犯タグが実装されてるのを見て感心してしまった。解除もできる様になってるし。
イオニア数字ネイティヴではないので、イオニア数字を書く場合にはいちいち調べて書かないといけないのだけれど、実際面倒なので、(La)TeXマクロにしてLaTeXなどで書く際に簡単に変換できるようにしようというのが今回の目的。
イオニア数字というのは、古代ギリシャで使われた数値の書き方であって、ギリシャ文字を使って数値を表現するものである。一般にギリシャ数字とも呼ばれるとのこと。
詳しくはWikipediaなどを参照: ギリシアの数字 - Wikipedia
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以上の書き方はさまざまであるらしい。
この実装で用いたものは次のようなものである:
The largest number named in Ancient Greek was the myriad myriad (written MM) or hundred million.
https://en.wikipedia.org/wiki/Myriad
まず、一~千の位の数字を変換することを考える。
これは、元の数をnとすると、各位の数字は
となる。次にこれを\ifcaseを使ってギリシャ数字の各文字に変換する。
次に一万以上の数値について。
元の数値を10000で割ったものについて、その値をΜの上に積み重ねて表示する。数値の表示は一~千の位の数字に変換するマクロを共用している。
また、このとき、10000がΜとなるように条件分岐を行っている。
一億以上の数に対応するために再帰っぽい感じで書いてしまったのだが、どうせ処理系がまでしか対応してないので、ごちゃごちゃするだけであまりメリットなかったように思う。
でも、この規則だと困ったことになる、気がする。#TeX pic.twitter.com/Zx9I3phPYh
— ZR-TeXnobabbler(∉∅) (@zr_tex8r) 2017年8月10日
気が向いたら修正する……。
pix2pixというモデルがある。入力画像と、それと一対一対応する変換ターゲットの画像を用意すると、その間の変換を自動で学習してくれるというものである。
元論文はこれ:
この論文内では、モノクロ写真に着色するだとか、線画に色を付ける、航空写真から地図を生成する、ラベリング画像から写真を復元する、などが行われている。要するに、何らかの画像のペアを用意すれば、ペア間の画像変換が学習できるっぽい。
それで、入力画像としてアニメキャラの顔の画像を用意し、出力画像として入力画像から目のハイライトを消したものを用意すれば、アニメキャラの目からハイライトを消すモデルが学習できないだろうか。
幸い、pix2pixのChainer実装が公開されている:
ので、これを改変することによって作成していく。
まず、データセットがなければ話にならないので用意する。
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以上繰り返してもほとんど結果が改善しているようには見えなかった。
上手くいっているところを選んだとはいえ、割といい感じにハイライトが消えてる気がする(たのしい)。
現状、入力画像のうち顔の領域の大きさが256×256 px程度である場合にはうまくいくが、大きさが大きかったり小さかったりするとあまりうまくいかない。つまり、スケール不変性がない。
これは、カスケード分類器で切り出した顔画像を256×256に変形してそれを用いてトレーニングしているためであると考えられ、data augmentationなどで訓練画像として様々なスケールの入力が与えられるようにすることである程度は対応できるのではないかと思う。
また、元画像で目のハイライトが小さい場合にはあまり効果が感じられない。これについてもdata augmentationで様々にスケールを変更したものを入力とすることで多少改善するとは思うが、どうなんだろう。
更に、訓練画像を抽出したカスケード分類器の特徴からか斜めや横向きの顔の画像が少なく、正面向きではうまく働くが、横向くとハイライトが消えなかったりする様である。
後、訓練されたモデルはほとんど顔しか知らないので、画面に文字などが被さってる場合も文字のあたりを目のハイライトとして勘違いするらしく、暗くなったりした。
他にも、訓練に使った画像の作品以外の画像だとハイライトが消えづらいということがあった。目の描き方は作品によって千差万別で、様々な絵柄に対応するためには訓練画像のバリエーションを増やすしかないように思う。何にせよ訓練データセットの作り方次第で改善の余地は大きそうな気がする。
(この記事は手探りで書いてるので大いに勘違いを含んでいる可能性があります。ご了承ください。)
VGG16をベースとしたFaster R-CNNを使って顔検出を行うというテクニカルリポートがあった。
ここでは、WIDER FACEという顔検出用のデータセットの上でトレーニングを行っている。
これを再現してみたい*1。
ChainerCVにFasterRCNNVGG16というそのものズバリのものがあるので、これを使ってトレーニングをしてみて、顔検出をやってみる。
今回はChainerCVの、Faster RCNNのサンプルコードchainercv/examples/faster_rcnn/を改変することによって行った。サンプルコードでは、PASCAL VOCデータセットに従ってトレーニングをし、20種類の物体が認識できる様だった。
ここでは、要するに、顔だけを検出するので、物体認識において検出できるクラスの数を1とすれば、とりあえずは動くはずである。
あんまりよくわかってないので、ハイパーパラメータはサンプルコードのものをほぼそのまま利用している。
依存ライブラリ
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
などとすれば、次のように顔と予測された矩形領域とスコアが表示される。
なお、faceと表示されてるのはPASCAL VOCの多クラス検出のコードを流用したためで、冗長になっている。
まず、顔検出用のWIDER FACEデータセットをダウンロードする。
ここから
をダウンロードしてきて展開する。
そして、次のようにトレーニング向け/評価向けデータセットの、画像ファイルの含まれるフォルダとアノテーションデータのパスを指定して 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、青が検出結果である。スコアが低いほど色が薄くなっている。
ブラーのかかった顔、小さい顔などは2重に検出されていたり、検出されなかったりする。また、検出されてもずれがおおきかったりしているようだ。
見たところ、検出できる領域の最小があまり小さくなく(あまり小さい領域は検出できない)、動かす幅にも制限がある気がする。パラメータの設定次第では改善するのかもしれない? 割と大きなサイズをもった顔はちゃんと検出されているようだ。
*1:再現できているとは言っていない。
先日のChainer Meetupにて、ChainerCVというライブラリを知った。
Chainerの上で動作する、コンピュータビジョンタスクのためのディープラーニングライブラリとのことである。
このChainerCVで、Faster R-CNNによる物体検出が簡単に利用できるらしく、exampleを動かすことで試してみた。
記事時点でのChainer, ChainerCVのバージョンは以下の通り。
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')
これは最新のリポジトリでは直ってるので、新しいバージョンがリリースされれば解決される問題であると思う。
git clone https://github.com/chainer/chainercv.git cd chainercv git checkout v0.5.1 cd examples/faster_rcnn
demo.pyは物体検出を行い、結果を表示する。デフォルトでは、トレーニング済みのモデルをネットからダウンロードしてきてそれを利用する。
適当な画像hoge.jpgを用意して
python demo.py hoge.jpg
とすると、検出結果が表示される🐱
これはモデルのトレーニング用プログラムで、次のようにして実行する。0番目のGPUを使用する設定にしている*1。
python train.py --gpu 0
しかし、Windowsでは実行すると次のようなエラーが発生する。
AttributeError: Can't pickle local object 'main.<locals>.transform'
このサイトによると、multiprocessingがサブプロセスを生成する方法がUnixとWindowsで異なっていることがエラーの原因らしい。
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
すると、先ほどと同様の結果が出力された。
*1:GTX 1060で10時間ほど、CPUだと60日程度かかるらしい。
TrueType命令で遊ぶシリーズ。
前に擬似乱数を生成する関数を作ったので、それを使って何かやってみようというのが今回の目的である。
アウトラインをぶれさせてみたら、ランダムさが効果的に使えるのではないかと思うので、それをやってみる。
M+フォント(mplus-1p-regular.ttf)のアウトラインをガタガタにしている。フォントサイズによって文字の形が変わっているところに注目してほしい。
イラレとかで大きさを変えていくのを見ても面白い。
Adobe Illustrator CS6でこんな感じ。 pic.twitter.com/FqEdumiHSz
— にせねこ (@nixeneko) 2017年6月6日
やっていることは単純で、各制御点の座標を読み出し、それに対して乱数生成関数によって生成した乱数を(適当にスケールして)足し合わせ、その座標に制御点を動かす。これをX, Y軸、および全ての制御点に対して行う。
さて、実際に実装してみる。
各種値の初期化は'prep'で行う。
まず、擬似乱数関数に与える初期seedを適当に初期化する。できるだけ乱雑になってほしかったのでMPPEM命令でPPEMを取得してそれを初期seedとした。これにより、フォントサイズによって乱数列が異なり、最終的に得られるアウトラインもフォントサイズ依存になる。
このseedをStorage Area 0番地に保存する。
次に、乱数のスケールに使用するscale factor を準備する。
XまたはY軸方向に移動する距離の最大値(つまり、スケール後の乱数の絶対値の最大)をCVTテーブルの0番に書いておく。ここでは30 (FUnits)とした。
これをRCVTで読みだすとpixel単位になるので、それをとする。
乱数の最大値は0x7FFFFFFFであるので、とする。
このとき、の場合に、を計算する際にオーバーフローしてしまうため、それを防ぐためにという処理を入れている。
読みだした乱数をで割ることで、乱数(の絶対値)が0~pの範囲に含まれる様になる。
最後にscale factor を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]
番号 | value |
---|---|
0 | seed (PPEMで初期化) |
1 | scale factor |
関数0を呼び出して乱数を取得し、'prep'でStorage Areaの1番地に保存したscale factor で割ることでスケールする。
この際、取得した乱数を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
指定された制御点番号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
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
グリフから呼び出され、指定回数(=制御点の個数回)関数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が完成品である。
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
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