※当サイトではアフィリエイト広告を利用しています

Python プログラミング 画像処理

【Python・OpenCV】ポスタライズ効果の実装

2024-06-26

はじめに

ポスタライズ効果とは、画像の色数を減らして特定の色階調だけを残す効果です。
この技法によって、画像の色が限られた数の階調に分かれ、アート作品のような仕上がりになります。
この効果を実現する方法は複数ありますが、本記事では以下の実装方法を紹介します。

  1. LUTを使用したポスタライズ処理
  2. K-meansクラスタリングを利用したポスタライズ処理

利用シーン

ポスタライズ効果は、主に芸術的な表現の場面で利用されますが、一般的にも以下のような場面で活用されています。

  • 画像圧縮
    ポスタライズ処理によって画像の色数を減らすことで、画像ファイルサイズを小さくすることができます。画像の色が限られた種類になるため、データ量が削減されます。限られた帯域で画像を送信する必要がある場合などに、ポスタライズによる画像圧縮が役立ちます。
  • 可視化と認識のしやすさ向上
    ポスタライズ処理によりノイズや細かい模様が減り、大まかな形状がくっきりと浮かび上がります。このため、画像の特徴を人間が認識しやすくなったり、コンピュータビジョンでの物体検出などの前処理として使われることがあります。
  • プライバシー保護
    個人の顔写真などからポスタライズ処理をすることで、識別が難しくなり、プライバシーを保護できます。監視カメラの映像などでも同様の効果が期待できます。
  • 工業製品の検査
    製品の表面の傷や欠陥を検出する際に、ポスタライズ処理によってコントラストを付けると、欠陥箇所が目立ちやすくなります。
    例えば、自動車のボディなどの金属製品の傷や傷を検出する際に、利用されることがあります。金属製品の表面はつやがあり、微細な傷が見つけにくい場合がありますが、ポスタライズ処理を施すことで、傷の部分とその周辺との色のコントラストが強調され、自動検出がしやすくなります。また、半導体ウェハーの欠陥検査などにも用いられるようです。

このように、ポスタライズ効果は芸術的用途だけでなく、幅広い分野での画像処理に役立つ可能性があります。状況に合わせてポスタライズの程度を調整することが重要です。

LUTを使用したポスタライズ処理

LUT(Look-Up Table)は、入力ピクセル値をあらかじめ定義されたテーブルを使って変換する手法です。
LUTを使用することで、ピクセル値の変換を高速に実行できます。LUTの特徴は以下の通りです。

手法:各ピクセル値をLUTを用いて変換します。LUTは、各入力値に対する出力値を定義したテーブルです。

柔軟性:LUTを自由に定義することで、さまざまな変換を実現できます。例えば、色補正、ガンマ補正、特殊効果など。

計算の効率:変換はテーブル参照による単純な操作なので、非常に高速です。

OpenCVでは、LUTを画像に適用するcv2.LUT関数が提供されています。

cv2.LUT関数

cv2.LUT関数は画像のピクセル値をLook-Up Table(LUT)を使って変換するために使用します。

cv2.LUT(入力動画, lut[, dst])

引数

項目説明
入力画像(必須)・入力画像は、1チャンネルまたは3チャンネルの画像。通常、グレースケール画像またはRGBカラー画像を入力とします。
numpy.ndarrayオブジェクト
lut(必須)・256エントリを持つ1次元配列(1チャンネル画像の場合)または3次元配列(3チャンネル画像の場合)で、データ型はuint8です。
・各ピクセル値に対する出力値が格納されています。例えば、lut[0]には入力値0に対する出力値が、lut[255]には入力値255に対する出力値が格納されます。
・カラー画像の場合、lutは形状が (256, 1, 3) となります。各チャンネルごとに256エントリの配列が必要です。
numpy.ndarrayオブジェクト
dst(オプション)結果がこの変数に格納されます。指定しない場合は、入力画像と同じサイズ・型で新しい配列が作成されて結果が格納され、戻り値として取得できます。

戻り値

LUTが適用された画像データをnumpy.ndarrayオブジェクトで返します。

サンプルコード:等間隔LUTポスタライズ

単純に等間隔に階調を区切って減色する方法のサンプルコードとなります。
例えば、8ビット画像を4レベルにポスタライズする場合、LUTは以下のようになります:

  • 入力値 0-63 → 出力値 31
  • 入力値 64-127 → 出力値 95
  • 入力値 128-191 → 出力値 159
  • 入力値 192-255 → 出力値 223

このように、入力範囲が均等に分割され、各範囲の中央値が出力値として使用されます。
次のサンプルで示す非線形マッピングとは対照的で、より単純で直接的なアプローチです。

このサンプルでは10階調に減色します。

処理の手順は次の通りです。

  1. 画像の読み込みとぼかし処理:
    cv2.imread() で画像を読み込みます。
    次に cv2.blur()cv2.bilateralFilter() でぼかし処理を行います。これにより画像内の色の種類が減り、後のクラスタリング処理が効率的になります。
  2. ポスタライズ処理の実行:
    posterization()関数を定義し、この関数内でポスタライズ処理を実行します。
  3. 入力画像のチャンネルの並びを変換:
    結果の表示にmatplotlibを使用するため、入力画像のチャンネルをBGR→RGBに変換します。
  4. 結果の表示:
    処理前の入力画像と、処理後の画像を並べて表示します。

これらの手順は、他の実装方法でも同様です。

import cv2
import numpy as np
import matplotlib.pyplot as plt

def posterization(image, levels):
    # LUTを作成    
    x = np.arange(0, 256)  # [0, 1, 2, ..., 255]
    divider = np.linspace(0, 255, levels + 1)[1]
    quantiz = np.linspace(0, 255, levels, dtype=np.uint8)
    color_levels = np.clip((x / divider).astype(np.uint8), 0, levels - 1)
    y = quantiz[color_levels]

    # 変換
    posterized = cv2.LUT(image, y)
    return posterized, y

def main():
    # 画像の読み込み
    image = cv2.imread("image.jpg")

    # 色数を減らすため、ぼかしを適用
    blurred = cv2.bilateralFilter(cv2.blur(image, (3, 3)), 9, 75, 75)

    # 階調数を設定
    levels = 10

    # ポスタライズ
    posterized, table = posterization(blurred, levels)

    # BGRのチャンネル並びをRGBの並びに変更(matplotlibで結果を表示するため)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) 
    posterized = cv2.cvtColor(posterized, cv2.COLOR_BGR2RGB) 

    # 結果の可視化
    plt.rcParams["figure.figsize"] = [9,6]          # 表示領域のアスペクト比を設定
    title = "Posterization with LUT: codevace.com"
    plt.figure(title)                               # ウィンドウタイトルを設定
    plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.95) # 余白を設定

    plt.subplot(221)                                # 2行2列の1番目の領域にプロットを設定
    plt.imshow(image)                               # 修復後の画像を表示
    plt.title("Original Image")                     # 画像タイトル設定
    plt.axis("off")                                 # 軸目盛、軸ラベルを消す

    plt.subplot(223)                                # 2行2列の3番目の領域にプロットを設定
    plt.imshow(posterized)                          # 修復後の画像を表示
    plt.title("Posterized Image")                   # 画像タイトル設定
    plt.axis("off")                                 # 軸目盛、軸ラベルを消す

    plt.subplot(224)                                # 2行2列の4番目の領域にプロットを設定
    h, w = image.shape[:2]                          # 画像のサイズを取得
    plt.gca().set_aspect(h/w)                       # 画像と同じグラフのサイズに設定
    plt.xlim(-10,265)                               # x軸の表示範囲を-10〜265に設定
    plt.ylim(-10,265)                               # y軸の表示範囲を-10〜265に設定
    plt.plot(table)                                 # 修復後の画像を表示
    plt.title("LUT")                                # 画像タイトル設定

    plt.show()                                      # ウィンドウを表示する

if __name__=='__main__':
	main()

25行目で階調数を10に設定しています。

実行結果は下の様になり、左上に入力画像、左下にポスタライズ画像、右下にLUTをグラフ表示したものを表示しています。
LUTは横軸が入力画素値、縦軸が出力画素値を示しています。

サンプルコード:非線形LUTポスタライズ

LUTにシグモイド関数を利用したポスタライズ処理の例を示します。
人間の視覚特性に合わせた非線形マッピングを行うことがあります。これにより、より自然に見えるポスタライズ効果を得ることができます。

シグモイド関数 f(x) = 1 / (1 + e^(-ax)) 使用しています。ここで、a はステープネス(急峻さ)を制御するパラメータです。
下のサンプルコードでは、7行目のsteepnessという変数になります。
steepnessを大きくするとコントラスト(明暗)が強くなり、小さくすると弱くなる効果も得られます。

import cv2
import numpy as np
import matplotlib.pyplot as plt

def  posterization(image, clusters):
    # シグモイド関数を用いた非線形マッピング LUTを作成
    steepness = 5 # シグモイド関数の急峻さを制御するパラメータ、大きいほど急峻。
    x = np.linspace(0, 1, 256)
    y = 1 / (1 + np.exp(-steepness * (x - 0.5)))
    y = np.round(y * (clusters - 1)) / (clusters - 1)
    y = (y * 255).astype(np.uint8)

    # 変換
    posterized = cv2.LUT(image, y)
    return posterized, y

def main():
    # 画像の読み込み
    image = cv2.imread("image.jpg")

    # 色数を減らすため、ぼかしを適用
    blurred = cv2.bilateralFilter(cv2.blur(image, (3, 3)), 9, 75, 75)

    # 階調数を設定
    levels = 10

    # ポスタライズ
    posterized, table = posterization(blurred, levels)

    # BGRのチャンネル並びをRGBの並びに変更(matplotlibで結果を表示するため)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) 
    posterized = cv2.cvtColor(posterized, cv2.COLOR_BGR2RGB) 

    # 結果の可視化
    plt.rcParams["figure.figsize"] = [9,6]          # 表示領域のアスペクト比を設定
    title = "Posterization with Non-linear LUT: codevace.com"
    plt.figure(title)                               # ウィンドウタイトルを設定
    plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.95) # 余白を設定

    plt.subplot(221)                                # 2行2列の1番目の領域にプロットを設定
    plt.imshow(image)                               # 修復後の画像を表示
    plt.title("Original Image")                     # 画像タイトル設定
    plt.axis("off")                                 # 軸目盛、軸ラベルを消す

    plt.subplot(223)                                # 2行2列の3番目の領域にプロットを設定
    plt.imshow(posterized)                          # 修復後の画像を表示
    plt.title("Non-linear Posterized Image")        # 画像タイトル設定
    plt.axis("off")                                 # 軸目盛、軸ラベルを消す

    plt.subplot(224)                                # 2行2列の4番目の領域にプロットを設定
    h, w = image.shape[:2]                          # 画像のサイズを取得
    plt.gca().set_aspect(h/w)                       # 画像と同じグラフのサイズに設定
    plt.xlim(-10,265)                               # x軸の表示範囲を-10〜265に設定
    plt.ylim(-10,265)                               # y軸の表示範囲を-10〜265に設定
    plt.plot(table)                                 # 修復後の画像を表示
    plt.title("Non-linear LUT")                     # 画像タイトル設定

    plt.show()                                      # ウィンドウを表示する

if __name__=='__main__':
	main()

等間隔LUTポスタライズと同様に25行目で階調数を10に設定しています。

LUTのグラフでは、不等間隔に階調を区切っていることが確認できます。
x軸(入力画素値)の小さい部分と大きい部分の間隔がより広く、中心(x=128)付近の間隔が狭くなっています。
つまり、輝度の高い部分はより高く。低い部分はより低い値に、変換されることになります。

steepness = 5と設定されていますが、若干コントラストが強くなっています。

コントラスト改善の他方法を下の記事でも紹介しています。

K-meansクラスタリングを利用したポスタライズ処理

K-meansクラスタリングを利用したポスタライズの原理と特徴について以下に示します。

基本原理
K-meansクラスタリングは、データ点を K 個のクラスタに分類するアルゴリズムです。ポスタライズ処理においては、各ピクセルの色を「データ点」として扱い、これらを指定された数の代表色(クラスタ中心)に分類します。

アルゴリズムの流れ
1. 初期化:K個のクラスタ中心をランダムに選択します。
2. 割り当て:各ピクセルを最も近いクラスタ中心に割り当てます。
3. 更新:各クラスタの新しい中心を計算します(クラスタに属するピクセルの平均色)。
4. 収束するまで 2 と 3 を繰り返します。

色空間の扱い
通常、RGB色空間で処理を行います。各ピクセルは3次元空間内の点として表現されます。

距離の定義
クラスタ中心との「近さ」は通常ユークリッド距離で測定されますが、他の距離尺度も使用可能です。

クラスタ数の選択
クラスタ数 K は、最終的なポスタライズ画像の色数を決定します。
K が小さいほど、より強いポスタライズ効果が得られます。

計算コスト
K-meansは反復的なアルゴリズムであるため、大きな画像や高い K 値(階調数)では計算コストが高くなります

適応的な色選択
K-means法の大きな利点は、画像の実際の色分布に基づいて代表色を選択することです。
これにより、画像固有の最適な色パレットが生成されます。

エッジの保存
K-meansは自然にエッジを保存する傾向があります。
これは、異なる色領域の境界がクラスタの境界と一致しやすいためです。

OpenCVでは、K-meansクラスタリングを実行するcv2.kmeans関数が提供されています。

cv2.kmeans関数

cv2.kmeans関数の引数と戻り値について下にまとめました。

retval, bestLabels, centers = cv2.kmeans(data, K, bestLabels, criteria, attempts, flags[, centers])

引数

項目説明
data(必須)・入力データ。各行が1つのデータポイントを表します。 ポスタライズの場合、各行は1ピクセルのRGB値 (例: [B, G, R]) となります。
numpy.float32型の2次元配列
K(必須)・クラスタの数。ポスタライズでは、階調数。
・int型
bestLabels(必須)・各データポイントの初期クラスタラベル。
・通常はNoneを指定し、ランダムに初期化させます。
criteria(必須)終了条件。(type, max_iter, epsilon) のタプルで指定します。
・type: 終了条件の種類
(cv2.TERM_CRITERIA_EPS, cv2.TERM_CRITERIA_MAX_ITER, または両方の組み合わせ)
・max_iter: 最大反復回数
・epsilon: 要求される精度
attempts(必須)・アルゴリズムを実行する回数。
・int型
flags(必須)・初期中心の選択方法を指定します。KmeansFlagsから選択。
・cv2.KMEANS_RANDOM_CENTERS:ランダムに選択
・cv2.KMEANS_PP_CENTERS:k-means++アルゴリズムを使用
・cv2.KMEANS_USE_INITIAL_LABELS:ユーザーが提供した初期クラスタラベルを使用してK-meansアルゴリズムを開始することを指示します。適切な初期クラスタラベルを提供することで、アルゴリズムの収束が速くなる可能性があります。
centers(オプション)・初期クラスタ中心。指定しない場合は自動的に選択されます。
numpy.ndarrayオブジェクト

戻り値

項目説明
retval・コンパクトネス測度。クラスタ内の点と中心との距離の二乗和
numpy.float32
bestLabels・各データポイントに割り当てられたクラスタのラベル
・0からK-1までの整数値(int型)
centers・最終的なクラスタ中心の座標
・K行×特徴数列の2D配列

サンプルコード

K-meansクラスタリングを用いたポスタライズは、画像の内容に適応的で、視覚的に優れた結果を生成できる手法です。

下に示すサンプルコードで実施に確認してみましょう。

import cv2
import numpy as np
import matplotlib.pyplot as plt

def posterization(image, clusters):
    # OpenCVのK-meansを適用してクラスタ値とラベルを取得
    data = image.reshape((-1, 3)).astype('float32')
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
    _, labels, centers = cv2.kmeans(data, clusters, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)

    # クラスタ値から画像を再構築
    centers = np.uint8(centers)
    posterized = centers[labels.flatten()]
    posterized = posterized.reshape(image.shape)
    return posterized

def main():
    # 画像の読み込み
    image = cv2.imread("image.jpg")

    # 色数を減らすため、ぼかしを適用
    blurred = cv2.bilateralFilter(cv2.blur(image, (3, 3)), 9, 75, 75)

    # 階調数を設定
    clusters = 10

    # ポスタライズ
    posterized = posterization(blurred, clusters)

    # BGRのチャンネル並びをRGBの並びに変更(matplotlibで結果を表示するため)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) 
    posterized = cv2.cvtColor(posterized, cv2.COLOR_BGR2RGB) 

    # 結果の可視化
    plt.rcParams["figure.figsize"] = [9,3]          # 表示領域のアスペクト比を設定
    title = "Posterization with K-means Clustering: codevace.com"
    plt.figure(title)                               # ウィンドウタイトルを設定
    plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.95) # 余白を設定

    plt.subplot(121)                                # 1行2列の1番目の領域にプロットを設定
    plt.imshow(image)                               # 修復後の画像を表示
    plt.title("Original Image")                     # 画像タイトル設定
    plt.axis("off")                                 # 軸目盛、軸ラベルを消す

    plt.subplot(122)                                # 1行2列の2番目の領域にプロットを設定
    plt.imshow(posterized)                          # 修復後の画像を表示
    plt.title("Posterized Image")                   # 画像タイトル設定
    plt.axis("off")                                 # 軸目盛、軸ラベルを消す

    plt.show()                                      # ウィンドウを表示する

if __name__=='__main__':
    main()

25行目で階調数を10に設定しています。

実行結果は下の様になり、左に入力画像、右にポスタライズ画像表示しています。
減色後の色にとても自然な色が選択されている、イラスト的な画像を生成することが出来ました。

cv2.kmeans関数のflag引数に他の値を設定したり、階調数を変更することで異なる結果を得ることが出来ます。
試して見てください。

おわりに

本記事ではLUTとK-meansクラスタリングを使ったポスタライズ処理について紹介しました。
階調数を変えることで、ポスタライズ効果の強弱を調整できます。これらの手法はコードがシンプルで実装が簡単なため、ポスタライズ効果を試したい際におすすめです。ぜひ色々な画像に対して試してみてください。

ご質問や取り上げて欲しい内容などがありましたら、コメントをお願いします。
最後までご覧いただきありがとうございました。

参考リンク

-Python, プログラミング, 画像処理
-