d.sunnyone.org
sunnyone.org

ページ

2012-10-27

MSBuild Launcher 0.0.1をリリース

ここでちょっと触れたMSBuildの良いGUIが見つからなかったので、さくっと作った。

ダウンロードはこちらから。
https://github.com/sunnyone/MsbuildLauncher


MSBuildファイルを開くと、左側にターゲットのボタンができるので、クリックすれば実行される。
コマンドライン引数にも対応しているので、.msbuildを関連付けることも可能。

2012-10-25

C#の"ref"引数のMSIL的取り扱い(.NETと参照渡し)

C#でrefを使ったメソッドのMSIL的作り方/呼び出し方(参照渡しの方法)についてメモしておく。

サマリ

  • メソッドに渡す際は、アドレスを渡す。Cのポインタで言う&valのイメージ。
    • 引数のpushには ldloca命令を使う。
  • メソッド内では、アドレスの解決を行う。Cのポインタで言う*aのイメージ。
    • 取得にはldind命令シリーズ(ldobj)、設定にはstind命令シリーズ(stobj)を使う。
  • 型は、実際の型に&がついた「ref型」を利用する(例: System.Int32&)
    • Type.MakeByRefTypeでref型化、Type.GetElementTypeで非ref型化できる。
    • ref型かどうかは、Type.IsByRefで判断できる。

例

具体例として、参照を使うコードにありがちなSwapメソッドを作ってみる。

C#コード

void Swap(ref int x, ref int y) {
    int tmp;
    tmp = x;
    x = y;
    y = tmp;
}

void SampleFunc()
{
    int x = 10; int y = 20;
    Swap(ref x, ref y);
    System.Console.WriteLine("x, y: {0},{1}", x, y);
}

これをコンパイルしたものを逆アセンブルしたものが以下。
.method private hidebysig instance void  Swap(int32& x,
                                              int32& y) cil managed
{
  .maxstack  2
  .locals init ([0] int32 tmp)
  IL_0000:  nop
  IL_0001:  ldarg.1
  IL_0002:  ldind.i4
  IL_0003:  stloc.0
  IL_0004:  ldarg.1
  IL_0005:  ldarg.2
  IL_0006:  ldind.i4
  IL_0007:  stind.i4
  IL_0008:  ldarg.2
  IL_0009:  ldloc.0
  IL_000a:  stind.i4
  IL_000b:  ret
}

.method private hidebysig instance void  SampleFunc() cil managed
{
  .maxstack  3
  .locals init ([0] int32 x,
           [1] int32 y)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   10
  IL_0003:  stloc.0
  IL_0004:  ldc.i4.s   20
  IL_0006:  stloc.1
  IL_0007:  ldarg.0
  IL_0008:  ldloca.s   x
  IL_000a:  ldloca.s   y
  IL_000c:  call       instance void ConsoleApplication1.Program::Swap(int32&,
                                                                       int32&)
  IL_0011:  nop
  IL_0012:  ldstr      "x, y: {0},{1}"
  IL_0017:  ldloc.0
  IL_0018:  box        [mscorlib]System.Int32
  IL_001d:  ldloc.1
  IL_001e:  box        [mscorlib]System.Int32
  IL_0023:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object,
                                                                object)
  IL_0028:  nop
  IL_0029:  ret
} // end of method Program::SampleFunc

呼ばれるSwapメソッド

適当に端折りながら、上から見ていく。
(評価スタックのイメージを忘れてしまった場合は、この絵を思い出してほしい。)

まずメソッドの定義。
.method private hidebysig instance void  Swap(int32& x,
                                              int32& y) cil managed
int32ではなく、int32&型が使われている。これは、
Type t = typeof(Int32).MakeByRefType();
てな感じで得られる。

そしてtmp = x;の部分。
IL_0001:  ldarg.1
  IL_0002:  ldind.i4
  IL_0003:  stloc.0
これは、
  1. 1番目の引数(ref x)を持ってくる
  2. 持ってきたアドレスからint32の値を持ってくる
  3. 0番目のローカル変数に格納する
というイメージである。

x = y;はいったん飛ばして、y = tmp;部分を見るとこう。
IL_0008:  ldarg.2
  IL_0009:  ldloc.0
  IL_000a:  stind.i4
これは、
  1. 2番目の引数(ref y)を持ってくる
  2. 0番目のローカル変数からint32の値を持ってくる
  3. 持ってきたアドレスに持ってきた値を入れる
というイメージである。

x = yは上記の組み合わせ。
IL_0004:  ldarg.1
  IL_0005:  ldarg.2
  IL_0006:  ldind.i4
  IL_0007:  stind.i4

ここでは、int32なので、ldind.i4/stind.i4だが、型によって使うべき命令は違う。ただし、
すべての ldind 命令は、対応している組み込み値クラスを指定する Ldobj 命令のショートカットです。
(OpCodes.Ldind_I4 フィールド)なので、ldobj [type]でもよい。

ldobj/stobjの例としては、ref int?を使うと、以下の感じで生成される。
IL_0008: stobj valuetype [mscorlib]System.Nullable`1

Swap()を呼ぶメソッド

こちらは実際に呼び出しているところだけ。
IL_0001:  ldc.i4.s   10
  IL_0003:  stloc.0
  IL_0004:  ldc.i4.s   20
  IL_0006:  stloc.1
  IL_0007:  ldarg.0
  IL_0008:  ldloca.s   x
  IL_000a:  ldloca.s   y
  IL_000c:  call       instance void ConsoleApplication1.Program::Swap(int32&,
                                                                       int32&)
10、20をそれぞれx、yに入れたあと、this、xのアドレス、yのアドレスの順に持ってきて、関数をコールしている。

リフレクションでの扱い

すでにあるメソッドのMethodInfoをGetMethod()で持ってきて、ParameterTypeを見ると、
&のついたref型で入っている。この元の型を得たいときは、こんな感じになる。
if (parameterType.IsByRef) {
  Type valueType = parameterType.GetElementType();
}
(Type.MakeByRefType メソッド)

2012-10-21

テキトーに使うWCF

WCFのちょっとした実験をしたいとか、ちょっとしたプロセス間通信をしたいといったときに、app.configだとかServiceReferenceだとかはわずらわしい。そんなときに使うコードサンプル。




2012-10-10

自炊向け本画像処理スクリプト群bkimgproc公開

前回のエントリで予告した、ImageMagickのwrapper script群を公開しました。
https://github.com/sunnyone/bkimgproc
git cloneするか、"ZIP"のボタンを押してzipを落として展開してください。

本1冊ごとにディレクトリを作って連番の素材を入れていき、最後にスクリプトを実行するとまとめてくれるツールです。詳しい使い方は、READMEに載せました。

Ubuntu/DebianであればREADMEの方法で使えますが、普通のshell scriptなので、もしかしたらCygwinでも動くかもしれません。

特徴としては、以下の通りです。
  • pdf, cbz出力(PDFは見開き方向指示に対応)
  • 横向きスキャンデータの回転
  • 白レベル自動判定による白レベル調整
  • kobo向けリサイズ(kobo plugin)
  • 余白の自動カット(kobo plugin)
  • テキストのボールド化(kobo plugin)
使い方にも書いてありますが、白レベル判定は、配置したPDFを単位に行うことで、確実性を高めています。なお、PDFの中でも、偶数と奇数ページで白さが違うことがあったので、別々に判定して対応しています。

PDFの見開き方向指示は、こちらの方法を利用させていただいています。

ちなみにうちでは、convall.shの起動は、Jenkinsを使っています。

2012-09-27

ImageMagickで紙の白レベルを判定する

今日は、一部で「美白化」と呼ばれる、もともと白かった紙の色褪せた部分を白くする処理について。白くするには、どれだけ白っぽくするか、すなわち白とする基準を指定する必要があるが、ImageMagickを利用して、どのレベル以降を白と見做すか判断する方法について、この記事で述べる。

結果だけ使いたい方は、判定ロジックのシェルスクリプトを最後に載せているのでどうぞ。
実行すると、このようになる(以下は95%だった例)
$ ./whitelevel.sh test.jpg
95

この方法(+α)で運用しているが、かなりうまく動いている。

背景

ImageMagickを使って美白化をする例はいくつかWeb上にもあり、-levelを使ってパーセンテージ指定で白レベルを指定するか、-linear-stretchを使ってハイライト側のピクセル数から判断する方法があった。しかし、前者は紙によって異なるし、後者は白い部分が少ない画像で悲しい思いをする可能性があるので、実際の画像から白のレベルを決めたい、というのが背景である。

方針

直感的に言えば、白っぽい部分はヒストグラム上は山になっているはずなので、その直感の方法に合わせて、ヒストグラムを作成し、グラフの傾きから計算すればできそうである。しかし、「ImageMagickでヒストグラムを出力し、数値を計算可能な状態にparseする」「数値をもとに山を求める」というののどちらも面倒くさい。しかも、実際に画像を見てみると、白部分がなだらかだったりして、山になっていないこともある。

ちょっと困っていたが、画像を見ていてふと
「はじっこ(枠)って白くね?」
「ImageMagickって枠切りとれるわ」

という2点に気付き、枠を切り出し、その色を白とすることを思い立った。

もう少し具体的な方針を書くと、以下の通りである。
  1. convertコマンドの-trimを利用して、画像全体のサイズと枠の切り出し座標を得る。
  2. 枠のサイズが、白レベルを判定するのに十分であるかを確認する。
  3. 枠を切り出し、色の平均レベル(μ)と、標準偏差(σ)を算出する。
  4. μ-3σ(正規分布における99.73%の点)を得て、白レベルとする。
  5. (得られたレベルと-levelを利用して、美白化する。)

なお、紙の横方向(綴じてあるほうと、その反対側)は、均一でないことがあるので、余白は上下を用いる。

手順

1. convertコマンドの-trimを利用して、画像全体のサイズと切り出し座標を得る

まず、convertコマンドでは、-trimを使うと、余白を切り取ることができる。
-fuzzオプションを併せて利用すると、その範囲を誤差と見做してくれる。
例としては、以下の通り。
$ convert -trim -fuzz 40% hello.jpg hello-trim.jpg

こんな画像が:

こうなる:


さらに、convertコマンドでは、info:を出力先にすると、画像を出力する代わりに、画像の情報を吐き出してくれる。
$ convert -trim -fuzz 40% hello.jpg info:
hello.jpg JPEG 1338x719 1792x1076+184+136 8-bit DirectClass 0.020u 0:00.010

実は、必要な情報は含まれているので、このままでもいいのだが、-formatというオプションを利用すると、出力フォーマットを調整できる。

まずは、以下の情報を使う。
%H   page (canvas) height
%Y   page (canvas) y offset (including sign)
%h   current image height in pixels
それぞれ、「元画像の高さ」「切り出し縦オフセット」「切り出された画像の高さ」である。

$ convert -trim -fuzz 40%  hello.jpg  -format "%H %Y %h" info:
1076 +136 719

これで、切り出しサイズが得られた。

2. 枠のサイズが、白レベルを判定するのに十分であるかを確認する

端から黒かったりすると、白レベル判定に不十分なので、1.で得たサイズが妥当かどうか、確認する。
上記のサイズから計算するだけなので詳細は省くが、今は、高さが全体の1%以下あるいは25%以上の場合は、白レベル判定不能にしている。

3. 枠を切り出し、色の平均レベル(μ)と、標準偏差(σ)を算出する

サイズ情報を得ているので、-cropを使って画像を切り出し、また-formatとinfo:を使って白部分の平均と標準偏差を得る。今回は、以下の指定を使う。

%[mean]                 CALCULATED: average value statistic of image
%[standard-deviation]   CALCULATED: standard-deviation statistic of image

コマンドとしては、ボールド化のときに使った彩度をゼロにする-modulate 100,0を併せて、以下の形(わかりやすさのため改行を入れている)。
$ convert hello.jpg \
  -modulate 100,0 \
  '(' -clone 0 -crop x$TOP_BLANK+0+0 ')' \
  '(' -clone 0 -crop x+0+$BOTTOM_OFFSET ')' \
  -delete 0 \
  -append -format "%[mean] %[standard-deviation]" info:
64209.3 671.804

これは、上下の双方が白レベル判定として有効だったときで、上下をそれぞれ切り出し、もとの画像を削除したあと、くっつけて情報を得ている。$TOP_BLANKは%Yの値で、$BOTTOM_OFFSETは%Y + %hの値である。

4. μ-3σ(正規分布における99.73%の点)を得て、白レベルとする

この得られた白い部分は、ちゃんと白だけの画像であれば、正規分布だろうと想定して、正規分布では99.73%の値をカバーできる、μ-3σの点を白と判断する。
正規分布のイメージはこちら等で: http://www.cap.or.jp/~toukei/kandokoro/html/14/14_2migi.htm

上記の例では、64209.3 - 3 * 671.804 = 62193.888である。
Maxが65535なので、パーセンテージにすると、655.35で割って、62193.888 / 655.35 = 94.901...である。

これで、95%というレベルが得られた。

(5. 得られたレベルと-levelを利用して、美白化する)

端から色がついていたりして、取得できない画像があるので、もう少し工夫が必要なのだが、一応得られたものを適用するには、-levelを利用して、以下のように処理できる。
$ convert -modulate 100,0 -level ,94% hello.jpg hello-white.jpg

実際にこれを使って白くしたものがこちら。

実装

上記を実装したスクリプトは、以下の通り。


よりうまく使うには

よりうまく使うには、単体の画像ではうまくいかず、画像群に対して使うのが妥当なのだけど、それをするとスクリプトを使う前提の説明がしんどいのでここでは省く。

実は何度か紹介してきたような方法を組合せて、決まった置き方をすると自動で本をまとめるスクリプト群ができていて、うまく動いているのだけど、PDF対応中なので、近日公開予定。


2012-09-22

画像分割ツールKiritoriMage公開

画像分割ツールKiritoriMage version 0.0.1をリリースした。GUIアプリケーションのフリーソフトウェアの新規リリースは、もう10年ぶりくらいになる。

http://github.com/sunnyone/kiritorimage/
ダウンロード: https://github.com/downloads/sunnyone/kiritorimage/KiritoriMage-0.0.1.zip

どんなソフトウェアかというと、区切り線を指定して、その区切られた領域をクリックしていくことで、画像を分割していくツール。以下の画像を見てもらえれば、どんなアプリケーションかわかると思う。


たいていの画像処理ソフトは、1回の保存オペレーションでひとつの範囲しか保存できないのに対し、このツールは複数の範囲が保存できるのが特徴。ちなみに、mageと呼ぶほどすごいことはできない。

自分用なので、保存形式がJPEG固定だったり、保存フォルダは画像と同じフォルダのみ(実は裏技があるのだけど)だったりと、制限は多いのだけど、ひとまずは使えるようになったので公開。

MITライセンスにしているので、改変や再配布も自由。もし拡張とかあればpull requestしてもらえれば。

2012-09-09

Taoを使ってC#/WPFでゲームパッドのボタンを読み取る

ちょっとしたツールを作るために、ゲームパッドをC#で書いたWPFアプリケーションから読もうと思ったが、意外とすっきりいかなかった。結論としては、Tao Frameworkというフレームワークに含まれるWin32 APIのwrapperを使うことで、気軽に読める。

---

ゲームパッド (Joystick? Joypad?)を読むにはDirectInputが定番らしくて、たしかに簡単そうに見えたのでチャレンジしてみたのだけど、managed DirectXを.NET 4で使うにはapp.configを書き替えないといけないとか、SetCooperativeLevelの第一引数がWindows.Windows.Forms.Controlだったりする罠(HWNDも取れるので実は後述の方法でいけるのだけど)にあったりして、挫折した。

ゲームパッドを読むためのAPIがWin32のwinmmというのにあるらしいとわかったが、P/Invokeの定義を書くのが面倒なので探してみたら、Tao Frameworkというゲーム用のフレームワークに定義されているらしい。調べてみると、MITライセンスなので使うことにした。

簡単な使い方

Joystick系のAPIはTao.Platform.Windowsに入っている。Tao Frameworkにはインストーラもあるのだけど、このdllを参照追加すればいいだけなので、zipを落としてdllだけ横に置くのが簡単でいいと思う。

参照追加したら、JOYINFO構造体をnewして、joyGetPos()というメソッドにjoypad IDをセットで渡せばおわり。IDはただの0からの連番。なお、joyGetNumDevs()は、つながっているデバイスの数を返すわけじゃないから注意だ。

for (int i = 0; i < Tao.Platform.Windows.Winmm.joyGetNumDevs(); i++)
{
    Tao.Platform.Windows.Winmm.JOYINFO joyinfo = new Tao.Platform.Windows.Winmm.JOYINFO();
    if (Tao.Platform.Windows.Winmm.joyGetPos(i, ref joyinfo) == Tao.Platform.Windows.Winmm.JOYERR_NOERROR)
    {
        this.textBlock1.Text += String.Format("Joypad[{0}] detected. Button: {1}\n", i, joyinfo.wButtons);
    }
}
パッドによってはもっと詳しい情報が取れるjoyGetPosEx()もあるらしい。自分の用途にはその必要がなかったので使わなかったけど、このへんのAPIの使い方についてはこのへんに書いてある。

イベント的に読み取る1: タイマーで読み取る

もうちょっと実用的に使うために、押されたことを検知したい。

API的には、joySetCapture()というメソッドがあり、これを使うと、ウィンドウメッセージを飛ばすことができる。しかし、引数からしてポーリングっぽい感じなので、WPFアプリケーションであれば、HWNDとか意識しなくていいので、タイマーで見たほうが素直だと思うのでそうした。実装はこのような感じ。


イベント的読み取る2: joySetCaptureで読み取る

それでもjoySetCaptureが使いたいならば、こんなかんじ。 一応動いた。