ブンバボーンな毎日

RubyやPython, JavaScriptなど勉強したことなど、IT関連の記事を書いています

python画像処理入門6 2値化と3つの閾値たち

こんにちは、エンジニアのさもです。

今回は画像認識の前処理などで使われる2値化を実装してみます。

目次

はじめに

2値化とは、画像を白と黒の2色で塗り分ける処理を言います。

2値化することで、画像認識・検出がやりやすくなり、処理も高速になります。

今回は以下の画像を2値化したいと思います

f:id:s-uotani-zetakansu:20170904142000j:plain

2値化すると下のような画像になります

f:id:s-uotani-zetakansu:20170904141621j:plain

スポンサーリンク

実装

2値化のアルゴリズムは簡単です。

各画素値がある閾値を超えている場合は白、超えていなかったら黒というように塗り分けるだけです。

もし以下の画像処理のコードで何をしているのか分からないところがあれば、こちらの記事で詳しく解説しているので、ぜひご覧になってください。

www.uosansatox.biz

また、以下ではimagesフォルダのmori.jpgという画像を使っていますが、お使いの環境に合わせて変えてください

閾値を128にしてみる

0~255のちょうど中間である128を閾値に使ってみます。

また、今回使う画像はカラー(RGB)なので、r,g,bの平均が128を超えると白を塗り、超えていなかったら黒を塗るというようにします。

from PIL import Image, ImageDraw
import numpy as np

# 色を判定して、白または黒を返す
# 引数のthresholdが閾値
def select_color(threshold, color):
  # r,g,bの平均を求める
  mean = np.array(color).mean(axis=0)
  if mean > threshold:
    return (255, 255, 255)
  else:
     return (0, 0, 0)

# 2値化した画像を返すメソッド
def to_bin(img, w, h):
  bin_img = Image.new('RGB', (w, h))
  threshold = 128

  # select_colorメソッドを使って塗る色を決めながら、新しい画像を作っていく
  np.array([[bin_img.putpixel((x, y), select_color(threshold, img.getpixel((x,y)))) for x in range(w)] for y in range(h)])
  return bin_img

# 画像ファイルの読み込み
img = Image.open("../images/mori.jpg").convert("RGB")
w, h = img.size
# 画像を2値化して保存する
to_bin(img, w, h).save("./bin_mori.jpg")

実行結果は以下のようになるかと思います。

f:id:s-uotani-zetakansu:20170904143245j:plain

うまくいってそうですね。

ですが、画像全体が暗い(ほとんど128未満になってしまう)または全体的に明るい場合には真っ黒or真っ白の画像が出力されてしまい、期待した処理結果にはなりません。

そこで、閾値を画像全体の中央値になるように改良したいと思います

中央値を閾値に使う

ここで中央値は、各画素値のr,g,bの平均を大きい順に並べたとき、ちょうど真ん中にくる値です。

from PIL import Image, ImageDraw
import numpy as np

def select_color(threshold,color):
  mean = np.array(color).mean()
  if mean > threshold:
    return (255, 255, 255)
  else:
     return (0, 0, 0)

def to_bin(img, w, h):
  # 各画素値のr,g,bの平均を集めた配列を作る
  thresholds = []
  np.array([[thresholds.append(np.array(img.getpixel((x,y))).mean()) for x in range(w)] for y in range(h)])

  # sortで並べ替えて、真ん中の値を閾値にする
  threshold = np.sort(np.array(thresholds))[w * h // 2]

  bin_img = Image.new('RGB', (w, h))
  np.array([[bin_img.putpixel((x, y), select_color(threshold, img.getpixel((x,y)))) for x in range(w)] for y in range(h)])
  return bin_img

# 画像ファイルの読み込み
img = Image.open("../images/mori.jpg").convert("RGB")
w, h = img.size
# 画像を2値化して保存する
to_bin(img, w, h).save("./bin_median_mori.jpg")

実行すると以下のようになります。

f:id:s-uotani-zetakansu:20170904144129j:plain

これで全体的に暗い画像でもちょうど全体の半分の画素は黒くなり、もう半分は白くなりました。

しかしこの場合も少し問題があります。

例えば、画像のなかで灰色の文字が書いてあるとします。文字全体では少しずつ色が異なっていると思うのですが、中央値を使う場合だと、文字の少し色の薄いところが白になり、少し濃いところが黒になってしまうかもしれません。

つまり、色の濃さが似ているのに、白と黒に塗り分けられてしまい、不自然な画像になってしまいます。

そこで、色の濃さの「かたまり」を考慮して塗り分ける必要があります。有名な方法に「大津の方法」というのがあります。

大津の方法

詳しいアルゴリズムや理論の部分は、以下のサイトが分かりやすかったので、こちらを参照してください。

判別分析法(大津の二値化) 画像処理ソリューション

実装は以下になります

from PIL import Image, ImageDraw
import numpy as np

def select_color(threshold,color):
  mean = np.array(color).mean()
  if mean > threshold:
    return (255, 255, 255)
  else:
     return (0, 0, 0)

def to_bin(img, w, h):
  #各画素値のr,g,bの平均を求める
  means = np.array([[img.getpixel((x,y)) for x in range(w)] for y in range(h)]).mean(axis=2).reshape(w * h,)

  # ヒストグラムを作る
  hist = np.array([np.sum(means == i) for i in range(256)])

  max_v = 0
  threshold = 0
  # 0から255まで順に計算し、適切な閾値を求める
  # 閾値より大きい画素値をクラス1、小さい画素値をクラス2とする
  for th in range(256):
    n1 = sum(hist[:th])                                 # クラス1の個数
    m1 = np.dot(hist[:th], np.array(range(256))[:th])   # クラス1の値の平均
    n2 = sum(hist[th:])                                 # クラス2の個数
    m2 = np.dot(hist[th:], np.array(range(256))[th:])   # クラス2の値の平均
    if n1 == 0 or n2 == 0:
      v = 0
    else:
      # クラス間分散の分子を求める
      v = n1 * n2 * (m1 / n1 - m2 / n2) ** 2
    # クラス間分散の分子が最大となる閾値を更新していく
    if max_v < v:
      max_v = v
      threshold = th

  bin_img = Image.new('RGB', (w, h))
  np.array([[bin_img.putpixel((x, y), select_color(threshold, img.getpixel((x,y)))) for x in range(w)] for y in range(h)])
  return bin_img

# 画像ファイルの読み込み
img = Image.open("../images/mori.jpg").convert("RGB")
w, h = img.size
# 画像を2値化して保存する
to_bin(img, w, h).save("./bin_otu_mori.jpg")

実行すると以下のようになります

f:id:s-uotani-zetakansu:20170904145428j:plain

最後に

今回は2値化を実装してみました。

こういう処理はOpenCVを使うと簡単にできると思うのですが、エンジニアである限り、せめて趣味の中では何かを作りたいですよね。

高機能なものを使うのも面白いですが、使う人ではなく、作る人でありたいと思います。

読者登録をしていただけると、ブログを続ける励みになりますので、よろしくお願いします。