画像が白黒・グレースケール・カラーのいずれであるかを手で指定するのは動いた。
だがまぁ、完璧でなくてもいいなら機械にもできるわけである。
というわけで画像を色でソートすることを考えた。うまくソートすればだいたい分類されて、手で指定するのも楽になる。
具体的にはたとえば、カラー画像というのは彩度の高い色がたくさん入っている画像である。というわけで、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} で最小・最大・平均・標準偏差が求められる。
で、やってみた結果、カラー画像は予想通り彩度が高い方で確実に分類できる。
それに比べるとグレースケールはそこまではうまくいかない。明度 (平均でも標準偏差でもどちらでも) で白黒から分離できるが、カラーとは混ざってしまう。
ScanSnap S1500 に長尺読取機能があることに気がついた。
いままで文庫本のカバー (広げると 400mm 以上ある) をいっきにスキャンできず半分位に切っていたのだが、長尺読取だと 863mmまで読めるそうなので切る必要がなくなる。
早速やってみよう、というわけで試すが、うまくいかない。ボタンを長押しすると長尺モードになるという話だが、依然としてスキャンしてくれない。(scanimage コマンドで)
まぁ、scanimage コマンドだとやりかたが変わってくるのだろう、と思っていくつか試すと、scanimage に -y 876.695 --page-height 876.695 --ald=yes とオプションをつけてうまくいった。
(--ald=yes は以前から使っていたが、読み取りサイズよりも縦に短い紙が来たときに、紙の終わりでスキャンを打ちきるオプションである。)
さて、カバーを一つの画像として読み取るようにすると、ひとつ問題が生じた。
これまで、複数の本をブラウザで表示するとき、最初の画像を代表として表示していたのだが、その画像が適切ではなくなってしまったのである。表紙画像は代表として適切だと思うが、表紙の部分が画像の中で小さくなりすぎてしまうのである。
というわけで、カバーの中で表紙部分を切り出して、別画像にしたくなった。しかし、手動でやるのは面倒である。自動で可能だろうか?
しばらく考えて、可能であるという結論に達した。
とりあえず、いろんな誤差は無視して考える。
本のカバーは、表表紙側の折込部分・表表紙・背表紙・裏表紙・裏表紙側の折込部分、という 5つの部分からなる。このうち、表表紙だけ (もしくはついでに背表紙) を自動的に取り出せるか、というのが問題である。
--ald=yes により、画像サイズから、上記の 5つを合わせた幅は分かる。(実際の画像は 90度回転しているので幅じゃなくて高さだが、人間がみたときの方向で考えよう)
つぎに、表表紙と裏表紙の幅は同じである。そして、その幅は、本文のページの幅と同じである。
背表紙の幅 (つまり本の厚さ) については、カバー下 (本体表紙) のスキャン結果から得ることにした。カバー下は表表紙・背表紙・裏表紙が連続した厚紙で、手で簡単にはがせる。裁断する前にはがして、一枚の画像としてスキャンするとその幅が得られる。これに本文のページ幅を 2回減じれば背表紙の幅になる。
ここで折込部分の幅が表表紙側と裏表紙側で等しいと仮定すると、カバーの中の 5つの部分の長さが同定できる。
まぁ、誤差はあるので、欲しいところにたいしていくらか大きめに切り出せばいいだろう。
さて、実装としては 3つの画像 (カバー、カバー下、本文) のファイル名を指定するというのが素朴だが、それも面倒なので、自動的にそれらを探すようにしてみよう。
戦略はこんなである。カバーはもっとも幅の広い画像である (同じ幅が複数あったら最初の画像)。本文は幅で画像をソートして、中央にある画像である。カバー下は、本文の画像の幅の 2倍よりも幅広い中で、もっとも狭い画像である (同じ幅が複数あったら最初の画像)。
口絵が折り込まれていたりすると怪しいが、うまくいかなかったらファイル名を指定すればいいので、だいたいうまくいけばいい。
で、やってみたところ、帯に邪魔された。帯はカバーよりも微妙に幅が広いようだ。まぁ、外側に配置されるんだからそりゃそうか。ad hoc だが、もっとも広い幅の 9割以上の幅をもつ画像の中で、もっとも狭い画像ということにした。
それでだいたいうまくいくようになった。
金色の帯があって、うまくスキャンできない。黒くなってしまう。
検索すると、金色や銀色はスキャンしにくいもののようである。スキャナ内部で、光源の発した光がセンサに届かないのであろう。金色や銀色は鏡の一種であって反射光に指向性があると考えれば、それは納得できる。
改善策としてはトレーシングペーパーとか、半透明で乱反射するものを敷く、というのがあったが、それはフラットベッドスキャナの話で、ScanSnap S1500 では難しそうである。
と、いうところで、ScanSnap S1500 には A3キャリアシートなるものが付属していたことを思いだし、それで帯を挟んでスキャンするといくらか良くなった。
ただ、帯は長すぎて A3キャリアシートに入りきらないという問題はある。半分に折れば入るけれど。
[latest]