それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

画像が、白黒かカラーか判定する。(白黒に近いスキャン画像を判定する)

画像がカラーか、白黒かを判別する方法では、偶然にはてな質問の回答を見つけたので、コピペで行っていた。正直言って何がどうなってるのかよくわかってなかった。自宅の書籍や書類を整理するために大量にスキャンしていると、やっぱりこの判定がほしいなと思った。そのためもう一度見直すことにした。

画像がカラー・モノクロを判定する。

convert コマンドで画像を変換する、閾値より上下で2値にする、最後にmean(平均)を取り出す。

convert 001.jpg  \
  -colorspace HSB \ 
  -separate \
  -delete 0 \
  -fx "u*v" \
  -blur 2x2 \ 
  -threshold 30% \
  -format '%[fx:mean]\n' info:

もともとの回答者のコメントをみると、Convertで何をしているのかが見える。

colorspace HSB : 色空間をHSBに
separate : HSBになってる各チャンネルを分離して3枚のモノクロ画像に
delete 0 : 最初の0番の画像(色相)を削除
fx "u*v" : 1番(彩度)と2番(明度)の画像の各ピクセルを掛け合わせる。これで円柱型から円錐型のHSB空間における彩度になる
blur 2x2 : ノイズ除去用にぼかし
threshold 30% : 彩度30%を閾に二値化
format '%[fx:mean]' : 全体の平均値。30%で二値化しているので30%以上となったピクセルの割合になる。
書籍の自炊画像から、カラーページとモノクロページを判別するプ… - 人力検索はてな

サンプル画像を見ながら試す。

手順通りにやってみる。

元画像

HSBにする

RGBをHSBにする。(画像自体は変わらない。)

convert 001.jpg \
 -colorspace HSB \
 a-color.jpg

セパレートする

カラースペース(HSB:Hue色相・Saturation彩度・Brightness明度)に分割した状態で保存する。3枚になる。

convert 001.jpg \
 -colorspace HSB \
 -separate\
 b-color.jpg

b-color-0.jpg (Hue)

b-color-1.jpg (Sat)

b-color-1.jpg (Bri/V)

Hueを削除

上記のところから、インデックス=0であるHueを削除したもの

convert 001.jpg \
 -colorspace HSB \
 -separate \
 -delete 0 \
 c-color.jpg

2枚できる。出力に変化がないので、サンプル省略。

2枚を乗算

S,B の2枚を乗算する。オーバーレイ。彩度を外したので、色がないものになる。

convert 001.jpg \
 -colorspace HSB \
 -separate \
 -delete 0 \
 -fx 'u*v'\
 d-color.jpg

ぼかす。

細かいところがノイズにならないようちょっとぼかす

convert 001.jpg \
 -colorspace HSB \
 -separate \
 -delete 0 \
 -fx 'u*v' \
 -blur 2x2 \
 e-color.jpg

( blur 2,2 程度ではあまり変化はない、逆に考えると、見た目には変わらない程度に、ゴミが除去されたということ)

30% で2値化

30% ( 色範囲 0-65535の30%なので、19660>x , 19660<x ) の2値に分割する。

convert 001.jpg \
 -colorspace HSB \
 -separate \
 -delete 0 \
 -fx 'u*v' \
 -blur 2x2 \
 -threshold 30% \
 f-color.jpg

指定した閾値より濃いところは黒に、薄いところは白に2値化されている。

全体の平均を取る。

この状態で、全体の平均(白黒のピクセル数を集計し平均を取る、ツイッタのサムネイルの背景色、dominant_colorsを計算するようなもの)

convert 001.jpg \
 -colorspace HSB \
 -separate \
 -delete 0 \
 -fx 'u*v' \
 -blur 2x2 \
 -threshold 30% \
 -format '%[fx:mean]\n'\
 info:

結果は次のようになっている。

0.216293

これは、0-65535の20(=14174)である。

この結果から、2値化して、色数が多い事がわかるので、カラーであるとわかる。

ぎゃくに、書籍のように白背景に黒文字、など色数が少ないものは、もっと小さな値になる。

書籍の例

たとえば、次のような画像で計算すると

convert dora.jpg  -colorspace HSB  -separate  -delete 0  -fx 'u*v'  -blur 2x2  -threshold 30%  -format '%[fx:mean]\n' info:

結果は、ゼロである。

0

次の画像は、0.21916 である。

このことから、画像が白黒に近いほど数字は0にちかづき、30%(0.3)を閾値にしているので、0.3より小さければ、ほぼ線画であるようなきがする。

ruby でやってみる。

このヒューリスティックアルゴリズムで本当に大丈夫なのか。画像を変えて調べたい。

大量に調べると画像の変換や一時ファイルへの書き出しがめんどくさいので、プログラムからオンメモリで処理していきたい。

とりあえず、手元ですぐ使えそうなruby を使ってみる。

mkdir work 
cd work 
bundle init 
sudo apt install libmagickwand-dev 
bundle add pry 
bundle add rmagick

コードを書いてみる。

#!/usr/bin/env ruby

require 'rmagick'
require 'pry'

def mean(file)
  blob = File.open(file).read
  i = Magick::Image.from_blob(blob).first
  i.resize_to_fit!(600)
  i.colorspace=Magick::HSBColorspace
  h = i.channel(Magick::HueChannel)
  v = i.channel(Magick::LuminosityChannel)
  u = i.channel(Magick::SaturationChannel)
  h.write('x-b-color-0.jpg')
  u.write('x-b-color-1.jpg')
  v.write('x-b-color-2.jpg')
  i = u.composite(v,0,0,Magick::MultiplyCompositeOp)
  i.write('x-d-color.jpg')
  i.blur_image(2,2)
  i.write('x-e-color.jpg')
  f = i.dup.bilevel_channel( Magick::QuantumRange * 0.3 )
  f.write('x-f-color.jpg')
  r = f.channel_mean(Magick::GrayChannel).map { |x| x / Magick::QuantumRange }[0]
end
(1..30).each{|i|
  f =  sprintf('%03d.jpg',i)
  r = mean(f)

  puts sprintf("%5s: %05f", f, r )
}

スキャン・スナップで取り込んだ書籍で試してみる。 実行してみる

bundle exec ruby rmagick.rb
001.jpg: 0.114141
002.jpg: 0.000000
003.jpg: 0.000000
004.jpg: 0.000000
005.jpg: 0.000000
006.jpg: 0.000000
007.jpg: 0.000000
008.jpg: 0.000000
009.jpg: 0.000000
010.jpg: 0.000000
011.jpg: 0.000000
012.jpg: 0.000000
013.jpg: 0.000000
014.jpg: 0.000000
015.jpg: 0.000000
016.jpg: 0.000000
017.jpg: 0.000000
018.jpg: 0.000000
019.jpg: 0.000000
020.jpg: 0.000000
021.jpg: 0.000000
022.jpg: 0.000000
023.jpg: 0.000000
024.jpg: 0.000000
025.jpg: 0.000000
026.jpg: 0.000000
027.jpg: 0.000000
028.jpg: 0.000000
029.jpg: 0.000000
030.jpg: 0.019163

あー良いなこれ。

ruby rmagick と convert のちがい

チャンネルやカラースペースを入れると、ImageListになる。

thredshold の箇所が、2値化を示す bi-level-channel という、わかりやすい名前になっている(しかしその分調べにくい。)

i.dup.bilevel_channel( Magick::QuantumRange * 0.3 )

なるほど、ね。

MagickよりOpenCVで調べたほうが楽だったか。

rmagick で画像処理してる人いなくて調べるのめんどくさかった。OpenCVPythonにすればよかった。

OpenCV使うともっと楽な方法が見つかった。

rgb でグレースケールを判定

そもそも、画像がグレースケール(2値化)に処理されている、処理済みの画像であれば、もっと簡単なのですよね。

#!/usr/bin/env ruby

require 'rmagick'
require 'pry'

def rgb(file)

  blob = File.open(file).read
  i = Magick::Image.from_blob(blob).first
  i.resize_to_fit!(600)
  r = i.channel(Magick::RedChannel)
  g = i.channel(Magick::GreenChannel)
  b = i.channel(Magick::BlueChannel)

  return r == g && g == b
end


(1..30).each{|i|
  f =  sprintf('%03d.jpg',i)
  rr = rgb(f) ? 'gray': 'color'

  puts sprintf("%5s: %5s", f, rr )
}

たしかに、もともとの画像がすでにグレースケールであれば、ピクセル計算でチェックせずに簡単に判定が可能ですね。

参考資料

https://stackoverflow.com/questions/21294036/

https://teratail.com/questions/372846

https://chantastu.hatenablog.com/entry/2022/09/07/233026

https://sabopy.com/py/scikit-image-4/