/dev/null/onishy

どーも。

Crypkoをはじめる

Crypko.aiが面白いです。

オタクなら好きそうなのに僕の周りであまりやっているという話を聞かないので、 布教も兼ねて説明していきたいと思います。

イーサリアムのウォレットを作る必要があるので、意外とハードルが高く感じますが、 まぁ5分もあれば始められると思います。

Crypkoとは

Crypkoとは、かわいい女の子を取引したり融合したりしてコレクションするゲームです。

ただし、一枚として同じ女の子はいません。

crypko.ai

です。

@_aixile さんが以前出した make.girls.moe というプロジェクトを発端にしています。

make.girls.moe

これは画像を生成するディープラーニングの一手法であるGAN(Generative Adversarial Network)を用いて、 女の子のアバターを自動で生成するというものです。

この技術とブロックチェーンを組み合わせたものがCrypkoです。

各カード(=女の子)に遺伝子情報のようなものが対応していて、 2枚のカードから新しいカードを作ることができます。

この新しいカードはブロックチェーン上に記録されるため改竄が不可能であり、 同じカードは一つとして存在しない、とのことです。

実際同じ親から2枚カードを作ると違う子が生まれます。 親の遺伝子がどのように受け継がれるかは、恐らくランダムな比率に基づいていそうですが、 この辺りのアルゴリズムについては公開されていません。

ただ一つ言えるのは、この遺伝子情報から生成される女の子の画像のクオリティがものすごく高いということです。

はじめてみる

0. テストネットについて

前述のようにCrypkoはブロックチェーン上でのやり取り(トランザクション)をベースにしているので、 まずはブロックチェーンを利用する準備が必要です。

イーサリアムという仮想通貨の名前を聞いたことがある人も多いと思いますが、 Crypkoではイーサリアムのテストネットブロックチェーンテスト用のネットワーク)を使います。

テスト用のネットワークなので、トークン(通貨)は無料でもらうことができます。 ただし無尽蔵にというわけではないのでご注意ください。

テスト用のネットワークなので、基本的に実際の通貨としての価値はありません。

少なくとも、誰かが「Crypkoを使うために金を払ってでも買う」みたいに、価値を見出そうとしない限りはそうです。(それはそれで胸熱展開ですね。)

1. MetaMaskの導入

イーサリアムを利用するために、MetaMaskという便利なプラグインが存在します。 これはブラウザ上でイーサリアムを支払う際の財布(ウォレット)のようなものです。

Chromeではこちらから。 MetaMask - Chrome ウェブストア

まずはこれをインストールします。

Mnemoric Phraseはランダムな12単語の羅列で、別のアプリでウォレットを使おうとすると聞かれるのでメモしておきます。

言われたとおりにアカウントを作ると、拡張機能としてブラウザから使えるようになります。

2. トークンをもらう

トークンをもらうにはhttps://www.rinkeby.io/#faucetに書いてあるように、以下のようにします。

  1. MetaMaskの「…→Copy Address to clipboard」からウォレットのアドレスをコピペし、外部SNSに公開する(ツイートするとか)。 f:id:onishy:20180602153626p:plain:w300

  2. Rinkeby https://www.rinkeby.io/#faucetに当該ツイートやポストのURLを入力する。

  3. "Give me Ether"をクリックする。

f:id:onishy:20180602153738p:plain:w300

ここで、"Give me Ether"には"3Ether/8hrs" "7.5Ethers/day" "18Ethers/3days"の3種類のもらいかたがあることがわかります。

計算すればわかりますが、"3Ether/8hrs"が一番オトクです。ただし8時間ごとに申請をする必要があって面倒なので、 とりあえず遊んでみたい方は"18Ethers/3days"で良いと思います。

3. Crypkoへのログイン

これでCrypkoへログインする準備ができました。

MetaMaskのネットワークを「Rinkeby Test Net」にして、crypko.aiを開いてパスワードを設定してログインすれば完了です。

あそぶ

それでは遊んでみます。

お迎えする

まずは女の子がいないと始まらないので、買いますお迎えします。

18Ethersあれば大抵の女の子はお迎えできてしまいますが、とりあえず1Ether以下の好みの女の子を2人選ぶと良いでしょう。 N/R/SR/SSR/URとか書いてありますが今は気にしなくて良いです。

「マーケット→販売」を選んで、好きな女の子を選びます。

f:id:onishy:20180602154826p:plain:w300

この子を選びました。

f:id:onishy:20180602154806p:plain:w300

Buyをクリックすると、MetaMaskによる承認の画面が出てくるので、Acceptします。

f:id:onishy:20180602154800p:plain:w300

しばらくすると購入が完了し、自分のコレクションに入ります。融合するために同じようにもう一人購入しましょう。

お迎えした子を選ぶと、こんな画面になります。

f:id:onishy:20180602154833p:plain:w300

女の子の右下に四角形が2つ重なった「融合」ボタンがあるのでクリックし、融合したい他の女の子を選びます。

今回はこの2人の女の子を融合します。

f:id:onishy:20180602154948p:plain:w300

融合が完了するまでしばらく待ちます。

f:id:onishy:20180602154821p:plain:w500

完了しました。

f:id:onishy:20180602154813p:plain:w500

そうすると新しい女の子ができます。極めてシンプルです。

f:id:onishy:20180602154751p:plain:w300

自分の持っているカードは、「マイカード」から確認することができます。

f:id:onishy:20180602161240p:plain:w300

売買する

お気づきの方も多いと思いますが、さっきの女の子も誰かが売りに出していたものです。Crypkoでは人身売買が可能です。

さらにヤバいのが、女の子をレンタルできる点です。 自分のカードを有償でレンタルし、融合する権利を売買することができます。ヤバい。

先ほどの「融合」ボタンの隣に「販売」ボタンと「貸出」ボタンがありますが、これを選ぶとレートを決めることができます。

オークション形式なので、最大値と最小値を決めておくと、最大値から値段がだんだん下がっていきます。

f:id:onishy:20180602155721p:plain:w300

ざっくり言えばこれだけです。ただし、システムについて理解する必要があります。

システム

レーティング(N/R/SR/SSR/UR)とは

レーティングは、融合後に次の融合を開始するまでの時間(Cooldown)を表しています。 URなら一瞬、SSRなら15秒くらいが目安です。

先ほど融合に使った子はこんな感じで11時間のcooldownになりました。

f:id:onishy:20180602161322p:plain:w300

これはそのカードを融合した回数に依存します。 なので、最初はURでも、そのカードを親として融合していくと、だんだんSSR→SRと言ったふうにレートが下がっていきます。 これにより同じ遺伝子を持つ子供の数が制限されています。

イテレーションとは

Iterは、そのカードが第何世代なのかを表しています。

Iter0はランダムに運営側で生成されてマーケットに放出される女の子で、Iter1以上は誰かが融合して作った女の子です。

子供のIter数は親のうちIter数が大きいものに1を足した数になり、これはレーティングと違って変わることはありません。

Iter0同士の子供はIter1になりますし、Iter50とIter3の子供はIter51になります。

子供の初期状態でのレーティングはIter数に依存します。Iter1で生まれてきた子供はURになりますし、Iter100で生まれてきた子供はNになります。

親のレーティングには一切関係がないので、Iter0/N同士の子供はIter1/URになります。この辺りを知っているとちょっと有利になるかもしれません。

デリバティブ

カードを開くと、その子をベースに融合した子の一覧と親の一覧を見ることができます。

これをたどっていくと、自分が売買/レンタルに出した子が今どうなっているのか、みたいな確認もできて面白いです。

余談

この章では若干テクニカルな話をします。別に読む必要はないです。

Cryptokitties

ブロックチェーン上でカードを集めて融合して…というのはCrypkoが最初ではなく、Cryptokittiesというものがあります。 www.cryptokitties.co

これはその名の通り猫を集めるもので、カード同士を融合して別の猫を作ったり、売買したりするものです。Crypkoはこれを真似した感じですね。

こちらはMain Network上で実際にイーサリアムがないと売買できないので僕はやったことがありません。

ただGANで女の子を集めるほうがモチベーションは上がりますね。。

属性

各カードには属性が入っています。

NIPSの元論文ではDiscriminatorの最後にClassifierとして34クラス分類用の全結合層をつけています。 クラスも一致しているようなので、恐らく画像の生成後にClassifierを通した結果でしょう。

ちなみに今まで遺伝子情報と言ってきているのはGANで言うG(z)のzです。

zはうまく学習ができていると内挿が可能です。つまり足して2で割ったベクトルをネットワークに入力すると、中間的な生成画像が得られます。 子供のzは、何らかの比率で親のzを混ぜ合わせていそうです。

イーサリアムトランザクションを見てみる

「取引」メニューから各取引のトランザクションが確認できます。

トランザクションを確認すると、親のID2つと子のID1つ、それからめっちゃ長い数字が一つはいっています。

f:id:onishy:20180602160909p:plain:w500

256bitsあるのでこれが遺伝子情報っぽいですね。サーバーサイドで計算した結果が入ってそうです。

理論上は2^256種類のCrypkoが存在することになります。

技術の詳細

作者の方のWebサイトはこちらです。

yanghuaj.org

NIPSのWorkshopに論文も出しているので、興味があれば読んでみると良いと思います。

[1708.05509] Towards the Automatic Anime Characters Creation with Generative Adversarial Networks

おわり

GANでここまでのクオリティのキャラクターが生成できるのはすごいですね。

遊び方は色々だと思いますが、自分の知っているキャラクターに似たキャラを生成してみるみたいな遊び方もできそうです。

今後どうなっていくかが楽しみです。

ちょっとだけ良い英語を書く

eeic Advent Calendar 13日目の記事です。

インターン・就活用の書類、論文など、そろそろ英語でポエムを書かなきゃと思っている人も多いでしょう。

良い英語のためには、本当はちゃんと文法を勉強した方が良いです。

でもそんなことは言ってられないと思うので、とりあえず英文を書く上で、知っておくだけでいつもよりちょっとだけ良い英語を書けそうなテクニックをいくつか挙げます。割と当たり前のことを言っているかもしれない。

本当は例文とかも色々挙げたかったけど、あまり時間がなかったので気が向いたら。

類義語辞書を使う(重要)

同じ表現を多用するのはボキャ貧です。アカデミック界隈の礼儀はあまり知らないが、therefore連弾が来ると普通の人はボキャ貧ワロスってなると思う。

例えば英語には順接の接続詞や副詞がたくさん用意されています。therefore, thus, hence, as a resultとか。

あとはこれを良い感じに分散させます。だいたい意味は同じだけどちょっとずつ違うので、「therefore thus 違い」とかでググると出てくる英語ブログを見ながら、パズル感覚ではめていくのが良い。

他の英単語についても同じ。this paper investigates… previous work investigated… the investigation on...みたいに同じ単語が被っていると、やっぱりボキャ貧ワロスってなる。

そういう場合は類義語辞典=thesaurusを使う。

有名なのは thesaurus.com で、ここに行くと、単語について意味ごとに類義語を教えてくれる。

でもこれが絶妙に使いにくいので、日本語の類義語をWeblioで調べてから英訳してみたりするのも良いかも。

良いポエムになるかどうかは結構この辺が大事。

例えば「be interested in」を「passionate about」とか「have enthusiasm」とかにするだけでなんかそれっぽい英語になりがち。

曖昧な表現を避ける

haveとかtakeとかはあんまり使わないほうが良いです。 have, take, get, putあたりは便利すぎて意味が曖昧になりがちな単語です。

isとかも場合によってはそうで、例えば「Hemi is God」だと、Hemi means Godなのか、Hemi is a name of Godなのか分かりません。

上のボキャ貧の話ともつながりますが、できるだけ読み違えを生まないような表現を心がける。日本語でもそうですけどね。

強めの動詞を使う

thinkとかwantとかはちょっと意志が弱そうに見えます。日本語でも「〜思う」みたいな文章が避けられるのと同じ。

「〜と考えられる」と書きたい場合は、「A results from…」みたいな客観的な感じに書くのも良い。

主観的に「〜思った」みたいに書きたい場合は、「believe」とか「confirm」とかを使うと良いかもしれない。

受動態をなるべく使わない

受動態は、可能なら避けます。回りくどいので割と嫌われることが多いです。 Wordで書いていると「親に向かってなんだその受動態は!」と言われて波線を引かれます。

形容詞的な用法や、"problem was fixed"くらいの軽いものならOKですが、 能動態に変換できる限りは、能動態に変換するのが吉。

ただし、使わざるを得ない場合もあります。例えば主語をはぐらかしたい場合。 「A is considered as…」みたいなものは能動態に直せないので、受動態で書くしかないです。

そういう時は、読みやすさを意識して書きます。

parallelismを意識する

parallelismというのは、品詞や要素の並列性です。

andを付けるなら、並列されるものはできるだけ品詞や時制を一致させます。

どこが並列されているのかが分かりやすいのが大事。長い文章になってくると本当に読みにくくなります。

並列化できないなら、そもそも文章を分けたほうが良い可能性が高いです。

例えば、"I like shopping and to read a book"とか言うと、"doing"と"to do"が混じっていて、並列性を意識していないのであまりきれいじゃありません。 きれいじゃないので可読性も下がります。片方がdoingならどちらもdoingにするべき。

同様に、名詞とdoingとかも気にすると良いです。

WordのFlesch-Kincaid Grade Levelを使う

Microsoft Wordには、Flesch-Kincaid Grade Levelという機能があります。

これはある指標を基に「やーいお前の英文◯年生〜〜」と言ってくれるツールです。 Flesch–Kincaid readability tests - Wikipedia

ただの単語長や音節数を基にしたヒューリスティックなので、長い単語を使えば学年は上がります。 フランクすぎる英語は短い単語を使いがちなので、これを意識するとそこそこまともな文章になります。

これは経験上意外と有用で、12、13年生くらいを目指すと、そこそこフランクすぎない英語になります。 ちなみにアメリカは学年を小学1年から通算して言うので、高校卒業の年が12年生です。

逆に、ヤバ長い物質名を乱用する化学系とかだとあんまり参考にならないかもしれませんが、普通の文章だったら結構参考になります。

Googleを活用する

Google is God. 最近のGoogle翻訳は強いので、とりあえず日本語をGoogle翻訳に突っ込んでみると、思わぬ表現を得られたりします。 とりあえず突っ込んでみて参考にするのはオススメ。誤訳も多いのでそのまま使うのはあんまりオススメしません。

あとは、Google Ngram Viewer を活用します。

ngram viewerは、ある表現が書籍に出てくる割合とかを教えてくれます。

なので、慣用句とか専門用語とか、「これ本当にこんな言い方する?」とか「ここってinなのかonなのかどっちだ?」とか思ったら、とりあえず突っ込んでみると本質情報が得られがち。

母国語で論理構成を確認する

英語で文章を書いていると一種のハイな状態になっていて、ちゃんと読むと実は論理が破綻していたりします。

また英語を読んでいるときも、英語の理解に集中していてそういうのには気づかなかったりします。

一度和訳して読み返してみると、思わぬミスが発覚したりします。 英→日はGoogle翻訳とかで良いと思います。Google翻訳に訳せる程度のわかりやすさかどうかも一緒に確認できる。


以上、ちょっとだけ良い英語を書けそうなテクニックでした。

僕の主観的な部分もありますし、常に成り立つわけでもないので、あまり気にしすぎないほうが良いと思います。

結局は英語のできる人に読んでもらうのが一番な気もします。

Imagine how your dreams come true, EEIC.

の目覚めは、いつも通り無意識だった。

この腕に貼り付けられたウェアラブル端末によって、起床時刻に目覚まし代わりの心地よい電気刺激が与えられるのだ。まぁウェアラブルなんて言葉は今では当たり前すぎて、ほとんど聞かなくなった古い言葉だ。

メガネをかけると、壁に今日の予定が現れた。仕組みはよく分からないが、レンズ全体が透明なディスプレイになっていて、現実世界に重ねて映像が映し出されるのだ。

「1時間後にミーティング!!」

全てが適切にスケジューリングされていて、今では、時刻を意識することすら少ない。


布団を脱ぎ捨ててリビングに下りると、母がテレビ(メガネに映し出されているのだ)を見ながら朝食をとっていた。

「今年は、イチゴが安いらしいわよ」と母が言った。

農業は完全に自動化されているが、翌年の需要や収穫率が予測され(あるいはそれを制御するために)、生産量が制御されるのだ。

朝ドラを映していた僕のメガネに「母さんと同じチャンネル」と話しかける。3D映像の記者が、青森県のイチゴ工場を取材していた。


母は「行ってくるわね」と言うと自動車に乗り、音もなく出かけた。電気で動く自動車はとても静かだ。

母は車が近づくと警告を出してくれるメガネなしでは、未だに外に出るのが怖いらしい。もっとも、自動運転だから事故に遭うほうが難しいと思うのだけれど。


そういえば、最近は「電気」なんてものは意識しなくなってしまった。

無線給電でコンセントなんて危ないものはなくなってしまったし、昔は街中に張り巡らされていたらしい電信柱も、今では全部地中に埋まって、ロボットによって自動でメンテナンスが行われる。発電に用いる資源の問題も、核融合炉が実用化されてからはすっかり静かになった。

電気を意識するのは雷の日くらいだ。


僕は部屋に戻って、今日の仕事を始める。

「ミーティング」と呼ばれるこの仕事では、人工知能の合成音声と会話をして、たまに簡単な作業をするだけだ。合成音声と言っても人間の音声と聞き分けがつかないし、人間と会話するのと何ら変わりはない。でも強いて言うなら、「人工知能」の本体を見たことがないのは変な気持ちだ。

この会話を通して、人工知能は何かを学んで賢くなるらしい。今では、芸能人なんかを除いて、国民のほとんどがこの仕事をしている。

人工知能に認められた一部の変わった人たちは、コンピュータと一緒に研究機関で新しい技術を作っているらしい。


今日の「ミーティング」では、人工知能と他愛もない会話をしながら、昨日ドローン便で届いた立体のブロックを使ってパズルを解いた。

人工知能によれば、なんでも、これは次の宇宙探査機の形状を最適化するのに役立つらしい。

宇宙開発といえば、最近は火星の開発が活発で、中でも嫌気性細菌を送り込んで酸素を生み出すプロジェクトと、大規模な国際データセンター設置のプロジェクトをよく耳にする。まぁ案の定、環境保護団体がうるさいけれど。


4時間くらい仕事をした。 疲れたけど、今日の仕事はこれで終わり。

最近の研究によると、一日の適切な仕事時間は3時間くらいだそうだ。それ以外は、友達と遊んだり、映画を見たり、日記や物語を書いたりすることが推奨されている。

そういう経験のほうが、「ミーティング」で人工知能にとって人から学べることが増えるんだそうだ。


ベッドに寝転んで、今日見る映画を選んでいると、友人からメッセージがきた。

「勉強会しようぜ!」

僕は快諾すると、支度を始めた。 オンラインだから家を出る必要はないけれど、服を着替える。

知識でコンピュータに勝つことはできないけれど、何かを「学ぶ」のは、すごく楽しい。

参考資料

スペキュラティブ・デザイン

とは、「未来を夢想し、そこにある課題を先取りする」問題提起の方法です。

どんな未来を目指しましょうか。

VLC弄ってみた 番外編 - Waifu2x-CaffeをLinux上でコンパイルする

さて、前回はなぜフィルタの話をしたのかというと、実はWaifu2xで動画の拡大をやろうと思っていたんですね。

Waifu2xってなに

Waifu2xは少し前にtwitterで話題になった画像拡大アルゴリズムで、ざっくり言うと、ニューラルネットワークを用いて、ある画像が「別の画像の縮小である」と仮定して、モデルを基に元の画像を推定することで高画質な拡大ができるそうです。

学習済みのモデルにはいくつか種類があるようですが、アニメタッチの画像などで特に力を発揮するようです。

もっと知りたい方は、この辺の記事とかを読んでいただけると分かりやすいかと思います。

Waifu2xをVLCに組み込みたい

それで、このWaifu2xですが、いくつかバージョンがあります。 最初にtwitterで話題になったのはこれで、こちらの論文を参考にしているようです。

ただ、luaで書かれていてあまり汎用性がないため、C++で書かれたwaifu2x-converter-cppや、高速化を目指して機械学習ライブラリCaffeやcuDNNを用いたwaifu2x-caffeなどが有志によって実装され、公開されています。

僕も実際に試してみましたが、やはりCaffe版が最速のようだったので、是非こいつをVLCに組み込みたいと思ったわけです。

ただ、こいつがVisual Studioで書かれていて、Windows向けなんですね。

僕が使っているのはUbuntuなので、頑張ってLinuxコンパイルできないか…ということで、まずはCMakeでこいつをコンパイルすることにしました。

CMakeってなに

CMakeというのは、使うライブラリなどを書いておくとMakefileを自動で生成してくれるツールです。似たような用途では他にもautomakeというのがあって、VLCではこちらを使っています。

Makefile.amのamはautomakeの略ですね。

ですが、automakeは少し冗長な書き方になってしまうので、CMakeのほうがすっきりとかける傾向にあります。

この手のツールには他にもOMakeとかninjaとか、jsonで書けるgypとか色々ありますが、使ったことがあって一般的で一番手っ取り早そうだったのでCMakeにしました。

必要なもの

NVIDIAのサイトから、CUDA, cuDNNをダウンロード・インストールしておいてください。

対応GPUを搭載したマシンが必要になります。

書く

まずwaifu2x-caffeをcloneしてきます。

git clone https://github.com/lltcggie/waifu2x-caffe.git
git submodule update --init --recursive
mkdir cmake

ここにCMakeLists.txtを付け足しますが、その前にCMakeでは一般的にサポートされているOpenCV以外のライブラリを使いたい場合、自前でFind***.cmakeというファイルを用意する必要があります。こいつの役目は主にディレクトリを探してくることです。自分で書くのはめんどくさいので、他の人が書いたものを借りてきましょう。良い時代です。

まず、FindGlog.cmake, FindCUDA.cmakeというファイルが必要です。次のファイルをそれぞれcmake/FindGlog.cmake, cmake/FindCUDA.cmakeとして保存します。

そして、cuDNNをdetectするために、caffeのCuda.cmakeから、165行目〜を拝借します。これをcmake/FindcuDNN.cmakeとして保存します。

################################################################################################
# Short command for cuDNN detection. Believe it soon will be a part of CUDA toolkit distribution.
# That's why not FindcuDNN.cmake file, but just the macro
# Usage:
#   detect_cuDNN()
function(detect_cuDNN)
  set(CUDNN_ROOT "" CACHE PATH "CUDNN root folder")

  find_path(CUDNN_INCLUDE cudnn.h
            PATHS ${CUDNN_ROOT} $ENV{CUDNN_ROOT} ${CUDA_TOOLKIT_INCLUDE}
            DOC "Path to cuDNN include directory." )

  get_filename_component(__libpath_hist ${CUDA_CUDART_LIBRARY} PATH)
  find_library(CUDNN_LIBRARY NAMES libcudnn.so # libcudnn_static.a
                             PATHS ${CUDNN_ROOT} $ENV{CUDNN_ROOT} ${CUDNN_INCLUDE} ${__libpath_hist}
                             DOC "Path to cuDNN library.")

  if(CUDNN_INCLUDE AND CUDNN_LIBRARY)
    set(HAVE_CUDNN  TRUE PARENT_SCOPE)
    set(CUDNN_FOUND TRUE PARENT_SCOPE)

    mark_as_advanced(CUDNN_INCLUDE CUDNN_LIBRARY CUDNN_ROOT)
    message(STATUS "Found cuDNN (include: ${CUDNN_INCLUDE}, library: ${CUDNN_LIBRARY})")
  endif()
    message(STATUS "Found cuDNN (include: ${CUDNN_INCLUDE}, library: ${CUDNN_LIBRARY})")
endfunction()

そして、waifu2x-caffe/に次のCMakeLists.txtを保存します。

cuDNNをインストールしたディレクトリをexportしておきます。

export CUDNN_ROOT=/usr/lib/cuda/include

次のコマンドを実行し、コンパイルできることを確かめましょう。

mkdir build; cd build
cmake ..
make

著作権上微妙なFindcuDNN.cmake以外のファイルをgithubに上げてあります。

https://github.com/onishy/Waifu2x-Caffe-Linux.git

結果

waifu2x-caffeは無事にコンパイルできました。ただ、こいつをVLCに移植する段階で少し挫折してしまいました。

まず処理時間的にリアルタイム処理は難しかったので、適当に数フレームごとに適用する処理を書かなければいけなかったのが少し面倒だったというのがあります。

またautomakeでのライブラリのリンクなんかが面倒すぎて、あまり時間もなくなってきたのでやめてしまいました。ごめんなさい。。

VLC弄ってみた#3 - フィルタを追加する

第3回目の今回は、VLCのフィルタ機能について解説します。

VLCには色々な機能がありますが、その中でも1、2位を争うレベルで知られていない機能と言っても過言ではないのが、フィルタ機能です。

フィルタ機能とは?

普通に再生した動画は以下。

f:id:onishy:20151104004758p:plain

試しに、次のコマンドをつけてVLCを起動してみましょう。

vlc --video-filter rotate

このウィンドウで適当な動画を再生すると、次のような実行結果になります。

f:id:onishy:20151104004802p:plain

その名の通り、動画を45度回転させて再生する機能のようです。

一体どういう時に使うのか、サッパリ見当が付きません…

次に、このようなコマンドをつけて起動してみます。

vlc --video-filter ball

先ほど同様に動画を再生すると…?

f:id:onishy:20151104004807p:plain

!?!?!?

どうやら、動画から輪郭を抽出して、それを壁にボールが跳ねる仕様っぽい…??

こんなものがVLCの正式版に入っているんですから奇妙です。

フィルタ機能の実装

され、これらの謎のフィルタですが、どこに実装されているかというと、modules/video_filterの中です。これらのファイル一つ一つ、その数58個!!!そしてその大半が実用性の見いだせないおもちゃフィルタです。暇人かよ。

psychedelicとかも割とマジキチで面白いです。結果は試してみてください。

さてフィルタですが、既存のフィルタを踏襲すれば、比較的簡単に実装することができます。

ここでは、ballフィルタをパクって、ボールの代わりに画像を表示できるようにします。

モジュールの登録

ball.cの122行目、vlc_module_begin()から始まる部分が、vlcのライブラリにフィルタをモジュールとして登録しています。

vlc_module_begin ()
    set_description( N_("Ball video filter") )
    set_shortname( N_( "Ball" ))
    set_help(BALL_HELP)
    set_capability( "video filter2", 0 )
    set_category( CAT_VIDEO )
    set_subcategory( SUBCAT_VIDEO_VFILTER )

    add_string( FILTER_PREFIX "color", "red",
                BALL_COLOR_TEXT, BALL_COLOR_TEXT, false )
    change_string_list( mode_list, mode_list_text )

    add_integer_with_range( FILTER_PREFIX "speed", 4, 1, 15,
                            BALL_SPEED_TEXT, BALL_SPEED_LONGTEXT, false )

    add_integer_with_range( FILTER_PREFIX "size", 10, 5, 30,
                            BALL_SIZE_TEXT, BALL_SIZE_LONGTEXT, false )

    add_integer_with_range( FILTER_PREFIX "gradient-threshold", 40, 1, 200,
                            GRAD_THRESH_TEXT, GRAD_THRESH_LONGTEXT, false )

    add_bool( FILTER_PREFIX "edge-visible", true,
              EDGE_VISIBLE_TEXT, EDGE_VISIBLE_LONGTEXT, true )

    add_shortcut( "ball" )
    set_callbacks( Create, Destroy )
vlc_module_end ()

ballフィルタに関する情報がずらずらと並べられています。まずはこれを適当に変更します。

vlc_module_begin ()
    set_description( N_("Hemi video filter") )
    set_shortname( N_( "Hemi" ))
    set_help(BALL_HELP)
    set_capability( "video filter2", 0 )
    set_category( CAT_VIDEO )
    set_subcategory( SUBCAT_VIDEO_VFILTER )

    add_string( FILTER_PREFIX "color", "red",
                BALL_COLOR_TEXT, BALL_COLOR_TEXT, false )
    change_string_list( mode_list, mode_list_text )

    add_integer_with_range( FILTER_PREFIX "speed", 4, 1, 15,
                            BALL_SPEED_TEXT, BALL_SPEED_LONGTEXT, false )

    add_integer_with_range( FILTER_PREFIX "size", 10, 5, 30,
                            BALL_SIZE_TEXT, BALL_SIZE_LONGTEXT, false )

    add_integer_with_range( FILTER_PREFIX "gradient-threshold", 40, 1, 200,
                            GRAD_THRESH_TEXT, GRAD_THRESH_LONGTEXT, false )

    add_bool( FILTER_PREFIX "edge-visible", true,
              EDGE_VISIBLE_TEXT, EDGE_VISIBLE_LONGTEXT, true )

    add_shortcut( "hemi" )
    set_callbacks( Create, Destroy )
vlc_module_end ()

hemiというのは、僕の中でhogeと同義語の単語です。あまり気にしないでください。

BMP画像の読み込み

画像を表示するので、まずはbmpを読み込む関数を作らなければいけません。VLCには画像系の関数は色々充実していそうなものですが、bmpに出力する関数はあっても、読み込む関数はないようです。

とりあえず動けばいいので、今回はアルファチャンネルなどは考慮しません。GIMPで40x40に縮小した正方形の画像をbmpに出力した、どーもくんのアイコンを用います。 f:id:onishy:20151105015953j:plain

なのでここは、検索すると出てくる中でも一番シンプルな実装をされている、物理のかぎしっぽさんのソースコードを拝借させて頂きます。

ただ、こちらのソースコードには少し読み込みのバグがあり、そのまま使うと数ピクセル分黒いピクセルが現れた後、RGBが入れ替わった画像が不思議な周期で現れるというおかしな挙動を示してしまいます。

少しデバッグをすると分かるのですが、これはヘッダを読み込んだ際にバッファを先送りしていないためなので、56行目あたりに2行だけソースコードを追加します。

//68bytes捨てる
char null_buf[128];
fread(null_buf, sizeof(unsigned char), 68, fp); //Throw away

これで読み込み関数はOKです。

描画と基底変換

次に、ボールの代わりにこの画像を描画します。

読み込まれた画像は2次元配列になっているので、大して難しくはありません。元々円が描画されていた部分(drawBall)を、次のように変更するだけです。

static void drawHemi( filter_sys_t *p_sys, picture_t *p_outpic )
{
    Image *img = Read_Bmp(hemi_filename);

    int x = p_sys->i_hemi_x;
    int y = p_sys->i_hemi_y;
    int size = p_sys->i_hemiSize;

    const int i_width = p_outpic->p[0].i_visible_pitch;
    const int i_height = p_outpic->p[0].i_visible_lines;

    for( int j = y - img->height/2; j < y + img->height/2; j++ )
    {
        bool b_skip = ( x - size ) % 2;
        int m_y = j - (y-img->height/2);
        for( int i = x - img->width/2; i < x + img->width/2; i++ )
        {
            /* Draw the pixel if it is inside the disk
               and check we don't write out the frame. */
            {
                int m_x = i - (x-img->width/2);
                ( *p_sys->drawingPixelFunction )( p_sys, p_outpic,
                                    get_color_func(img->data[m_y * img->width + m_x]).comp1,
                                    get_color_func(img->data[m_y * img->width + m_x]).comp2,
                                    get_color_func(img->data[m_y * img->width + m_x]).comp3,
                                    i, j, b_skip );
                b_skip = !b_skip;
            }
        }
    }   
}

少しややこしいのは、VLCが必ずしもRGBで画像を描画するわけではないところです。

ball.cの冒頭、48行目付近を見てみます。

#define COLORS_RGB \
    p_filter->p_sys->colorList[RED].comp1 = 255; p_filter->p_sys->colorList[RED].comp2 = 0;        \
                                p_filter->p_sys->colorList[RED].comp3 = 0;        \
    p_filter->p_sys->colorList[GREEN].comp1 = 0; p_filter->p_sys->colorList[GREEN].comp2 = 255;    \
                               p_filter->p_sys->colorList[GREEN].comp3 = 0;       \
    p_filter->p_sys->colorList[BLUE].comp1 = 0; p_filter->p_sys->colorList[BLUE].comp2 = 0;        \
                               p_filter->p_sys->colorList[BLUE].comp3 = 255;      \
    p_filter->p_sys->colorList[WHITE].comp1 = 255; p_filter->p_sys->colorList[WHITE].comp2 = 255;  \
                                  p_filter->p_sys->colorList[WHITE].comp3 = 255;

#define COLORS_YUV \
    p_filter->p_sys->colorList[RED].comp1 = 82; p_filter->p_sys->colorList[RED].comp2 = 240;        \
                                p_filter->p_sys->colorList[RED].comp3 = 90;        \
    p_filter->p_sys->colorList[GREEN].comp1 = 145; p_filter->p_sys->colorList[GREEN].comp2 = 34;    \
                               p_filter->p_sys->colorList[GREEN].comp3 = 54 ;      \
    p_filter->p_sys->colorList[BLUE].comp1 = 41; p_filter->p_sys->colorList[BLUE].comp2 = 146;      \
                               p_filter->p_sys->colorList[BLUE].comp3 = 240;       \
    p_filter->p_sys->colorList[WHITE].comp1 = 255; p_filter->p_sys->colorList[WHITE].comp2 = 128;   \
                                  p_filter->p_sys->colorList[WHITE].comp3 = 128;

ここでYUVという文字列が出てきました。

YUV(Wikipedia)というのは、輝度信号Yと色差信号2つを用いて色を表現する方法です。

これを、231行目からのswitch文で次のように呼び出しています。

switch( p_filter->fmt_in.video.i_chroma )
{
    case VLC_CODEC_I420:
    case VLC_CODEC_J420:
        p_filter->p_sys->drawingPixelFunction = drawPixelI420;
        COLORS_YUV
        break;
    CASE_PACKED_YUV_422
        p_filter->p_sys->drawingPixelFunction = drawPixelPacked;
        COLORS_YUV
        GetPackedYuvOffsets( p_filter->fmt_in.video.i_chroma,
                             &p_filter->p_sys->i_y_offset,
                             &p_filter->p_sys->i_u_offset,
                             &p_filter->p_sys->i_v_offset );
        break;
    case VLC_CODEC_RGB24:
        p_filter->p_sys->drawingPixelFunction = drawPixelRGB24;
        COLORS_RGB
        break;
    default:
        msg_Err( p_filter, "Unsupported input chroma (%4.4s)",
                 (char*)&(p_filter->fmt_in.video.i_chroma) );
        return VLC_EGENERIC;
}

コーデックによって、RGBを用いるか、YUVを用いるかを決めています。

そして、この処理結果を395行目で次のように利用しています。

( *p_sys->drawingPixelFunction )( p_sys, p_outpic,
                    p_sys->colorList[ p_sys->ballColor ].comp1,
                    p_sys->colorList[ p_sys->ballColor ].comp2,
                    p_sys->colorList[ p_sys->ballColor ].comp3,
                    i, j, b_skip );

見ての通り、特定の色のみをサポートしていて(ボールの色は単色で良い)、かつ単色のためよほど変な色でない限り問題ありません。

BMPから取得した色はRGBであるため、これをYUV空間の値に変換する必要があります。この辺によると、次の変換公式を使うことができます。

Y = 0.299R + 0.587G + 0.114B
U = -0.169R - 0.331G + 0.500B
V = 0.500R - 0.419G - 0.081B

Y = (0.257 * R) + (0.504 * G) + (0.098 * B) + 16
Cr = V = (0.439 * R) - (0.368 * G) - (0.071 * B) + 128
Cb = U = -(0.148 * R) - (0.291 * G) + (0.439 * B) + 128

これを基に、先ほどのマクロがあった部分を次のように書き換えます。

typedef struct{
    unsigned char b;
    unsigned char g;
    unsigned char r;
}Rgb;

typedef struct{
    char comp1;
    char comp2;
    char comp3;
}color_t;

color_t (*get_color_func)(Rgb color);

color_t get_rgb_color(Rgb color) {
    color_t c = {color.b, color.g, color.r};
    return c;
}

// Y =  0.299R + 0.587G + 0.114B
// U = -0.169R - 0.331G + 0.500B
// V =  0.500R - 0.419G - 0.081B

// Y  =      (0.257 * R) + (0.504 * G) + (0.098 * B) + 16
// Cr = V =  (0.439 * R) - (0.368 * G) - (0.071 * B) + 128
// Cb = U = -(0.148 * R) - (0.291 * G) + (0.439 * B) + 128

color_t get_yuv_color(Rgb color) {
    color_t c = {
        (int)( 0.257 * color.r + 0.504 * color.g + 0.098 * color.b)+16,
        (int)( 0.439 * color.r - 0.368 * color.g - 0.071 * color.b)+128,
        (int)(-0.148 * color.r - 0.291 * color.g + 0.439 * color.b)+128};

    return c;
}

#define COLORS_RGB \
    p_filter->p_sys->colorList[RED].comp1 = 255; p_filter->p_sys->colorList[RED].comp2 = 0;        \
                                p_filter->p_sys->colorList[RED].comp3 = 0;        \
    p_filter->p_sys->colorList[GREEN].comp1 = 0; p_filter->p_sys->colorList[GREEN].comp2 = 255;    \
                               p_filter->p_sys->colorList[GREEN].comp3 = 0;       \
    p_filter->p_sys->colorList[BLUE].comp1 = 0; p_filter->p_sys->colorList[BLUE].comp2 = 0;        \
                               p_filter->p_sys->colorList[BLUE].comp3 = 255;      \
    p_filter->p_sys->colorList[WHITE].comp1 = 255; p_filter->p_sys->colorList[WHITE].comp2 = 255;  \
                                  p_filter->p_sys->colorList[WHITE].comp3 = 255;   \
    get_color_func = get_rgb_color; printf("using rgb mode\n");

#define COLORS_YUV \
    p_filter->p_sys->colorList[RED].comp1 = 82; p_filter->p_sys->colorList[RED].comp2 = 240;        \
                                p_filter->p_sys->colorList[RED].comp3 = 90;        \
    p_filter->p_sys->colorList[GREEN].comp1 = 145; p_filter->p_sys->colorList[GREEN].comp2 = 34;    \
                               p_filter->p_sys->colorList[GREEN].comp3 = 54 ;      \
    p_filter->p_sys->colorList[BLUE].comp1 = 41; p_filter->p_sys->colorList[BLUE].comp2 = 146;      \
                               p_filter->p_sys->colorList[BLUE].comp3 = 240;       \
    p_filter->p_sys->colorList[WHITE].comp1 = 255; p_filter->p_sys->colorList[WHITE].comp2 = 128;   \
                                  p_filter->p_sys->colorList[WHITE].comp3 = 128;   \
    get_color_func = get_yuv_color; printf("using yuv mode\n");

こうすることで、get_color_funcが、色情報によって正しい色を返してくれるため、画像をYUVに変換することができました。

コード全体像:

そして最後に、Makefileをいじっておきます。modules/video_filter/Makefile.amの中に、次の段落を追加します。

libhemi_plugin_la_SOURCES = $(SOURCES_hemi)
libhemi_plugin_la_CPPFLAGS = $(AM_CPPFLAGS) $(CPPFLAGS_hemi)    -DMODULE_NAME_IS_hemi
libhemi_plugin_la_CFLAGS = $(AM_CFLAGS) $(CFLAGS_hemi)
libhemi_plugin_la_CXXFLAGS = $(AM_CXXFLAGS) $(CXXFLAGS_hemi)
libhemi_plugin_la_OBJCFLAGS = $(AM_OBJCFLAGS) $(OBJCFLAGS_hemi)
libhemi_plugin_la_LIBADD = $(LIBS_hemi)
libhemi_plugin_la_LDFLAGS = $(AM_LDFLAGS) -rpath '$(video_filterdir)' $(LDFLAGS_hemi)

プロジェクトのディレクトリでconfigureとmakeを実行して完成です。

実行結果:

f:id:onishy:20151105015733p:plain

おまけ: VLC+OpenCV

さて、フィルタと言えばOpenCVですが、以前も少しだけ触れたとおり、VLCOpenCVを使うのはちょっとテクニックが必要です。

というのも、VLCのpicture_tには色空間がYUVの場合が存在するわけですが、このために少し変換がややこしいわけです。

実は、VLCのフィルタの中にopencv_example.cppというのがあります。OpenCVを使うからには、IplImageなりcv::Matなりにpicture_tを変換する必要があるわけですが、何と、こいつによると次のように変換できるらしいんですね。

158行目

//(hack) cast the picture_t to array of IplImage*
p_img = (IplImage**) p_pic;

これはさすがにまさか嘘だろ、と思いますが、真っ赤な嘘です。

実際こいつでコンパイルするとセグフォを吐きやがります。

少しググると、StackOverflowでこのような記事を見つけることができます。こちらの記事によれば、正しいコードは以下の通りです。

//picture_t to IplImage without segmentation fault
    p_img = cvCreateImageHeader( cvSize( p_pic->p[0].i_pitch, p_pic->p[0].i_visible_lines ),
        IPL_DEPTH_8U, 1 );
    cvSetData( p_img, p_pic->p[0].p_pixels, p_pic->p[0].i_pitch );

随分まともな雰囲気が漂っていますね。IplImageを(コンストラクタのない時代なので)初期化関数によって初期化し、次にpicture_tの色情報を書き込んでいますが、いずれもp[0]のみを利用しています。

p[0]というのは、picture_tのYUVのうちYのことで、これには輝度情報が含まれています。つまり、この変換によって、p_imgにはpicture_tのもつ画像情報のうち、モノクロのデータのみがコピーされます。他の色情報(色差)は完全に欠落してしまいます。実際にimShowとかで表示させてみると分かるでしょう。

ただ、このopencv_exampleフィルタは、どうやら顔認識を行うだけのフィルタなようなので、これだけでも問題がないわけです。(僕の手持ちの動画では残念ながら本当に顔認識をしているのかは分かりませんでしたが…)

OpenCVの色認識などのフィルターを使いたければ、YUVの3レイヤー全ての情報を使わなければいけません。ただ、残念ながらIplImageは古いので、RGBしかサポートしません。cv::MatならYUVでもサポートしてくれます。正直できそうなもんですが、今回は学生実験であまり時間がないので、またの機会にすることにします。

VLC弄ってみた#2 - 時間をミリ秒にしてみる

第2回目の今回は、VLCにちょっとした改造を加えてみたいと思います。

VLCでは再生時間が秒単位でしか表示されませんが、ミリ秒単位でのシークがしたいことってありますよね。ありますよね?

そういう需要に応えるため、VLCの再生時間をミリ秒で表示させてみます。

環境はUbuntu 15.04/14.04です。MacWindowsではGUIの描画が若干異なるため、恐らくこれだと変わりません。

どこをいじれば良いのかを探す

まずは、どこをいじれば良いのかを探す必要があります。

とりあえず描画部分のパーツを見てみることにしましょう。今回いじりたいのはシーク中の再生時間ですが、これは2ヵ所に表示されています。一つはシークバーの左端、現在の再生時間が表示されている場所で、もう一つはカーソルを合わせた際に出てくるツールチップです。

描画部分がmodule/gui/qt4あたりに入っていることは何となく分かるので、とりあえずこの中をのぞいてみると、utilの中に"timetooltip.cpp" "timetooltip.hpp"というファイルがありました。これがツールチップ情報を持った描画クラスのようです。

/* timetooltip.hpp */
class TimeTooltip : public QWidget
{
Q_OBJECT
public:
explicit TimeTooltip( QWidget *parent = 0 );
void setTip( const QPoint& pos, const QString& time, const QString& text );
virtual void show();

protected:
virtual void paintEvent( QPaintEvent * );

private:
void adjustPosition();
void buildPath();
QPoint mTarget;
QString mTime;
QString mText;
QString mDisplayedText;
QFont mFont;
QRect mBox;
QPainterPath mPainterPath;
QBitmap mMask;
int mTipX;
};

mTextというのが表示されているテキストで、setTip関数を通じてセットされていることが容易に想像できます。

ただこのクラスはパーツを定義しているだけなので、実際にsetTipしている場所を探さなければいけないですね。"timetooltip.hpp"をincludeしているファイルを検索すると、今度は同じディレクトリに"input_slider.cpp" "input_slider.hpp"というファイルが見つかりました。103行目で

/* Tooltip bubble */
mTimeTooltip = new TimeTooltip( this );
mTimeTooltip->setMouseTracking( true );

とあり、ビンゴ!ですね。

さて、実際にsetTipをしている場所を探すと、331行目に

if( likely( size().width() > handleLength() ) ) {
secstotimestr( psz_length, ( ( posX - margin ) * inputLength ) / ( size().width() - handleLength() ) );
mTimeTooltip->setTip( target, psz_length, chapterLabel );
}

という場所がありました。 secstotimestrという関数がキモのようですが、この関数はsrc/misc/mtime.cで次のように定義されています。

/**
* Convert seconds to a time in the format h:mm:ss.
*
* This function is provided for any interface function which need to print a
* time string in the format h:mm:ss
* date.
* \param secs the date to be converted
* \param psz_buffer should be a buffer at least MSTRTIME_MAX_SIZE characters
* \return psz_buffer is returned so this can be used as printf parameter.
*/
char *secstotimestr( char *psz_buffer, int32_t i_seconds )
{
if( unlikely(i_seconds < 0) )
{
secstotimestr( psz_buffer + 1, -i_seconds );
*psz_buffer = '-';
return psz_buffer;
}

div_t d;

d = div( i_seconds, 60 );
i_seconds = d.rem;
d = div( d.quot, 60 );

if( d.quot )
snprintf( psz_buffer, MSTRTIME_MAX_SIZE, "%u:%02u:%02u",
d.quot, d.rem, i_seconds );
else
snprintf( psz_buffer, MSTRTIME_MAX_SIZE, "%02u:%02u",
d.rem, i_seconds );
return psz_buffer;
}

見ての通り、秒数を受け取ってh:mm:ssのフォーマットで文字列を吐き出す関数です。なので、こいつをコピーして、millisecstotimestrという関数を作り、こちらを呼び出すようにしましょう。

実際にいじってみる

次のように関数を定義し、ヘッダファイルも編集しておきます。

char *millisecstotimestr( char *psz_buffer, int32_t i_milliseconds )
{
if( unlikely(i_seconds < 0) )
{
millisecstotimestr( psz_buffer + 1, -i_milliseconds );
*psz_buffer = '-';
return psz_buffer;
}

div_t d;
unsigned short millisec, sec;
d = div( i_milliseconds, 1000);
millisec = d.rem;

d = div( d.quot, 60 );
sec = d.rem;
d = div( d.quot, 60 );

if( d.quot )
snprintf( psz_buffer, MSTRTIME_MAX_SIZE, "%u:%02u:%02u.%03u",
d.quot, d.rem, sec, millisec );
else
snprintf( psz_buffer, MSTRTIME_MAX_SIZE, "%02u:%02u.%03u",
d.rem, sec, millisec );
return psz_buffer;
}

とりあえず、現在位置を秒数で受け取っている分にはどうしようもないので、これをミリ秒で受け取るようにします。受け取る秒は次のように計算されているのでした。

( ( posX - margin ) * inputLength ) / ( size().width() - handleLength() )

ハンドルの位置と動画の長さから秒数を計算し、これがintにキャストされています。なので、ミリ秒を取得するためにはこれを1000倍するだけで良いでしょう。

if( likely( size().width() > handleLength() ) ) {
millisecstotimestr( psz_length, ( ( posX - margin ) * inputLength * 1000) / ( size().width() - handleLength() ) );
mTimeTooltip->setTip( target, psz_length, chapterLabel );
}

動かない??

これでコンパイルを行うと、コンパイルは通りますが起動時に次のようなエラーが出ます。

VLC media player 2.2.1 Terry Pratchett (Weatherwax)
Command Line Interface initialized. Type `help' for help.
> [0000000001229228] [cli] lua interface error: Error loading script [vlc]/share/lua/intf/cli.luac: lua/intf/modules/host.lua:279: Interrupted.
[00000000011224f8] core libvlc: Running vlc with the default interface. Use 'cvlc' to use vlc without interface.
[0000000001229228] core interface error: corrupt module: [vlc dir]/modules/gui/qt4/.libs/libqt4_plugin.so
[00007f194c01f458] core generic error: corrupt module: [vlc dir]/modules/gui/qt4/.libs/libqt4_plugin.so
[0000000001229228] skins2 interface error: no suitable dialogs provider found (hint: compile the qt4 plugin, and make sure it is loaded properly)
[0000000001229228] skins2 interface error: cannot instantiate qt4 dialogs provider
[0000000001229228] [cli] lua interface: Listening on host 

つまり、libqt4がライブラリとして不完全ということです。この原因を探るため、secstotimestrでgrepをかけ、millisecstotimestrとの差を見てみます。すると、なにやら怪しげなファイルにヒットします。

[vlc dir]/src/libvlccore.sym:

358 sdp_AddAttribute
359 sdp_AddMedia
360 secstotimestr
361 services_discovery_AddItem
362 services_discovery_EventManager

どうやらこのlibvlccore.symというファイルは、ライブラリに含まれるシンボルが集まったファイルのようです。 libvlccore.symで再度プロジェクト全体に対してgrepをかけてみます。すると、/src/Makefile.am内に

libvlccore_la_SOURCES = $(SOURCES_libvlc)
libvlccore_la_LDFLAGS = \
$(LDFLAGS_libvlccore) \
-no-undefined \
-export-symbols $(srcdir)/libvlccore.sym \
-version-info 8:0:0
libvlccore_la_LIBADD = $(LIBS_libvlccore) \
../compat/libcompat.la \
$(LTLIBINTL) $(LTLIBICONV) \
$(IDN_LIBS) $(LIBPTHREAD) $(SOCKET_LIBS) $(LIBDL) $(LIBM)
libvlccore_la_DEPENDENCIES = libvlccore.sym
if HAVE_WIN32

とあります。-export-symbolsオプションについてGNU libtoolのドキュメントを見てみると、確かに

-export-symbols symfile

Tells the linker to export only the symbols listed in symfile. The symbol file should end in .sym and must contain the name of one > symbol per line. This option has no effect on some platforms. By default all symbols are exported.

ということらしいです。

つまり、shared libraryでは、望まないシンボルをライブラリに含めないよう、シンボルの指定ができるということです。そのため、ライブラリに含まれないmillisecstotimestrを呼び出そうとするとエラーが起きていたのでした。なるほどなるほど。

ということで、ここにmillisecstotimestrを一行追加して再automake・コンパイルします。これでOKです。

実行結果

f:id:onishy:20151104002409p:plain

ヨッシャヨッシャ!!

Hacking the VLC - How the VLC media player works

This is a translation of my previous article from Japanese.

So, let's begin with analyzing how the vlc media player is working.

The version I used is 2.2.1, which can be downloaded from here. https://www.videolan.org/vlc/download-sources.html

Multi-Threading of VLC

Since VLC is highly multi-threaded program, I must first explain the role of each thread. The important threads to note are the following:

  • Playlist: src/playlist/thread.c
    • does managing of the playlist
  • Video Output: src/video_output/video_output.c
    • does output of the video
  • Decoder: src/input/decoder.c
    • does decoding of the video. Filters are also applied here.
  • The drawing thread (i.e. Qt)

In fact there seems to be more than 10 additional threads running, but I couldn't follow all of them.

If you are interested in those threads, you might want to watch the function "vlc_clone", which is called every time the new thread is created. But be sure that, since VLC is designed to work on multi-platform, it depends on the device which GUI thread is actually called.

Video Processing of VLC

Whoo! Now we are ready to understand how the video is processed inside the VLC!

The video is processed in the following steps:

  1. The video is loaded by Playlist thread.
    • The signals (Play/Stop, Pause, etc.) from GUI thread is received and processed here.
  2. The Decoder thread decodes the video with ffmpeg.
    • The decoded frames are then pushed to picture_fifo.
  3. VideoOutput thread outputs the frame as decoding goes on.
    • the frame is popped from the fifo.
    • the process decides whether the frame should be displayed or not based on the timestamp.

This is basically all what VLC does.

The picture_fifo mentioned above is the fifo of decoded pictures. The frames are pushed in order decoded and popped when played. See picture_fifo.c for further implementations.

There is another struct similar to picture_fifo - picture_pool. This is written in picture_pool.c. It seems like it does bind some pictures or mutex-and-gc-like things. I didn't need to touch these components, so I don't know what it actually does.

Structs

There are many VLC-unique structs defined. Fortunately, today there are BUNCH of libraries floating around which make me not write these stuffs. But it wasn't that easy in 2001. These unique structures make the VLC sources very very complicated.

The astronomical numbers of structs are defined in VLC, but just let me list the ones I think are important (and interesting).

vlc_object_t: Object

Defined in vlc_common.h. The main function can be found in vlc_variables.h.

Since the VLC was developed in 2001, and maybe C++ wan't popular then, the core of VLC is written in C. But I can't imagine how hard to write such a huge program without object-oriented language.

Therefore, VLC has written all those object-oriented structures only with C. Maybe developers were too mad that they couldn't have private variables and member functions in C structs.

I guess that the developers were too happy to use those structures then, but they are slightly outdated today. Hmm...

vlc_object_t creates an object as it is given the type and the name string. The content of an object is described in vlc_value_t union as following:

/**
 * VLC value structure
 */
typedef union
{
    int64_t         i_int;
    bool            b_bool;
    float           f_float;
    char *          psz_string;
    void *          p_address;
    vlc_object_t *  p_object;
    vlc_list_t *    p_list;
    mtime_t         i_time;
    struct { int32_t x; int32_t y; } coords;

} vlc_value_t;

vlc_variables.h has many functions defined for each of the supported types. The functions are given the prefix of var_ as a namespace. All basic variables used in VLC are using this structure.

block_t, picture_t: Pictures

VLC reads the video by block, and writes by frame. block_t and picture_t are defined for these purposes.

block_t is a multi-purpose data block and defined in vlc_block.h:

struct block_t
{
    block_t    *p_next;

    uint8_t    *p_buffer; /**< Payload start */
    size_t      i_buffer; /**< Payload length */
    uint8_t    *p_start; /**< Buffer start */
    size_t      i_size; /**< Buffer total size */

    uint32_t    i_flags;
    unsigned    i_nb_samples; /* Used for audio */

    mtime_t     i_pts;
    mtime_t     i_dts;
    mtime_t     i_length;

    /* Rudimentary support for overloading block (de)allocation. */
    block_free_t pf_release;
};

As you can see, block_t is a lineare list, which can store encoding/decoding information in pts/dts. Thus, block_t is used for managing data by or transfer data in block.

It is obvious that picture_t stores a picture. The purpose of picture_t is limited to storing decoded frames, and single object itself contains enough information for displaying the picture.

/**
 * Video picture
 */
struct picture_t
{
    /**
     * The properties of the picture
     */
    video_frame_format_t format;

    plane_t         p[PICTURE_PLANE_MAX];     /**< description of the planes */
    int             i_planes;                /**< number of allocated planes */

    /** \name Picture management properties
     * These properties can be modified using the video output thread API,
     * but should never be written directly */
    /**@{*/
    mtime_t         date;                                  /**< display date */
    bool            b_force;
    /**@}*/

    /** \name Picture dynamic properties
     * Those properties can be changed by the decoder
     * @{
     */
    bool            b_progressive;          /**< is it a progressive frame ? */
    bool            b_top_field_first;             /**< which field is first */
    unsigned int    i_nb_fields;                  /**< # of displayed fields */
    void          * context;          /**< video format-specific data pointer,
             * must point to a (void (*)(void*)) pointer to free the context */
    /**@}*/

    /** Private data - the video output plugin might want to put stuff here to
     * keep track of the picture */
    picture_sys_t * p_sys;

    /** This way the picture_Release can be overloaded */
    struct
    {
        atomic_uintptr_t refcount;
        void (*pf_destroy)( picture_t * );
        picture_gc_sys_t *p_sys;
    } gc;

    /** Next picture in a FIFO a pictures */
    struct picture_t *p_next;
};

You can think of IplImage in OpenCV. Yet, picture_t has no such capability that it can be directly casted to IplImage. I won't explain the reason today, so wait for a while.

Terms

  • PTS/DTS
    • PTS is an abbreviation for Presentation Time Stamp, and presents the time when the frame SHOULD be played.
    • DTS stands for Decode Time Stamp, which is the time the frame was decoded.
  • I/P/B frame
    • The idea of I/P/B frames is that the frames can be classified to 3 types, I/P/B. This method is used widely, including ffmpeg.
    • Briefly explaining, there are I frames which contain all pixel information. Between I frames, there 2 types of frames, P and B, which differ in compressing level. The P frames are softly compressed and has DIFFERENCES from the previous I or P frame. B frames are hardly compressed frames between P frames, which contains complementary infomation between them. B frame can only be decoded by referencing to previous and next P frames.
    • By using this method, the process can decode the frames under both slow and fast environments with a good quality. (from Wikipedia)
    • decoder_synchro.c has further descriptions.
/*
 * DISCUSSION : How to Write an efficient Frame-Dropping Algorithm
 * ==========
 *
 * This implementation is based on mathematical and statistical
 * developments. Older implementations used an enslavement, considering
 * that if we're late when reading an I picture, we will decode one frame
 * less. It had a tendancy to derive, and wasn't responsive enough, which
 * would have caused trouble with the stream control stuff.
 *
 * 1. Structure of a picture stream
 *    =============================
 * Between 2 I's, we have for instance :
 *    I   B   P   B   P   B   P   B   P   B   P   B   I
 *    t0  t1  t2  t3  t4  t5  t6  t7  t8  t9  t10 t11 t12
 * Please bear in mind that B's and IP's will be inverted when displaying
 * (decoding order != presentation order). Thus, t1 < t0.
 *
 * 2. Definitions
 *    ===========
 * t[0..12]     : Presentation timestamps of pictures 0..12.
 * t            : Current timestamp, at the moment of the decoding.
 * T            : Picture period, T = 1/frame_rate.
 * tau[I,P,B]   : Mean time to decode an [I,P,B] picture.
 * tauYUV       : Mean time to render a picture (given by the video_output).
 * tau´[I,P,B] = 2 * tau[I,P,B] + tauYUV
 *              : Mean time + typical difference (estimated to tau/2, that
 *                needs to be confirmed) + render time.
 * DELTA        : A given error margin.
 *
 * 3. General considerations
 *    ======================
 * We define three types of machines :
 *      14T > tauI : machines capable of decoding all I pictures
 *      2T > tauP  : machines capable of decoding all P pictures
 *      T > tauB   : machines capable of decoding all B pictures
 *
 * 4. Decoding of an I picture
 *    ========================
 * On fast machines, we decode all I's.
 * Otherwise :
 * We can decode an I picture if we simply have enough time to decode it
 * before displaying :
 *      t0 - t > tau´I + DELTA
 *
 * 5. Decoding of a P picture
 *    =======================
 * On fast machines, we decode all P's.
 * Otherwise :
 * First criterion : have time to decode it.
 *      t2 - t > tau´P + DELTA
 *
 * Second criterion : it shouldn't prevent us from displaying the forthcoming
 * I picture, which is more important.
 *      t12 - t > tau´P + tau´I + DELTA
 *
 * 6. Decoding of a B picture
 *    =======================
 * On fast machines, we decode all B's. Otherwise :
 *      t1 - t > tau´B + DELTA
 * Since the next displayed I or P is already decoded, we don't have to
 * worry about it.
 *
 * I hope you will have a pleasant flight and do not forget your life
 * jacket.
 *                                                  --Meuuh (2000-12-29)
 */

This is how VLC can play the video on both fast and slow machines.

I will add more info if I could recall more...