ruby+opencvで画像処理(2値化)
画像処理の授業取っててruby+opencvで実装したのでその時のことをメモメモφ(・・
opencvといえばc++かpythonってイメージがあるけど、最近勉強中のrubyで実装したいなーと思ったのでしてみた。
開発環境
rubyでopencvを使うためのgemがopencv3系に対応していないのでこんな感じ。
インストール関係で叩いたコマンドは以下の通り。 地味にハマったのはopencv3も入っているからオプションでopencv2の方を見るようにしないといけなかったってところ。
$ brew install opencv $ gem install ruby-opencv -- --with-opencv-dir=/usr/local/Cellar/opencv/2.4.13.2
2値化とは
画像を白(1)か黒(0)の2つの値のみにすること。 色の情報が必要では無く形に着目するときとかに使うと良い。 また、情報量を減らせるので計算の面でもシンプル&高速になるとかなんとか。
現実世界やと、駐車場でナンバープレート見て清算済みなら勝手に開けてくれるとかあるけど、ああいうときに使われているらしい。
(雑な理解やけど多分だいたいあってる)
2値化の仕方
さて、どうやって2値化するかって話やけど、中身はすごい簡単。 各ピクセルの濃度値が閾値より大きければ黒、小さければ白にするだけ。 自分で実装してもそんなに大変ではないけど、opencvには閾値を渡せば2値化してくれるメソッドがあるので凄い簡単。
2値化するところは以下の通り。
カラーの画像をグレースケールにしてから、threshold()
を呼べば終わり。
正直引数については詳しく調べてなくて、255は8bitカラーの最大値からきててとりあえずこう書いとけばいいという雑な理解。
def binarize(file, t) image = CvMat.load(file) gray_img = image.BGR2GRAY bin_img = gray_img.threshold(t.to_i, 255, :binary) end
2値化は簡単にできるけど、問題になるのはその閾値をどうやって決めるかという話になって、それはいくつかアルゴリズムがあるらしい。
授業で出てきたのは、
の4つ。
微分ヒストグラム法
微分という名前の通り、濃度値の変化が大きいところに着目して閾値を決める方法。
ヒストグラム法やからヒストグラムを作るんやけど、どういうものかというと横軸が濃度値、縦軸が微分値の和になるようなヒストグラム。
多分普通は縦軸が単に画素数になるから、ここでは微分値を重みとしてちょっと変化させてるという感じ。
微分値の定義としては、| (基準となる画素の濃度値) - (隣の画素の濃度値) |
を周辺8マス分計算してその総和を取る。
ヒストグラムを作るところは以下のように実装した。
def create_diff_histogram(gray_img) hist = Array.new(256, 0) [*0...gray_img.rows].product([*0...gray_img.cols]).each{ |(y, x)| b = gray_img.at(y, x) diff = 0 # 周辺8マスとの差分の絶対値の総和の出す. # 自分自身との差は0になって結果に影響ないので9マス分計算している. [*y-1..y+1].product([*x-1..x+1]).each{ |(y2, x2)| if y2.between?(0, gray_img.rows-1) && x2.between?(0, gray_img.cols-1) diff += (b[0] - gray_img.at(y2, x2)[0]).abs end } hist[b[0]] += diff } hist end
全てのピクセルを網羅するためにx座標とy座標の直積を取る必要があって、それは[*0...gray_img.rows].product([*0...gray_img.cols]).each
と書いた。
product()
を使うと直積を取ってくれるのだが、配列で渡す必要があるので[*0...gray_img.rows]
としてrangeではなく配列にした。
個人的にはこの行がなんかもうちょっといい感じに書けないのかなーと思ってるけど、書き方がわからないのでこのまま。
各座標における色はat(y, x)
で取れる。
このat(y, x)
の返り値はCvScalar
型になってて、カラーだとRGB値、グレースケールだと輝度値が入っている。
CvScalar
は4つのdoubleを持つ配列になっているので、b[0]
のような書き方になる。
ちなみに、カラーのときはBGRの順に値を持っているので注意が必要。
あと、個人的に引っかかった点は引数が(x, y)でなく(y, x)だったこと。 最初何も考えずに(x, y)で書いていたのでout of rangeでエラー吐きまくってた。
微分値を計算するところは、周辺8マスを計算するためにまた直積を書いて実装している。 この書き方やと縦横3×3の9マスを網羅してて基準になるマスも入るけど、計算結果は変わらないので9マス分計算している。 あとは、端っこのピクセルやと隣がいない場合があるのでそのためのチェックもしてる。
普通なら2重ループになる直積が一行で書けるのはいいなーと思うけど、なんとなく読みづらいのが直したいポイント.
判別分析法
判別分析法は別名大津の2値化ともいうらしい。
ある値を閾値にしたときの分離度なる値を算出してそれが最大になるような閾値を採用するというアルゴリズム。 分離度は、閾値を元にそれより暗い/明るいの2つのグループに分けて、それぞれのグループでの分散を求めて、クラス内分散やクラス間分散を定義して、って感じで定義される。 この分離度だが、今回はその最大値を求めるという点で大小関係の比較ができれば良いので、結局各グループ内の画素数と濃度値の平均があれば十分になる。
数式の詳しい説明はちょっと長くなるのでこちら。
実装としては、閾値をある値にした時の分離度を求めて、それが最大になる閾値を返すというメソッドを作るだけなので簡単で、以下の通り。
def discriminant(file) image = CvMat.load(file) gray_img = image.BGR2GRAY hist = create_histogram(gray_img) eval_value, best_t = 0, 0 (0...255).each{ |t| # t := 閾値 # w1(w2) := 左側(右側)の画素数 # m1(m2) := 左側(右側)の平均 w1, w2 = 0, 0 m1, m2 = 0, 0 w1 = hist[0..t].reduce(:+) m1 = hist[0..t].map.with_index {|n, idx| n * idx}.reduce(:+)/w1 rescue 0 w2 = hist[t+1..255].reduce(:+) m2 = hist[t+1..255].map.with_index(t+1) {|n, idx| n * idx}.reduce(:+)/w2 rescue 0 e = w1 * w2 * (m1 - m2) ** 2 if eval_value < e eval_value = e best_t = t end } best_t end
create_histogram()
は横軸が濃度値、縦軸が画素数のシンプルなヒストグラムを返すように実装したメソッド。
indexが濃度値、値が画素数の配列の形でデータを保持している。
画素数は単に配列の値を足していけば良いだけなのでreduce(:+)
するだけ。
平均値は、画素数×濃度値を計算するためにindexと値が両方なので、map.with_index{|n, idx| n * idx}
のようにしてまず画素数×濃度値を計算した。
そのあとは同じくreduce(:+)
で総和を求めて、画素数w
で割って終わり。
画素数w
は0になることがあるのでZeroDivisionErrorの時のためにrescue 0
を書いている。
ただ、この書き方をすると他のエラーを握りつぶしてしまうのでもしかしたらよくないかも。
あと工夫しているところは閾値より大きいところを見るときにhist[t+1..255].map.with_index(t+1)
という書き方をしているところ。
配列の後ろ側を切り取ってからmapするときに、もとの配列でのindexを使いたい場合はwith_index(t+1)
とすることでindexを任意の値から数えることができる。
実行結果
画像処理定番のlenna使ってみました。 lennaだと2つの手法の差があまり出なくて向き不向きみたいなのが見えなくて残念やけど。
元画像
グレースケールにした画像
判別分析法を使って2値化した画像(閾値=122)
最後に
一応プログラムはgithubにあげてます。
今後も授業でいろいろ実装していく予定なので適宜まとめていこうかなと思ってます。