2015年8月15日土曜日

canvasでvideoを描画

html5のcanvasで動画を扱い方を調べつつ書いたもの。
ブラウザの違いでめんどいことは多いがなれればそれほどでもないかも。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
</head>

<body>
<canvas id="cnvs" width="640" height="480"></canvas>
<input id="play-or-pause" type="button" value="loading">
<script>
(function() {
  var canvas = document.getElementById("cnvs");
  var ctx = canvas.getContext("2d");
  var button = document.getElementById("play-or-pause");
  var vid = document.createElement("video");
  vid.autoplay = true;
  vid.loop = true;
  if (vid.canPlayType("video/mp4").length === 0) {
    return false;    
  }
  vid.addEventListener("loadeddata", function(){
      vid.pause();
      draw();
      button.value = "play";
    }, false);
  vid.src = "susie.mp4";

  function draw() {
    for (var dy = -118; dy < canvas.height; dy += 144) {
      for (var dx = -160; dx < canvas.width; dx += 192) {
        ctx.drawImage(vid, dx, dy);
      }
    }
  }

  button.onclick = function() {
    var intervalId = null;
    if (button.value === "pause") {
      clearInterval(intervalId);
      vid.pause();
      button.value = "play";
    } else if (button.value === "play"){
      intervalId = setInterval(draw, 1000 / 30);
      vid.play();
      button.value = "pause";
    }
  };
})();
</script>
</body>
</html>



前回と同じくボタンで再生/一時停止。

手元の環境(Windows10,64bit)で確認したところ、Chrome/Firefoxではこのページを開いた場合、0フレームが表示された状態でpauseがかかるが、Edge/IE11では最初のフレームは表示されず真っ黒(Edge)か透明(IE11)である。
Edgeはwebkitと挙動が違えばバグらしいのでEdgeのはバグなんだろう(多分)。

追記:
スマホ(Android)のChromeで見てみるとEdgeと同じ挙動になった。
ってことはバグにはならない可能性のほうが高いのかな?
ここらへんはっきりさせて欲しい。

canvasで2Dアニメーション

html5のcanvasアニメーションを調べながら書いたコード。
とりあえず画像の表示と移動、回転を試したかっただけ。
手持ちの素材を適当に使ったらかなりくるった出来上がりになってしまったが気にしてはいけない。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
</head>

<body>
<canvas id="cnvs" width="640" height="480"></canvas>
<input id="play-or-pause" type="button" value="loading">
<script>
/*
  background: 背景のURL
  flyingObjects: 移動物のURLの配列
  num: 移動物の数
  fps: フレームレート
  xmax:1フレーム当たりの移動物のx軸方向の最大移動量(px)
  ymax:1フレーム当たりの移動物のy軸方向の最大移動量(px)
  tmax:1フレーム当たりの移動物の最大回転角(deg) 
*/
(function(background, flyingObjects, num, fps, xmax, ymax, tmax) {

  var canvas = document.getElementById("cnvs");
  var ctx = canvas.getContext("2d");
  var button = document.getElementById("play-or-pause");

  const PI2 = Math.PI * 2;

  var loader = (function() {
    var count = 0;
    return function() {
      if (++count > num) {
        button.value = "play";
        draw();  
      }
    }
  })();

  var bg = new Image();
  bg.src = background;
  bg.onload = loader;

  var fo = new Array(num);
  for (var i = 0; i < num; i++) {
    fo[i] = new Image();
    fo[i].params = {
      speedx: Math.random() * 2 * xmax - xmax,
      speedy: Math.random() * 2 * ymax - ymax,
      dstx: Math.random() * canvas.width,
      dsty: Math.random() * canvas.height,
      theta: ((Math.random() * 2 * tmax - tmax) * Math.PI / 180),
      angle: 0,
    };
    fo[i].src = flyingObjects[Math.random() * flyingObjects.length | 0];
    fo[i].onload = loader;
  }

  function draw() {
    ctx.drawImage(bg, 0, 0, canvas.width, canvas.height);
    for (var i = 0; i < num; i++) {
      var f = fo[i];
      var p = f.params;
      var cx = f.naturalWidth / 2;
      var cy = f.naturalHeight / 2;

      ctx.save();
      ctx.translate(p.dstx + cx, p.dsty + cy);
      ctx.rotate(p.angle);
      ctx.drawImage(f, -cx, -cy);
      ctx.restore();

      p.dstx += p.speedx;
      p.dsty += p.speedy;
      p.angle += p.theta;

      if (p.dstx > canvas.width) {
        p.dstx = -f.naturalWidth;
      }
      if (p.dstx < -f.naturalWidth) {
        p.dstx = canvas.width - 1;
      }
      if (p.dsty > canvas.height) {
        p.dsty = -f.naturalHeight;
      }
      if (p.dsty < -f.naturalHeight) {
        p.dsty = canvas.height - 1;
      }
      if (p.angle < -PI2) {
        p.angle += PI2;
      }
      if (p.angle > PI2) {
        p.angle -= PI2;
      }
    }
  };

  button.onclick = (function() {
    var intervalId = null;
    var interval = 1000 / fps | 0;
    return function() {
      if (button.value === "pause") {
        clearInterval(intervalId);
        button.value = "play";
      } else if (button.value === "play") {
        intervalId = setInterval(draw, interval);
        button.value = "pause";
      }
    };
  })();

})("images/soccer.jpg",
   ["images/saturn.png",
    "images/pig.png",
    "images/heart.png",
    "images/japan.gif"],
    50, 50, 3, 5, 30);
</script>
</body>
</html>




ボタンを押せば再生/一時停止します。

2015年8月7日金曜日

PHPでenum(のようなもの)

PHPでファイル送信機能付きのメールフォームを書いていた時のお話し。

入力フォームなんだから当然validationとかもしなければいけないわけで、いろいろやるクラスを別に作って結果を定数で返すようにした。
こういうときCだったらenumを使うのが多分普通なのですが、PHP5にはenumはないんですな。

enumよりももっとスマートな方法もあるのかもしれないけど、パッとは思いつかない。
それにJavaでもC#でも、switch文は頑なに拒否するPythonですらenumは標準モジュールで用意されてるんですから、やはりenumはいいものなのでしょう。

ググってみたら「enum実装しました!」とかいう人も当然いるわけですが、しかしenum程度でわざわざ外部ライブラリとか大げさな気もする。
で、自分なりの方法を模索するわけです。

とりあえずぱっと思いつくのはこれですな。
class RetCode {

    const VALID_POST = 0;
    const INVALID_POST = 1;
    const SHORTAGE_POST = 2;
    const INVALID_FILE_UPLOADED = 3;
      ・
      ・
      ・ 
}

自分で連番ふって定数定義…はい、こんなもんenumじゃありません。
自動で重複しない数値を振ってくれなければenumの替わりとは絶対認められないに決まってます。

で、PHPのドキュメント5分ほど眺めてこうすることにしました。

class RetCode {

    private static $ret = [
        "VALID_POST",
        "INVALID_POST",
        "SHORTAGE_POST",
        "INVALID_FILE_UPLOADED",
            ・
            ・
            ・
    ];

    public static function get_retcode() {
        return array_flip(self::$ret);
    }
}

keyなしで配列定義してarray_flip()してやれば、重複しない値をもつ配列になりますね。
定義されてないkeyを使おうとすればNotice Errorでるから、typo防止にもなるっちゃなりますし。

Cのenumとそれほど手間も変わらないんで、お手軽につかう分にはいいんじゃないかと思います。

2015年8月6日木曜日

JavaScriptで要素の高さを揃える

随分長いことほっぽってたブログですが、何か書きたくなってきたので再開。

といっても、最近は動画関連のことはほとんどやってないので、そっちのほうの需要(あるのか?)は満たせないと思いますが。

で、最近の筆者は普段はhtmlとかJavaScriptとかPHPとかを書いたり書かなかったりしています。
今回のお題はそちらの方面です。

本題

さて、今回のネタは次のような感じです。


こんな感じの表示を
こうしたい。

「要素 高さ 揃える」とかでググればjQuery使ったやり方がたくさんヒットするわけですが、IE8のサポートも残り半年を切ってる昨今では、別にアニメーションさせるわけでもないのにjQuery使うのってダサいらしいので今回はパス。

jQueryを使わずにこれをやるheightLine.jsなるライブラリもあるようですが、実際に中身を見てみたところどうもよく理解できない。書かれたのが8年前ではブラウザ環境とかもずいぶん違うだろうしなあ。

というわけで、自分で書いてみることにしました。


まず高さを揃えたい各要素(element)の処理前の高さは、element.offsetHeightの値でわかるので、各要素のoffsetHeightの最大値を求める。
var elems = document.getElementsByClassName(className); //とりあえずクラス名で対象要素を取得
var maxHeight = 0;
for (var i = 0; i < elems.length; i++) {
    maxHeight = Math.max(maxHeight, elems[i].offsetHeight);
}
これで揃える高さが求まりました。

で、offsetHeightは要素のpadding(上下)+border(上下)+heightなので、maxHeightからpaddingとborderの値を引いた値を要素のstyle.heightに設定してやればいいわけですが、ここで少し問題。

要素のpaddingやborderを<style>タグや外部CSSファイルで指定している場合は、element.style以下のプロパティは全部空("")なのですね。

かと言って高さを揃えたい各要素のborder+paddingは(上の画像のように)すべて同じとは限らないわけで、ハードコーディングするのも問題ありすぎです。

さてどーしようとちょっと考えたらこうなりました。
for (i = 0; i < elems.length; i++) {
    var elem = elems[i];
    elem.style.height = "0";
    var height = maxHeight - elem.offsetHeight;
    elem.style.height = height + "px";
}
offsetHeight==padding(上下)+border(上下)+height ですから、height==0のときoffsetHeight==padding(上下)+border(上下)になる。

これでstyle.heightに入れる値はmaxHeightとoffsetHeightの差をとればよいことになります。 なんかインチキ臭いけど、簡単だし実用上は問題ない(多分)。

実際に使うときはこんな感じになります。

2013年8月25日日曜日

Transpose

transposeとは転置行列、つまり行列の行と列をひっくり返して新しい行列をつくることです。
画像をひとつの行列として見る場合、これをtransposeすると以下のようになります。


transeposeしたものに更にFlipHorizontal()をかければTurnRightになりますし、

FlipVerticalをかければTurnLeftになります。

VapourSynthにstd.Transpose()があるのに、TurnLeft/TurnRightがないのはこのためです。

さて、このtransposeは画像処理においては意外に重要な役割を果たします。
transposeすると、縦と横がひっくり返るため、フィルタ処理において縦と横でそれぞれ同じような処理をする場合(resizerとかconvolutionとか)、一方向の処理だけ書けば済むのです。
特にSSEとかCUDAとかでベクトル演算を利用するならば縦方向処理のほうが速いことが多いので、transposeの重要性は高まります。

というわけで、これは画像処理を嗜むものであれば自前でそこそこ速いやつを書けなければならんと思い、書いてみることにしました。

書いてみてわかったこと

1. SSE2を使う場合、8x8単位で処理するよりも16x16単位で処理するほうが速い
xmmレジスタの数的に32bit(8個)だと16x16はいろいろとペナルティが大きそうなのですが、それでも16x16のほうが速いようです。
もし64bitであれば、xmmレジスタの数は16個に増えるので、さらに有利になりそうな気がします。

2. VC++10は俺の書いたコードをろくにloop unrollしてくれない
このコードを書いた当初は配列とforループを使ったもうちょっとスッキリしたコードでした。でも、コンパイラがループアンロールしてくれないの...orz
手動アンロールしただけで数十fpsスピードが上がったのを目の当たりにしたときはもうなんというか力が抜けてしまいました。
こんな糞コンパイラが世にはびこっているせいでC99の普及が遅れたなんて、もうなんというかね...

2013年8月9日金曜日

Alpha Max Plus Beta Min Algorithm

画像処理フィルタを書いていると頻出する計算に次のようなものがあります。
X = sqrt(A * A + B * B)
要するに二つの数の二乗の和の平方根を求めるわけです。
普通にC等で書くならば特に問題ないわけですが、いざSIMD化しようとなるととたんに面倒になります。

まず整数演算の場合、掛け算は16bit同士からしかできません。
よって8bit同士の掛け算をしたい場合は、両者をまず16bitに変換しなければなりません。
__m128i xmm0 = _mm_setr_epi8(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
__m128i zero = _mm_setzero_si128()
__m128i xmm1 = _mm_unpackhi_epi16(xmm0, zero);
xmm0 = _mm_unpacklo_epi16(xmm0, zero);
更に、16bit同士の掛け算は最大で32bitになる可能性があるため(65,535 x 65,535 = 4,294,836,225)、出力は32bitになります。
しかも素直に32bitで出力してくれるような命令は存在せず、上位16bitと下位16bitがわかれた状態で出力されるか、積和計算を利用するかしなければなりません。
__m128i xmm2 = _mm_mullo_epi16(xmm0, xmm0);
__m128i xmm3 = _mm_mulhi_epi16(xmm0, xmm0);
xmm0 = _mm_unpacklo_epi16(xmm2, xmm3);
xmm2 = _mm_unpackhi_epi16(xmm2m xmm3);

xmm3 = _mm_unpackhi_epi16(xmm1, zero);
xmm1 = _mm_unpacklo_epi16(xmm1, zero);
xmm1 = _mm_madd_epi16(xmm1, xmm1);
xmm3 = _mm_madd_epi16(xmm3, xmm3);
ただ二乗するだけで、最初は128bitのXMMレジスタ一本で済んでいたのがいつのまにやら4本まで膨れあがり、コードもすでに12行です。
この操作をもう一つの数に対しても行い全部で8本になったレジスタを足しあわせて再び4本のレジスタになったあと、今度はこれらの平方根を求めなければなりません。
しかし、平方根を求める命令は整数演算にはないので、32bit整数を単精度浮動小数点に変換してから行うことになります。
__m128 flt0 = _mm_cvtepi32_ps(xmm0)
flt0 = _mm_sqrt_ps(flt0)
xmm0 = _mm_cvtps_epi32(flt0)
これを4回行った後、最後に再び8bit整数に戻します。
xmm0 = _mm_packs_epi32(xmm0, xmm1);
xmm1 = _mm_packs_epi32(xmm2, xmm3);
xmm0 = _mm_packus_epi16(xmm0, xmm1);
なんでたかがsqrt(A*A+B*B)を行うためにこんな辛い目にあわねばならないのでしょう。
しかもこれで計算できるのは一度にたったの16個です(SSE/SSE2の場合)。
普通にCで書いて16回計算するのとスピードは変わらないか下手すると遅くなってしまいます。
例えばTEdgeMaskはtriticalプラグインでは珍しくクリティカルパートがSIMD化されていないのですが、おそらくはこの計算が出てくるためにやらなかったのでしょう。

というわけで、もしどうしてもスピードがほしいなら普通はこんなに馬鹿正直な計算はしません。

sqrt(A*A+B*B) -> abs(A)+abs(B)

正直、手抜きにも程があるとは思います。
しかし、実用上これで問題が出るケースは意外と少ないので、これで済ませてしまったりするのですね。

とまあ、コストとパフォーマンスの兼ね合いという人類普遍のテーマについてつらつらと考えていたところ、prunedtree氏が"alpha max plus beta min algorithm"を教えてくれました。

sqrt(A*A+B*B) -> max(abs(A),abs(B))*alpha+min(abs(A),abs(B))*beta

alphaとbetaの選び方によっては最大誤差3.96%、平均誤差1.3%……これはイイ!
流石はgradfun2dbやFRfunシリーズを書いた人です。こういうテクニックをよく知ってらっしゃる。
抜け道というものは探せばあるものなんだなぁと思いました。


ちなみにprunedtree氏は、昔はmarcFDという名前でした。
なんでも若気の至りによって犯した過ち(DeenやaWarpSharpのことだと思われる)を悔いて改名したそうです。

2013年8月5日月曜日

Hysteresis mask

Doom9のほうで「GenericFiltersにmt_hysteresisの同等品を追加してくれないか?あれはhandyでいいものだよ」というリクエストがありました。

mt_histeresisねぇ…そりゃ、あんたにとってはhandyかもしれんが、書く方の身にもなれよ。
あれはIIRフィルタだからSIMD化も出来ないし、遅いものしか書けないんじゃないかなぁ。
だいたいあれってgenericなfilterじゃないだろ、specificなfilterじゃねえか。

てなことをブツブツ言いながらもとりあえず書いてみることにしたわけです。

ちなみにmt_hysteresisは、二値化する際の閾値を変えた二つのエッジマスクからノイズの少ない一つのエッジマスクを作り出すフィルタです。

まずはアルゴリズムやロジックを理解するため、avisynthで同等のものが書けるかどうかを試してみます。
vapoursynthプラグインは9/10/16bit対応とか可変解像度/フォーマットとかのことも考えないといけないので、avisynthよりも面倒なのです。
で、Masktools1の方を参考に(Masktools2のコードはmanao氏の趣味なのかテンプレートやSTL使いまくりのメタメタコードなのでC++がよくわからない自分には理解できない)書いてみたわけですが…

再帰を使うとstack overflowを起こすし、かといってSTLの使い方もよくわからんので自分でスタックを実装するはめになりました(Masktools2はSTLのlistとpairを使っている)。
結果として解像度に縦横ともに65535までの制限ができたりメモリの使用量が多くなったり(1920x1080で約8MB)しましたが、まあそこらへんは現時点ではそれほど問題ではないと目をつぶることにします。

さて、このプラグイン、出力はmt_hysteresisと変わらないわけですが最大の問題はスピードです。
さっそくベンチマークだ!
#benchmark.avs
MPEG2Source("1440x1080_6360frames.d2v").ConvertToY8()
base = last.mt_edge(thY1=30, thY2=30)
alt = last.mt_edge(thY1=5, thY2=5)
ret= mt_hysteresis(base, alt) # Masktools2a48
# ret = Hysteresis(base, alt) # 今回の自作フィルタ
return ret

$ for i in {1..3}; do avs2pipemod -benchmark benchmark.avs; done

      mt_hysteresis  Hysteresis
1st     24.255fps     48.490fps
2nd     24.242fps     48.523fps
3rd     24.310fps     48.583fps
--------------------------------
avg     24.269fps     48.525fps

まさかのダブルスコアです。

原因がMSVC++のSTL実装が糞すぎるのか(Masktools2の配布バイナリはVC++10で筆者と同じ)、それともこんなクリティカルな処理にSTLを使うのが間違いなのかはわかりませんが、C++大好き人間は御託を並べる前にもう少し基本に立ち返るべきなのではなかろーかとか思いました。
世はC++11サイコーとか騒がしいですが、肝腎な部分でラクなものに走れば大事なものを失うことになりかねません。
つーか、Masktools2遅すぎるだろ。

さて、次はvapoursynth版書かなきゃならんのか…。

追記:
TurboPascal7氏よりmt_hysteresisが遅い件について指摘を受けました。
彼曰く、「あれはmanaoがlistを使ってるから遅いんだよ、vector使えば2、3倍速くなるのは確認済みだよ」
なるほど、vectorですか。失礼しました。
俺もmallocで一度に最大分確保するのやめてrealloc使うことにしよ。