【Python】Pillowを使ってピクセル操作!画像フィルタをかけてみる

Python
マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
クマ〜〜〜〜〜〜
マイケル
マイケル
今日はPythonの Pillow というライブラリを使って
いろんな画像フィルタをかけてみるよ!
エレキベア
エレキベア
画像フィルタって何クマ??
マイケル
マイケル
たとえばこのエレキベアを…
Eleki dere
マイケル
マイケル
グレーにしたり
Elekibear dere gray
マイケル
マイケル
モザイクをかけたり
Elekibear dere mosaic
マイケル
マイケル
いろいろな効果をかけることなんだ!
エレキベア
エレキベア
(人の画像で遊ぶなクマ・・・)
なるほどクマ
ペイントソフトとかでかけれるやつ クマね
マイケル
マイケル
Photoshop等ペイントソフトでかけるフィルタも、
内部で変換する処理を行なっている からね!
マイケル
マイケル
今回はその画像フィルタの中でも、簡単でスタンダードな

・ネガポジ反転
・グレースケール
・エッジ検出
・モザイク

の処理を実装していくよ!
エレキベア
エレキベア
それは楽しみクマ〜〜〜〜
スポンサーリンク

参考書籍

マイケル
マイケル
今回、画像フィルタの実装をするにあたって
参考にした参考書はこちらになります!

グラフィックスプログラミング入門

マイケル
マイケル
この本ではJavaScriptでの実装ですが、
数学理論等、グラフィックス描画の基礎から書かれているので
読んでるだけでも勉強になります!
エレキベア
エレキベア
表紙もかわいいクマね
読んでみるクマ
マイケル
マイケル
画像フィルタの話はおまけで載っているレベルだったけど、
目から鱗の内容でした!

ピクセルとは

マイケル
マイケル
画像フィルタの実装に入る前に、ピクセルについて理解しておこう!
エレキベア
エレキベア
ピクセルって何クマ??
マイケル
マイケル
説明しよう!
ピクセルとは
  • コンピュータで画像を扱う際の、色の最小単位 のことである!
エレキベア
エレキベア
色の最小単位クマ?
マイケル
マイケル
その通り!
例えば下のイルカの画像では、青や赤といった色をピクセル単位で表示しているんだ!
Screenshot 2021 02 17 22 23 40
↑イルカの画像(20px * 20px)
エレキベア
エレキベア
(イルカ・・・・???)
マイケル
マイケル
よくみる画像サイズの「px」は、言い換えれば縦横のピクセル数のことなんだね
エレキベア
エレキベア
なるほどクマ
画像は全てピクセル単位で色を表示しているクマね
マイケル
マイケル
今回行う画像フィルタは、ピクセル単位で色を取得して変換 という流れで処理を行うよ!
ピクセルの概念は基本になるので覚えておきましょう!
エレキベア
エレキベア
楽しみクマ〜〜〜〜

画像フィルタの実装

マイケル
マイケル
それじゃ早速実装してみよう!
今回画像フィルタを行う対象は、ラーメン豚山のラーメン画像です!
00 ramen base
↑ラーメン豚山はとてもおいしい
エレキベア
エレキベア
(なんでこれを選んだクマ・・・)

全体の処理

マイケル
マイケル
フィルタ4種類を全て実装したソースは以下になります!
image_filter()メソッドにフィルタの種類を渡して変換する 処理にしています!
from PIL import Image
import math

# 画像フィルタ種類
INVERT_FILTER = "INVERT_FILTER"
GRAYSCALE_FILTER = "GRAYSCALE_FILTER"
LAPLACIAN_FILTER = "LAPLACIAN_FILTER"
MOSAIC_FILTER = "MOSAIC_FILTER"


# 画像フィルタ処理
def image_filter(image, filter_type):
    # 画像のRGBA,サイズを取得
    rgba = image.convert('RGBA')
    size = rgba.size
    # 返却用画像生成
    filter_image = Image.new('RGBA', size)

    ###################
    # 画像フィルタ処理
    # (指定タイプで分岐)
    ###################
    # *** ネガポジ反転 ***
    if filter_type == INVERT_FILTER:
        for x in range(size[0]):
            for y in range(size[1]):
                # RGBAの取得
                r, g, b, a = rgba.getpixel((x, y))
                # 255から引いて反転
                r = 255 - r
                g = 255 - g
                b = 255 - b
                filter_image.putpixel((x, y), (r, g, b, a))
    # *** グレースケール ***
    elif filter_type == GRAYSCALE_FILTER:
        for x in range(size[0]):
            for y in range(size[1]):
                # RGBAの取得
                r, g, b, a = rgba.getpixel((x, y))
                # RGBの平均値を設定
                luminance = int((r + g + b) / 3)
                r = luminance
                g = luminance
                b = luminance
                filter_image.putpixel((x, y), (r, g, b, a))
    # *** ラプラシアンフィルタ(エッジ検出) ***
    elif filter_type == LAPLACIAN_FILTER:
        for x in range(size[0]):
            for y in range(size[1]):
                # RGBAの取得
                r, g, b, a = rgba.getpixel((x, y))
                # 上下左右のRGBA取得
                t_r, t_g, t_b, t_a = rgba.getpixel((max(x - 1, 0), y))
                b_r, b_g, b_b, b_a = rgba.getpixel((min(x + 1, size[0] - 1), y))
                l_r, l_g, l_b, l_a = rgba.getpixel((x, max(y - 1, 0)))
                r_r, r_g, r_b, r_a = rgba.getpixel((x, min(y + 1, size[1] - 1)))
                # 上下左右の色を加算し、中心の色*4 を除算する
                # (差分がない場合は0になる)
                r = t_r + b_r + l_r + r_r - (r * 4)
                g = t_g + b_g + l_g + r_g - (g * 4)
                b = t_b + b_b + l_b + r_b - (b * 4)
                filter_image.putpixel((x, y), (r, g, b, a))
    # *** モザイク ***
    elif filter_type == MOSAIC_FILTER:
        # ブロックサイズを定義
        block_size = 8
        for x in range(size[0]):
            for y in range(size[1]):
                # blockSizeで切り捨て、インデックスを求める
                set_x = math.floor(x / block_size) * block_size
                set_y = math.floor(y / block_size) * block_size
                # 求めたインデックスのRGBAを設定
                r, g, b, a = rgba.getpixel((set_x, set_y))
                filter_image.putpixel((x, y), (r, g, b, a))
    # *** その他 ***
    else:
        # そのまま返却
        filter_image = image
    # フィルタ結果を返却
    return filter_image


# 画像読込
read_image = Image.open("【画像パス】")
# 画像フィルタ処理
# ※第2引数にフィルタのタイプを指定
disp_image = image_filter(read_image, MOSAIC_FILTER)
# フィルタ結果表示
disp_image.show()
エレキベア
エレキベア
「画像フィルタ処理」の部分で変換しているクマね
マイケル
マイケル
その通り!
一つ一つ内容を見ていこう!

ネガポジ反転

マイケル
マイケル
まずはネガポジ反転
名前の通り色を反転させるフィルタで、かけた結果は以下のようになるよ!
01 ramen invert
↑ネガポジ反転後のラーメン
エレキベア
エレキベア
(まずそうクマ・・・。)
マイケル
マイケル
処理の流れとしては、

1. 画像からRGBAとサイズ(縦横のピクセル数)を取得
2. 「255からRGB値を引いて設定し直す」処理をピクセル分繰り返す

となっています!

・・・略・・・

# 画像フィルタ処理
def image_filter(image, filter_type):
    # 画像のRGBA,サイズを取得
    rgba = image.convert('RGBA')
    size = rgba.size
    # 返却用画像生成
    filter_image = Image.new('RGBA', size)

    ###################
    # 画像フィルタ処理
    # (指定タイプで分岐)
    ###################
    # *** ネガポジ反転 ***
    if filter_type == INVERT_FILTER:
        for x in range(size[0]):
            for y in range(size[1]):
                # RGBAの取得
                r, g, b, a = rgba.getpixel((x, y))
                # 255から引いて反転
                r = 255 - r
                g = 255 - g
                b = 255 - b
                filter_image.putpixel((x, y), (r, g, b, a))


・・・略・・・

マイケル
マイケル
RGB値は0〜255で表されるから255から引くことで反転できる、ということだね!
Screenshot 2021 02 17 22 04 05
Screenshot 2021 02 17 22 04 27

↑RGB値は0〜255で表すことができる

エレキベア
エレキベア
ピクセル一つ一つに対して反転処理を行うクマね

グレースケール

マイケル
マイケル
そして2つ目はグレースケール
これも名前の通り、モノトーン色のみにするフィルタだね!
02 ramen gray
↑グレースケール後のラーメン
エレキベア
エレキベア
(なんか悲しいクマ・・・。)
マイケル
マイケル
ラーメンが泣いている・・・!
マイケル
マイケル
どういう時に白黒になるかというと、
実はRGB値が全て同じ値の時に白黒になるんだ!
Screenshot 2021 02 17 22 07 11
Screenshot 2021 02 17 22 07 31

↑RGB値が同一の場合に白黒になる

エレキベア
エレキベア
それは知らなかったクマ
マイケル
マイケル
これを利用して、RGBの合計値を3で割ることで平均値を設定してあげます!

・・・略・・・

# 画像フィルタ処理
def image_filter(image, filter_type):
    # 画像のRGBA,サイズを取得
    rgba = image.convert('RGBA')
    size = rgba.size
    # 返却用画像生成
    filter_image = Image.new('RGBA', size)

    ###################
    # 画像フィルタ処理
    # (指定タイプで分岐)
    ###################

・・・略・・・

    # *** グレースケール ***
    elif filter_type == GRAYSCALE_FILTER:
        for x in range(size[0]):
            for y in range(size[1]):
                # RGBAの取得
                r, g, b, a = rgba.getpixel((x, y))
                # RGBの平均値を設定
                luminance = int((r + g + b) / 3)
                r = luminance
                g = luminance
                b = luminance
                filter_image.putpixel((x, y), (r, g, b, a))

・・・略・・・

マイケル
マイケル
これだけでグレースケール化することができます!
エレキベア
エレキベア
こんな簡単にできるクマね
感動クマ・・・。

エッジ検出

マイケル
マイケル
そして3つ目はエッジ検出
これはラプラシアンフィルターを使用して色の差分が大きい箇所のみ目立たせることで処理できるよ!
03 ramen edge
↑エッジ検出後のラーメン
エレキベア
エレキベア
これは難しそうクマ・・・。
どうやるクマ??
マイケル
マイケル
これは対象ピクセルの色と周りのピクセルの色に差分があるかをチェックすることで処理するよ!
Screenshot 2021 02 17 22 16 13
マイケル
マイケル
上の図のように、周りのピクセル(上下左右)の色の合計から中央ピクセルの色を4倍した値を引いた差分を設定するんだ!
その結果、差分が大きいほど白色に近い値になるから強調されるということだね!

・・・略・・・

# 画像フィルタ処理
def image_filter(image, filter_type):
    # 画像のRGBA,サイズを取得
    rgba = image.convert('RGBA')
    size = rgba.size
    # 返却用画像生成
    filter_image = Image.new('RGBA', size)

    ###################
    # 画像フィルタ処理
    # (指定タイプで分岐)
    ###################

・・・略・・・

    # *** ラプラシアンフィルタ(エッジ検出) ***
    elif filter_type == LAPLACIAN_FILTER:
        for x in range(size[0]):
            for y in range(size[1]):
                # RGBAの取得
                r, g, b, a = rgba.getpixel((x, y))
                # 上下左右のRGBA取得
                t_r, t_g, t_b, t_a = rgba.getpixel((max(x - 1, 0), y))
                b_r, b_g, b_b, b_a = rgba.getpixel((min(x + 1, size[0] - 1), y))
                l_r, l_g, l_b, l_a = rgba.getpixel((x, max(y - 1, 0)))
                r_r, r_g, r_b, r_a = rgba.getpixel((x, min(y + 1, size[1] - 1)))
                # 上下左右の色を加算し、中心の色*4 を除算する
                # (差分がない場合は0になる)
                r = t_r + b_r + l_r + r_r - (r * 4)
                g = t_g + b_g + l_g + r_g - (g * 4)
                b = t_b + b_b + l_b + r_b - (b * 4)
                filter_image.putpixel((x, y), (r, g, b, a))


・・・略・・・

エレキベア
エレキベア
なるほどクマ・・・!
こんな方法でエッジ検出していたクマね
マイケル
マイケル
ちなみに上下左右の4ピクセルのチェックを行う方法は4近傍法といって、
斜めも含めた8ピクセルをチェックする8近傍法という方法もあるよ!
エレキベア
エレキベア
処理の負荷と精度の考慮が必要ということクマね

モザイク

マイケル
マイケル
そして最後はモザイク
これは分かりやすくモザイクです!
04 ramen mosaic
↑モザイク後のラーメン
エレキベア
エレキベア
モザイククマ
マイケル
マイケル
これは、モザイクにするブロック数をあらかじめ決めておいて、
そのブロック範囲を全て同じ色で塗りつぶす
ことで実現できるよ!
Screenshot 2021 02 17 22 16 19

・・・略・・・

# 画像フィルタ処理
def image_filter(image, filter_type):
    # 画像のRGBA,サイズを取得
    rgba = image.convert('RGBA')
    size = rgba.size
    # 返却用画像生成
    filter_image = Image.new('RGBA', size)

    ###################
    # 画像フィルタ処理
    # (指定タイプで分岐)
    ###################

・・・略・・・

    # *** モザイク ***
    elif filter_type == MOSAIC_FILTER:
        # ブロックサイズを定義
        block_size = 8
        for x in range(size[0]):
            for y in range(size[1]):
                # blockSizeで切り捨て、インデックスを求める
                set_x = math.floor(x / block_size) * block_size
                set_y = math.floor(y / block_size) * block_size
                # 求めたインデックスのRGBAを設定
                r, g, b, a = rgba.getpixel((set_x, set_y))
                filter_image.putpixel((x, y), (r, g, b, a))

・・・略・・・

エレキベア
エレキベア
これは割と分かりやすいクマね
マイケル
マイケル
ブロック数で割った結果をインデックスとして扱う、というシンプルな処理だね!
モザイクはよく使いそうだから覚えておいて損はなさそうだ!

おわりに

マイケル
マイケル
というわけで4種類の画像フィルタの実装について紹介しました!
どうだったかな?
エレキベア
エレキベア
いつも使うフィルタの実装内容が分かって楽しかったクマ〜〜〜〜
マイケル
マイケル
画像フィルタはまだまだたくさんある!
興味をもったらぜひ他のフィルタの実装方法も調べてみましょう!
マイケル
マイケル
それでは今日はこの辺で!
アデュー!!
エレキベア
エレキベア
クマ〜〜〜〜〜〜〜〜〜

【Python】Pillowを使ってピクセル操作!画像フィルタをかけてみる 〜完〜

コメント