d.sunnyone.org
sunnyone.org

ページ

2015-07-21

OpenCvSharpで表紙の折り目を検知してみた

いわゆる本の自炊をする際、表紙は切らず長尺読みをし、それを分割して使う。その分割作業をサポートするために作ったのがKiritoriMageなのだけど、この切り取る座標を決定するのを自動化できるんじゃね?ってことでやってみた。



詳しくは以下に記述するが、要は折られている以上折り目が存在するので、それを検知すれば、どこで切ればいいかわかるという作戦。その折り目を見つけるにも、印刷されているほうを使うと大変なので、一緒にスキャンされる裏側を使うことにした。

それを実装したのがKiritoriMage 0.0.2で、切りたい画像とセットになる裏側を一緒にドラッグアンドドロップしてあげればこの機能が動作する。試してみたい方はこちらからどうぞ。
https://github.com/sunnyone/kiritorimage/releases/tag/v0.0.2


処理の詳細

OpenCvSharpの使い方例として、処理内容を解説していく。わかりやすさのために、順番を差し替えたり、定数をリテラルにしたりしている。また、OpenCvSharpなので実際の呼び出しとは異なるが、解説が詳しいので、opencv.jpのC/C++のコードの説明をリンクしている。

今よりずっとWPFを使えないときに書いたために、ソースはだいぶきれいではないのでKiritoriMageを読むのはお勧めしないが、この処理自体はFoldDetectUtil.csにある。

今回の例に使う画像を縮小(+α)したものがこちら。実際の表紙を使うと面倒なので、適当な表紙を作った。
この偽表紙は実際の表紙と異なり普通紙でガサガサなので、裏にはちょっとNRをかけてある。


Phase1 折り目候補の洗い出し

画像に手を加えることで、折り目らしき部分を抽出する。

1-1. 画像の読み込み

簡単。Matクラスのコンストラクタにファイル名を渡すだけ。
var matSrc = new Mat(backFilename);

1-2. AdaptiveThresholdによる2値化

これが全体の中で一番肝だと言っていい。最初使ったときは感動した。

ただの2値化は固定値か全体の値を見て白か黒かにするという感じだけど、AdaptiveThresholdを使うとピクセルの周りを見て白か黒か選んでくれる(参考: OpenCvSharpをつかう その15(適応的閾値処理))。全体を見てしまうとこの紙の裏の画像はほとんど差がないので、自動的にやろうとするとどうしても「全部白!」or「全部黒!」となってしまうのだけど、折り目境界部分は比較的違いがはっきりしているので折り目をはっきりと浮かび上がらせてくれる。

コード的には、CvtColorでグレースケールに変換した後、AdaptiveThresholdを呼べばOK。KiritoriMageでは、blockSizeは幅の1/200を奇数にしたもの(4000なら21)としている。
var mat = matSrc.CvtColor(ColorConversion.BgrToGray)
    .AdaptiveThreshold(255, AdaptiveThresholdType.MeanC, ThresholdType.Binary, blockSize, 1);

後述の輪郭検出の都合が良いので、反転しておく。
mat = ~mat;

1-3. 高さ1にリサイズ後、平滑化をかけて2値化

折り目は縦一線にできるので、Resizeで思い切って高さ1にする。この処理ではInterpolation.Areaが一番よかった。
Cv2.Resize(mat, mat, new Size(matSrc.Width, 1), interpolation: Interpolation.Area);
(高さ1ピクセルでは見えないので、この状態で高さを戻すリサイズをかけた状態はこんな感じ)

そのままでは黒と白が細かく散っているので、平滑化する。輪郭を残してくれそうなので、MedianBlurをチョイス。KiritoriMageではksizeはさっきのblockSizeと一緒にしている(動かしてて困らない数値だっただけで、一緒にしたいという意図はない)。
mat = mat.MedianBlur(ksize)

(高さ1は見えないので、高さを戻した状態の画像)

終わったら再度2値化する。ここでは大津の2値化を使った。
mat = mat.Threshold(0, 255, OpenCvSharp.ThresholdType.Otsu);

(再び2値化後の高さを戻した状態の画像)

1-4. 高さを作り輪郭検出

1次元のデータなのでこのまま回して分割していってもいいのだけど、だるいので高さを作ってOpenCVの輪郭検出FindContoursにまかせて、その輪郭を囲む矩形の座標をもらう。…なんて無駄なのでしょう!遅くて使えなかったら本気出す。まぁここでFindCoutoursの例を出すために使ったと言っても過言ではない。なお、FindContoursはmatを破壊するので注意。
Cv2.Resize(mat, mat, new Size(matSrc.Width, 3), interpolation: Interpolation.Cubic);
var rects = mat
    .FindContoursAsArray(ContourRetrieval.External, ContourChain.ApproxSimple)
    .Select(x => Cv2.BoundingRect(x))
    .OrderBy(x => x.X)
    .ToArray();

(選択できたものをCv2.Rectangleで書いた画像)

ここで折り目候補ができたが、ここまでで90%、成否が決まっているといっても過言ではない。候補が大量にできていてはうまくいかないし、そもそも各折り目がどれか検出されなかった時点でアウト。

本当はここで処理を完了したかったのだけど、そんな調子なので、折り目だけを検出するようにパラメータを構成するのは難しく、どれかの折り目が検出されなくなってしまうことがほとんどなので、候補から適切なものを選択することにした。

Phase2 折り目候補から折り目4つを選択

「表紙の紙の折り目」である、という特徴を使って、一番選択肢として妥当なものを選択する。

2-1. 折り目候補の一覧から4つを選ぶ全ての組み合わせの算出

いわゆる順列/組み合わせ。候補から4つを選ぶ組み合わせを列挙する。コードは省略。そこまで長くはないし、実際自前で実装しているんだけど、NuGetでパッとインストールできるこの手の処理が集まってるライブラリの都合のいいやつがないのよね。LINQに入っていてもおかしくない汎用さなんだけど。

2-2. 座標、幅をピクセル数から全体幅との比率に変換

まず各組み合わせについて、画像のサイズは毎回違っていて扱いにくいので、全体の幅で割って割合にする。書いていて思ったが先にやっておいたほうが計算量は少ないね。
var widthRatios = rectCands.Select(r => new { 
    Left = r.X / imageWidth, 
    Right = (r.X + r.Width) / imageWidth,
    Width = r.Width / imageWidth }).ToArray();

2-3. 幅が3%以上なら組み合わせを却下

組み合わせのひとつひとつを見て、折り目が太すぎるもの、具体的には全体幅の3%越えが存在するものはおかしいのでその組み合わせは却下する。過去データを見る限り、1%くらいだったので、余裕を持って3%。
rectSets = rectSets.Where(r => r.WidthRatios.All(x => x.Width < 0.03)).ToArray();

2-4. 細い同士、太い同士の割合(thinRatio, fatRatio)を計算

表紙の紙の特徴として、基本的に表紙と裏表紙(「太いほう」)は同じ幅だし、折り込まれた表紙の内側と裏表紙の内側(「細いほう」)も同じ幅のはず。そのため、表紙の幅/(表紙の幅+裏表紙の幅)という感じで太いほう、細いほうそれぞれの片側が占める割合を計算して(thinRatio, fatRatio)とする。
double leftThin = widthRatios[0].Left;
double rightThin = 1 - widthRatios[3].Right;
double thinRatio = leftThin / (leftThin + rightThin);

double leftFat = widthRatios[1].Left - widthRatios[0].Right;
double rightFat = widthRatios[3].Left - widthRatios[2].Right;
double fatRatio = leftFat / (leftFat + rightFat);
その(thinRatio, fatRatio)と(0.5, 0.5)の距離を計算して、一番近かったものを選んで、これを答えとする。
double thinFatDistance = Math.Sqrt(Math.Pow(fatRatio - 0.5, 2) + Math.Pow(thinRatio - 0.5, 2));
と
var bestMatchRectSet = rectSets.OrderBy(x => x.ThinFatDistance).First();

これで選ばれたものがこちら。


あとは切るX座標として0, 1, 3番目の右側を選んで終了。

いくつか手法を試して、結果的にわりとシンプルなやりかたに落ち着いた。太いほう:細いほうの比はあまり極端ではないはずなので、そこを基準にフィルタしてもいいかなと思っているけども、使い込んでいないのでまぁ様子を見てという感じ。一晩でやれたらいいかなくらいだったものを、だいぶ頑張って実装してしまった感があるので、ここで一区切りかな。

2015-07-13

bsdtarでzip展開時の先頭ディレクトリを削る

zipに限らずアーカイブにはこんな形でファイルが入っていることがある。
hoge-0.1.2/README.md
hoge-0.1.2/configure
hoge-0.1.2/src/
hoge-0.1.2/src/main.d

これをunzipで普通に展開すると、当然ながらhoge-0.1.2ができるが、バージョンが入っているのがうれしくないことがある。ここで、tarの「--strip-components」オプションと、zipを展開できるbsdtarを組み合わせることで、hoge-0.1.2を外して展開することができる。
$ bsdtar xvf hoge-0.1.2.zip --strip-components 1
x README.md
x configure
x src/
x src/main.d

どうでもいいTipsでした。

2015-07-04

Loqui 0.6.1 リリース(タブ補完機能の追加)

0.6.1を出しました(0.6.0はバグがあったので、すぐに0.6.1をリリース)。前回が2013-03なので、2年ぶりくらいですね。

そこまで大きな変更はないのですが、タブ補完機能は純粋に新機能なので、新しいバージョンにしてみました。現在のアカウントのチャンネル名と、現在のチャンネルのnick名を補完できます。かぶっていた場合はステータスバーに候補を出すので、意味ないから消してたという人がいたら、一度出してみてください(UIとしてはイマイチですが、邪魔にならず、しかも一番楽に作れたので...)

launchpadがGitに対応しているらしいので、本当はgitに移行しようかなとも思ったんですが、過去のリンクを切ってまでこれから変更していくこともないかなと。

昨日フォントが変えられず困っている方がいたので、確認してみたところ、チャンネルバッファのフォントが変更できないバグがあったので直したのが0.6.0のきっかけです。フォントは~/.loqui/stylercのGlobalFontの行を以下のように変更することで、変えることができます。

GlobalFont=Takao Gothic 14

2015-06-13

RustとWindowsのワイド文字列(LPCWSTR / LPWSTR)

最近?のWindowsでC FFIで文字列を受渡しをするとなると、ほぼ確実にでてくるのがワイド文字列(「Unicode」文字列)。それをRustでどう取り扱うかについて。

最初に感想を書いておくと、今のところ、Rustでのwchar_t *なNULL終端された「UTF-16」文字列の取り扱いは、char *なNULL終端されたUTF-8文字列に比べるとあまり親切な取扱いではない。しかし、そこまで大変でもなかった。たぶんUTF-8じゃないマルチバイト文字列を扱うよりラク。

前提知識

コードを読む前の前提の話について、箇条書きで書くとこんな感じ:
  • Rustのふつうの文字列は、ざっくり言うとimmutableなstrと、mutableなString。
  • std::str/Stringは、UTF-8文字列の型。NULL終端ではない。
  • std::ffi::CStr/CStringは、UTF-8なchar *(NULL終端あり)をRustの世界で扱えるようにするラッパーの型。コピーが起きにくいようになっている。
  • std::ffi::OsStr/OsStringは、Windowsのワイド文字列も扱えるようにした文字列の型(NULL終端でない)
  • unixとwindowsのそれぞれにOsStrExt / OsStringExt traitがあり、OsStr/OsStringの出入りに使える。
  • Windowsは、std::os::windows::ffi::OsStrExt と std::os::windows::ffi::OsStringExt (ソースを見るに1.0 stableなのにdoc.rust-lang.orgにない...)

変換関数

と、いうことで、便利なCStr/CStringは使えないものの、OsStr/OsStringを経由させればOK。ただし、NULL終端あり/なしの変換にちょっと小細工がいる、という感じ。

コードはこちら。

「from_wide_ptr」は、from_wide()が&[u16]を受け取るので、スライスを作っていて、スライスを作る大きさを\0までの長さで計算している。

「to_wide_chars」は、encode_wide()でIteratorを得て、NULLだけを返すIteratorをSome(0).into_iter()で作ってchain()でくっついたIteratorを生成し、collect()でVecにしている。

一応呼び出し側のサンプルはこちら→https://gist.github.com/sunnyone/1d9e081df7eada21911d
(コンパイル方法はRustでDLLを作るの記事を読んでね)

注意書き

trait作ってprimitiveにimplしたほうがいいかもしれない。そこは実際に自分が使うときにやるかも(真面目にOsStringExtに実装してpull reqしたら取り込んでもらえる?プロセスの堅さどうなんだろ...)

「from_wide_ptr」は、効率が悪いと思いつつ利便性は一番これがいいかなと思ってto_string_lossy()をownedにした(コピーした)Stringを返しているけども、これがいつでも最適解だとは思っていない。OsStringからはlosslessにencode_wide()できると言っているので、ただ引き回すだけならOsStringそのままがいいかもしれないし、厳密性が大事なやつは不正なバイト列があったとき(たぶんペアになってないサロゲートペアとか)に変換に失敗するinto_string()のほうがいいかもしれない。取り扱っているコード次第だと思う。

2015-06-12

C++のラムダ式を利用してお手軽にOutputDebugString()を呼び出す

Windowsでデバッグ出力をしたいと思って、OutputDebugString()を呼ぼうとすると、printf的なフォーマットがないので、ちょっと文字列に数値なんかをつけたいと思うと面倒。sprintfを呼んでしまうというのもひとつだけども、バッファサイズをどうしようとか考えたくない。boost::formatがあればまぁそれをさらっと使えばいいと思うけども、今回のは「boost持ってくるほどのものでもなぁ」というサイズのコードのとき。

で、stringstream使えばいいかなということでベタで呼び出すとこうなる(OutputDebugStringWのほうがいいと思うけど、そこは適宜読み替えで)。
std::ostringstream str;
str << "Num: " << i << std::endl;
OutputDebugStringA(str.str().c_str());

sprintf呼ぶよりはいいと思うのだけど、何回も呼ぼうとするとstr変数がすでにあったりなかったりでややこしい。それで今回は、ラムダ式を使う。

このようなユーティリティ関数を用意して:
void outputDebug(std::function<void(std::ostringstream&)> func) {
 std::ostringstream str;
 func(str);
 OutputDebugStringA(str.str().c_str());
}

こう呼び出す:
outputDebug([&](std::ostringstream& s) { 
 s << "Num: " << i << std::endl;
});

どうだろう?ostringstreamの型名を書くのがちょっと悲しいけど、最初よりはいいんじゃないかなと思う。

C++らしさというのは全然わからないので、仰々しくならずにできる、もっといいやりかたがあればぜひ教えてほしい。

2015-06-07

RustでDLLを作る

期待通りの方法で作れた。ちなみに:
c:\>rustc --version
rustc 1.2.0-nightly (613e57b44 2015-06-01) (built 2015-06-02)
今回のは特にfeature使ってないから1.0でいけるかな?

DLLの作り方

まずプロジェクトを作る。
cargo new rustdlltest

Cargo.tomlを書き換えて、[lib]を作る。詳しくはhttp://doc.crates.io/manifest.htmlを参照。

lib.rsに、no_mangleとextern "C"をつけて、関数を記述する。

詳しくは、Rust bookの「Foreign Function Interface」を参照。

あとは、
cargo build
か
cargo build --release
で、target\debugあるいはtarget\releaseにfoo.dllができている。

Dependency Walkerで見てみると、いろいろな関数がexportされているが、その中で指定したhelloがexportされていることがわかる。


呼んでみる

楽なのでC#から呼んでみるには、こんな感じ。


結果はこう:
>RustLibSample.exe
A negative count is not supported.
Result for -1: False
Hello, World: 0
Hello, World: 1
Hello, World: 2
Result for 3: True

問題ない。boolってなによと思ってintにしてみたら、0と1らしい。

2015-05-30

waifu2xのAvisynthプラグインを作った

Windowsで動くC++版を書かれた方がいる(waifu2x-converter-cpp)という話を聞いて「Avisynthに乗せられるんじゃね?」ってことで、実際に乗せてみました。これで動画も変換できます。

いろいろ更新がありそうなので、ダウンロードリンクや使い方を書いたページを用意しました。詳しくはこちらを見てください。
http://waifu2x-avisynth.sunnyone.org/


ファイルに書き出さなくていいとか、モデルのローディングを初回しかしないとか、中の入出力部分の処理をちょいちょい変えたりしているので、各フレームの画像をコマンドで変換するより若干より速いかと思います。


現時点では、見ての通りびっくりする速度でした(Core i5-3570S。ページ書いたとき、Haswellのマシンだと勘違いしてましたがIvyでした)。特にがんばってはいないので、最適化する余地はたくさんあると思います。しかし、物理で殴ってでも欲しい人がいるかもしれないので、出してみました。