天泣記

2011-04-10 (Sun)

#1

画像が白黒・グレースケール・カラーのいずれであるかを手で指定するのは動いた。

だがまぁ、完璧でなくてもいいなら機械にもできるわけである。

というわけで画像を色でソートすることを考えた。うまくソートすればだいたい分類されて、手で指定するのも楽になる。

具体的にはたとえば、カラー画像というのは彩度の高い色がたくさん入っている画像である。というわけで、RGB を HSV あたりに変換して、S の平均でソートすればいいのではなかろうか。S の平均が高いほうがカラー画像であろう。

しかし、これをやってくれるツールが見当たらない。

しばらく探した中で一番近かったのが ImagiMagick の identify -verbose である。これの出力の中には以下のように RGB それぞれの最小・最大・平均・標準偏差が含まれる。

Channel statistics:
  Red:
    Min: 0 (0)
    Max: 255 (1)
    Mean: 178.424 (0.699703)
    Standard deviation: 72.5375 (0.284461)
  Green:
    Min: 0 (0)
    Max: 255 (1)
    Mean: 167.423 (0.656562)
    Standard deviation: 86.4846 (0.339155)
  Blue:
    Min: 0 (0)
    Max: 255 (1)
    Mean: 170.658 (0.669249)
    Standard deviation: 83.569 (0.327722)

これの HSV 版が欲しいわけである。(今回の分類にはとりあえず H は不要だが。)

どうしたものかと思ったのだが、けっきょく自分で書くことにした。入力が PNM なら、きっと narray で済むだろう。

... というわけで書いてみたところ、raw な PPM で max が 255 以下ならたしかに難しくないことが分かった。

読み込みのところを抜粋すると以下のようになる。

WSP = /(?:[ \t\r\n]|\#[^\r\n]*[\r\n])+/

content = File.open(fn, 'rb') {|f| f.read }
if /\A(P[635241])#{WSP}(\d+)#{WSP}(\d+)#{WSP}(\d+)[ \t\r\n]/o !~ content
  raise ArgumentError, "unsupported format"
end
magic = $1
raise ArgumentError, "unsupported format" if magic != "P6"
w = $2.to_i
h = $3.to_i
max = $4.to_i
raise ArgumentError, "unsupported max value: #{max}" if 256 <= max
na = NArray.to_na($', "byte", 3, w, h)

正規表現でヘッダを切り出し、NArray.to_na に残りのバイト列を渡す。

PNM の中身は 6 種類ある。{カラー(PPM),グレースケール(PGM),白黒(PBM)}*{raw,plain} の 6種類である。raw というのは普通にバイナリなのに対し、plain は ASCII で (10進整数で) 値が格納される。

さて、上で扱っている P6 というのは raw な PPM で、(max が 255以下なら) RGB それぞれ 1byte で 1pixel あたり 3byte が w*h 個並んでいる。これは NArray.to_na で素直に扱える。

max が 256 以上の場合は厄介である。その場合 RGB それぞれが unsigned な 2byte (big endian) になるのだが、NArray の 2byte 整数は signed なので素直に当てはまるものがない。

PGM は PPM と同じである。raw であり、max が 255以下であれば素直に扱える。

plain の場合は NArray.to_na に直接渡すことはできない。

PBM は、ビットの並びなので、これも NArray には素直に当てはまるものはない。

というわけで、6種類中、素直に NArray.to_na で扱えるのは raw な PPM/PGM で max が 255 以下の場合だけであった。

とはいえ今回は素直に扱えるもので済むのでこれでやってみよう。

Wikipedia の HSV の項 によると RGB から HSV (の S,V) への変換は以下のようにすればいいらしい。

MAX = max(R,G,B)
MIN = min(R,G,B)

S = (MAX-MIN)/MAX
V = MAX

これを narray で以下のように書いてみた。もっと簡単に書けるかもしれないが。

r = na[0,true,true].reshape(w*h)
g = na[1,true,true].reshape(w*h)
b = na[2,true,true].reshape(w*h)
flags = r < g
min_rg = flags * r + (1-flags) * g
max_rg = flags * g + (1-flags) * r
flags = min_rg < b
min_rgb = flags * min_rg + (1-flags) * b
flags = max_rg < b
max_rgb = flags * b + (1-flags) * max_rg
den = max_rgb + max_rgb.eq(0)
v = max_rgb
s = (max_rgb - min_rgb).to_f / den

あとは {v,s}.{min,max,mean,stddev} で最小・最大・平均・標準偏差が求められる。

で、やってみた結果、カラー画像は予想通り彩度が高い方で確実に分類できる。

それに比べるとグレースケールはそこまではうまくいかない。明度 (平均でも標準偏差でもどちらでも) で白黒から分離できるが、カラーとは混ざってしまう。

2011-04-14 (Thu)

#1

ScanSnap S1500 に長尺読取機能があることに気がついた。

いままで文庫本のカバー (広げると 400mm 以上ある) をいっきにスキャンできず半分位に切っていたのだが、長尺読取だと 863mmまで読めるそうなので切る必要がなくなる。

早速やってみよう、というわけで試すが、うまくいかない。ボタンを長押しすると長尺モードになるという話だが、依然としてスキャンしてくれない。(scanimage コマンドで)

まぁ、scanimage コマンドだとやりかたが変わってくるのだろう、と思っていくつか試すと、scanimage に -y 876.695 --page-height 876.695 --ald=yes とオプションをつけてうまくいった。

(--ald=yes は以前から使っていたが、読み取りサイズよりも縦に短い紙が来たときに、紙の終わりでスキャンを打ちきるオプションである。)

#2

さて、カバーを一つの画像として読み取るようにすると、ひとつ問題が生じた。

これまで、複数の本をブラウザで表示するとき、最初の画像を代表として表示していたのだが、その画像が適切ではなくなってしまったのである。表紙画像は代表として適切だと思うが、表紙の部分が画像の中で小さくなりすぎてしまうのである。

というわけで、カバーの中で表紙部分を切り出して、別画像にしたくなった。しかし、手動でやるのは面倒である。自動で可能だろうか?

しばらく考えて、可能であるという結論に達した。

とりあえず、いろんな誤差は無視して考える。

本のカバーは、表表紙側の折込部分・表表紙・背表紙・裏表紙・裏表紙側の折込部分、という 5つの部分からなる。このうち、表表紙だけ (もしくはついでに背表紙) を自動的に取り出せるか、というのが問題である。

--ald=yes により、画像サイズから、上記の 5つを合わせた幅は分かる。(実際の画像は 90度回転しているので幅じゃなくて高さだが、人間がみたときの方向で考えよう)

つぎに、表表紙と裏表紙の幅は同じである。そして、その幅は、本文のページの幅と同じである。

背表紙の幅 (つまり本の厚さ) については、カバー下 (本体表紙) のスキャン結果から得ることにした。カバー下は表表紙・背表紙・裏表紙が連続した厚紙で、手で簡単にはがせる。裁断する前にはがして、一枚の画像としてスキャンするとその幅が得られる。これに本文のページ幅を 2回減じれば背表紙の幅になる。

ここで折込部分の幅が表表紙側と裏表紙側で等しいと仮定すると、カバーの中の 5つの部分の長さが同定できる。

まぁ、誤差はあるので、欲しいところにたいしていくらか大きめに切り出せばいいだろう。

さて、実装としては 3つの画像 (カバー、カバー下、本文) のファイル名を指定するというのが素朴だが、それも面倒なので、自動的にそれらを探すようにしてみよう。

戦略はこんなである。カバーはもっとも幅の広い画像である (同じ幅が複数あったら最初の画像)。本文は幅で画像をソートして、中央にある画像である。カバー下は、本文の画像の幅の 2倍よりも幅広い中で、もっとも狭い画像である (同じ幅が複数あったら最初の画像)。

口絵が折り込まれていたりすると怪しいが、うまくいかなかったらファイル名を指定すればいいので、だいたいうまくいけばいい。

で、やってみたところ、帯に邪魔された。帯はカバーよりも微妙に幅が広いようだ。まぁ、外側に配置されるんだからそりゃそうか。ad hoc だが、もっとも広い幅の 9割以上の幅をもつ画像の中で、もっとも狭い画像ということにした。

それでだいたいうまくいくようになった。

2011-04-16 (Sat)

#1

金色の帯があって、うまくスキャンできない。黒くなってしまう。

検索すると、金色や銀色はスキャンしにくいもののようである。スキャナ内部で、光源の発した光がセンサに届かないのであろう。金色や銀色は鏡の一種であって反射光に指向性があると考えれば、それは納得できる。

改善策としてはトレーシングペーパーとか、半透明で乱反射するものを敷く、というのがあったが、それはフラットベッドスキャナの話で、ScanSnap S1500 では難しそうである。

と、いうところで、ScanSnap S1500 には A3キャリアシートなるものが付属していたことを思いだし、それで帯を挟んでスキャンするといくらか良くなった。

ただ、帯は長すぎて A3キャリアシートに入りきらないという問題はある。半分に折れば入るけれど。

2011-04-24 (Sun)

#1

Debian BTS #623843: typo in pnmhisteq.1: pgnnorm


[latest]


田中哲