webエンジニアの日常

RubyやPython, JSなど、IT関連の記事を書いています

pythonによる画像処理入門part3 塗り絵を生成してみる

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

pythonによる画像処理入門の第三回目は画像から塗り絵を生成するプログラムに挑戦したいと思います。

目次

はじめに

前回はk平均法を使って画像の減色処理を実装しました。

今回は、画像のエッジ(色が変わるところ。輪郭のようなもの)を検出することにより、塗り絵風の画像に加工してみたいと思います。

使用する画像はこちらです。

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

この画像を塗り絵風にすると、こんな感じになりました。

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

エッジ検出にはソーベルフィルタというものを使います。

ソーベルフィルタとは

ソーベルフィルタは、画像の各点に対して、左右と上下それぞれどれくらい色が変化しているのか、変化率を計算し、ある値を超えた場合に、その点をエッジとするアルゴリズムです。

例えば、ある画像(100×100)があったとします。

画像のある位置(50, 50)の点に着目します。

点の周りにはもちろん他の点もあって、位置が(50, 50)の点を中心に縦横それぞれ3ピクセル分の画素値を抜き出したところ、以下のような値になっていたとします。

  • x
101100101
2010050
101540

ソーベルフィルタは、以下のような3×3の2つのフィルタで構成されています。

  • Fx
-101
-202
-101
  • Fy
-1-2-1
000
121

このとき、エッジかどうかを判定するのに用いる値は次のように計算されます。


\sqrt{(x * Fx)^2 + (x * Fy)^2}

まず、 x * Fxの部分は、xとFxを同じ場所にある要素同士かけ合わせて、すべての要素を足した値です。

上の例でいうと、


x * Fx =\\
-1 * 101 + 0 * 100 + 1 * 101\\
-2 * 20  + 0 * 100 + 2 * 50\\
-1 * 10  + 0 * 15  + 1 * 40\\
= 90

となります。おなじように x * Fyの値は、-336となります。

それぞれ2乗して、足してからルートをとります。すると、 \sqrt{90^2 + (-336)^2} \fallingdotseq 348となります。

したがって、閾値(いくつ以上ならエッジとするかの境界)をたとえば、100にすると、この点(50, 50)はエッジになります。

Fx, Fyの真ん中の行または列が2になっているのは、注目画素と隣り合っている画素の効果を強めるためです。

真ん中の行または列も1になっているものはプリューウィットフィルタと呼ばれます。

実装する

外枠

from PIL import Image, ImageDraw
import numpy as np
import math

# ソーベルフィルタによるエッジ検出
def edge_filter(img):
  return img

# 画像ファイルの読み込み
filename = "reduce_sin.jpg"
img = Image.open("./" + filename).convert("RGB")

line_img = edge_filter(img)
line_img.save("./line_" + filename)

とりあえず、メソッドの中身は空にしておいて、外枠だけを実装した段階です。

ひとまずこれで動くか試してみてください。line_元画像のファイル名 という名前のファイルが作られたら成功です。

ソーベルフィルタの実装

ソーベルフィルタは、次のedge_filterメソッド内に実装します。

edge_filterメソッドは、画像を引数にとり、エッジのみ描かれた画像を返すメソッドです。

# ソーベルフィルタによるエッジ検出
def edge_filter(img):
  # STEP1
  w, h = img.size
  line_img = Image.new('RGB', (w, h))
  converted_pixels = np.array([[img.getpixel((i, j)) for i in range(w)] for j in range(h)])

  # STEP2
  for x in range(w):
    for y in range(h):
      # STEP2-1
      if x == 0 or x == w - 1 or y == 0 or y == h - 1:
        continue
      # STEP2-2
      dfx = -1 * converted_pixels[y - 1][x - 1] + converted_pixels[y - 1][x + 1]\
            -2 * converted_pixels[y][x - 1]     + 2 * converted_pixels[y][x - 1]\
            -1 * converted_pixels[y + 1][x - 1] + converted_pixels[y + 1][x + 1]
      # STEP2-3
      dfy = -1 * converted_pixels[y - 1][x - 1] + converted_pixels[y + 1][x - 1]\
            -2 * converted_pixels[y - 1][x]     + 2 * converted_pixels[y + 1][x]\
            -1 * converted_pixels[y - 1][x + 1] + converted_pixels[y + 1][x + 1]
      # STEP2-4
      d = math.sqrt(max(dfx ** 2 + dfy ** 2))
      # STEP2-5
      if d > 50 :
        line_img.putpixel((x, y),(200, 200, 200))
      else:
        line_img.putpixel((x, y),(255, 255, 255))
  return line_img

解説

  • STEP1
    • 画像のサイズや画像を2次配列に変換するなどの初期化を行う
  • STEP2
    • 画像内の全ての画素について以下の2-1~2-5を行う
  • STEP2-1
    • もし、注目している画素が画像の端ならば処理を飛ばす
    • これは、画像の端の点ではフィルタがかけられないから
  • STEP2-2
    •  x * Fxを計算する
  • STEP2-3
    •  x * Fyを計算する
  • STEP2-4
    • 判定に用いる値( \sqrt{(x * Fx)^2 + (x * Fy)^2})を計算する
  • STEP2-5
    • もし閾値を超えていたら薄い黒を描画する
    • 閾値以下なら、白を描画しておく
    • 今は閾値として50を設定している。閾値を大きくするとエッジが少なくなり、小さくするとエッジが多くなる。

平滑化フィルタを事前に通しておく

エッジ検出は、小さなノイズに反応してしまい。ぶつぶつがたくさんある画像が出力されることがあります。そのため、平滑化フィルタをかけておき、ノイズの影響を小さくしておくとうまくいきます。

引数のfilter_sizeを大きくすることで効果を強くすることができます。ですが、大きくしすぎると、本来検出したいエッジも消えてしまうので、ほどほどが良いです。

# 平均化フィルタをかけた画像を返す
def mean_filter(img, filter_size):
  w, h = img.size
  image_pixcels = np.array([[img.getpixel((x,y)) for x in range(w)] for y in range(h)])
  filtered_img = Image.new('RGB', (w, h))
  for x in range(w):
    for y in range(h):
      x1 = max(0, x - filter_size)
      x2 = min(x + filter_size, w -1)
      y1 = max(0, y - filter_size)
      y2 = min(y + filter_size, h - 1)
      x_size = x2 - x1 + 1
      y_size = y2 - y1 + 1
      mean_r, mean_g, mean_b = image_pixcels[y1:y2 + 1, x1:x2 + 1].reshape(x_size * y_size, 3).mean(axis = 0)
      filtered_img.putpixel((x,y), (int(mean_r), int(mean_g), int(mean_b)))
  return filtered_img

mean_filterを先ほどのコードに追加し、次の一行を、edge_filter実行前に追加します。

filtered_img = mean_filter(img, 1)
filtered_img.save("./filtered_" + filename)

edge_filterの引数にはfiltered_imgを渡すようにしておきます。

実行する

実装したコードを保存します。例えばedge.pyとしておきます。

塗り絵にしたい画像はあらかじめ用意しておき、コード内のfilenameを適宜変更しておいてください

python edge.py

とすると実行できます。

処理が終わると、line_元画像ファイル名 というファイルが作成されています。

どうでしょう、塗り絵っぽくなったでしょうか?

アニメや漫画など、色の数が少ない絵はうまくいきます。

逆に森や海などの自然画は線が多すぎるように感じるかもしれません。

平滑化あるなし

平滑化フィルタをかけた場合と、そうでない場合の実行例を載せておきます。

  • 平滑化フィルタあり f:id:s-uotani-zetakansu:20170829001425j:plain

  • 平滑化フィルタなし f:id:s-uotani-zetakansu:20170829001433j:plain

平滑化フィルタありの方が、ややきれいになっているような気がします。

最後に

最後まで読んでいただきありがとうございました。

今回はエッジ検出のアルゴリズムを使って塗り絵風の画像を生成してみました。

ただのエッジ検出じゃないの?と思われたかもしれませんが、でも塗り絵っぽいでしょう?言ったもん勝ちです(笑)

また次回も楽しみにしておいてください。