Pythonでカラー画像を圧縮する方法(少ない色数でオリジナルの色に近い画像を生成する方法)

今日は少ない色数でオリジナルの色に近い画像を生成する方法を説明します。

一般的なカラー画像は赤、緑、青の3色の混ぜ合わせ(加法混色)で表現され、色数は約1600万色になります。

なぜ1600万色かと言うと、1600万色 ≒ R(赤)×G(緑)×B(青)=256諧調×256諧調×256諧調 で表現されるからです。

なぜ256諧調かと言うと、一つの色例えば R が8bit で表現され、2^8(2の8乗)= 256 の諧調を持つからです。

コンピュータでは 256諧調を 0 ~ 255 段階で表現します。

色はR、G、Bで3つあるため、256^3(256の3乗)= 16,777,216 となります。

これが、約1600万色となる理由です。

ですが、ほとんどの画像では、多くの色は実際には使用されず、画像内の多くのピクセルが同じ色または似たような色を持っています。

このため、似たような色を同じ色として表現することができれば色数を減らす(減色)ことができます。

今回は、減色させるアルゴリズムとしてk平均法によるクラスタリング法を用います。

ソースコード

以下のソースコードを見てください。

# kmean_recolored.py #  プログラム名

import numpy as np
from matplotlib import pyplot as plt
from sklearn.cluster import MiniBatchKMeans


# RGB色空間におけるピクセルの分布を可視化する
# なお、計算効率化のためimageの全ピクセル(縦×横)の内、10,000ピクセル分を使用
# imageの全ピクセル数で実行すると計算負荷が大きく処理時間も長くなる
def plot_pixels(image, title, colors = None, N=10000):
    if colors is None:
        colors = image

    rng = np.random.RandomState(0)
    i = rng.permutation(image.shape[0])[:N]
    colors = colors[i]
    R, G, B = image[i].T

    fig, ax = plt.subplots(1, 2, figsize=(16, 6))
    ax[0].scatter(R, G, color=colors, marker='.')
    ax[0].set(xlabel='Red', ylabel='Green', xlim = (0, 1), ylim = (0, 1))

    ax[1].scatter(R, B, color=colors, marker='.')
    ax[1].set(xlabel='Red', ylabel='Blue', xlim=(0, 1), ylim = (0, 1))

    fig.suptitle(title, size=20)


# 画像の読み込み
image = plt.imread('./test_photo/hana/cat_1_426px.jpg')

# imageのデータ形状確認
print(image.shape)

# 画像サイズ取得
h, w = image.shape[:2]

# imageのデータを正規化して(255.0で割って)0.0~1.0の間にデータをおさめる
image_1 = image / 255.0

# image_1のデータ形状を変形(全ピクセル数, 色ch数)
image_1 = image_1.reshape(h*w, 3)

# image_1のデータ形状確認
print('image_1=',image_1.shape)


### k平均法によるカラー圧縮 ###
# パラメータ
color_n = 16  # 色数

# 計算
kmeans = MiniBatchKMeans(color_n)
kmeans.fit(image_1)
new_colors = kmeans.cluster_centers_[kmeans.predict(image_1)]

# new_colorsのデータ形状確認
print('new_colors=', new_colors.shape)

# 約1600万色(RGB=256×256×256色)の場合の色分布を表示
plot_pixels(image_1, title="Input color space: 16 million possible colors")
plt.show()

# 16色(new_colors)に減色した場合の色分布を表示
plot_pixels(image_1, colors=new_colors, title="Reduced color space: 16 colors")
plt.show()

# imageの形状を整形
img_recolored = new_colors.reshape(image.shape)

# img_recoloredの形状を確認
print('img_recolored=', img_recolored.shape)

### 減色画像の表示 ###
# 画像を1行2列で表示
fig, ax = plt.subplots(1, 2, figsize=(16, 6), subplot_kw=dict(xticks=[], yticks=[]))

# 画像の間隔を指定
fig.subplots_adjust(wspace=0.05)

# 1行1列目にimageの画像を表示
ax[0].imshow(image)
ax[0].set_title('Original Image', size=16)

# 1行2列目にimg_recoloredの画像を表示
ax[1].imshow(img_recolored)
ax[1].set_title('16-color Image', size=16)

# imageとimg_recoloredが2枚並んだ画像を所定のフォルダに保存
plt.savefig('./test_photo/hana/temp_1/' + '{}.jpg'.format('16_color'))

# image画像を1枚だけ保存
fig, ax = plt.subplots()
plt.imshow(image)
plt.axis('off')
plt.savefig('./test_photo/hana/temp_1/original_16.jpg')
plt.show()

# img_recolored画像を1枚だけ保存
fig, ax = plt.subplots()
plt.imshow(img_recolored)
plt.axis('off')
plt.savefig('./test_photo/hana/temp_1/recolored_16.jpg')
plt.show()

ポイント

画像の読み込みに plt.imread( ) を使用する

画像の読み込みに cv2.imread( ) を使う方法もありますが、cv2 を使って読み込んだ画像は色順番がBGRになります。

それに比べ、plt.imread( ) で読み込むと色順番はRGBであるため、色の順番をBGR → RGBに変更する必要がありません。

このため、plt.imread( ) を使用します。

画像データを整形する

画像の各色(RGB)は 0~255の整数となっています。

また、配列は(高さ、幅、RGB)の3次元配列となっています。

画像の全ピクセルに対して色の分布状態を確認するため、画像データを以下のように整形します。

項目整形前整形後
RGB = 0 ~ 255RGB / 255.0 = 0.0 ~ 1.0
データ形式(高さ、幅、RGB)(高さ×幅、RGB)= (全ピクセル、色ch数)

対応するソースコードは以下です。

# imageのデータを正規化して(255.0で割って)0.0~1.0の間にデータをおさめる
image_1 = image / 255.0

# image_1のデータ形状を変形(全ピクセル数, 色ch数)
image_1 = image_1.reshape(h*w, 3)

また、以下の部分で色数を指定しています。16色に減色させるという意味です。

color_n = 16

k平均法でクラスタリングして色を減色する

k平均法によるクラスタリングにより、元のピクセルの色が変更され、各ピクセルに最も近いクラスタ中心の色が割り当てられます。

クラスタリングの計算は以下のコードで実行されます。

# 計算
kmeans = MiniBatchKMeans(color_n)
kmeans.fit(image_1)
new_colors = kmeans.cluster_centers_[kmeans.predict(image_1)]

計算する際、非常に大きなデータを処理することになります。

このため、画像の全データではなくデータの一部(サブセット)で動作するミニバッチk平均法を使用します。

これにより、計算負荷が抑えられ、かなり高速に計算することができます。

RGB色空間におけるピクセル分布を可視化する

可視化するコードは以下です。

# 約1600万色(RGB=256×256×256色)の場合の色分布を表示
plot_pixels(image_1, title="Input color space: 16 million possible colors")
plt.show()

# 16色(new_colors)に減色した場合の色分布を表示
plot_pixels(image_1, colors=new_colors, title="Reduced color space: 16 colors")
plt.show()

plot_pixels( ) のカッコ内を説明します。

なお、plot_pixels( ) は def で関数化されています。

実行時は def plot_pixels(image, title, colors = None, N=10000): が実行されます。

plt_pixels( ) の第一引数「 image_1 」は、def plot_pixels( ) の第一引数「 image 」に代入されます。

また、plt_pixels( ) の第二引数 「title = “Input color space: 16 million possible colors" 」は、def plot_pixels( ) の第二引数「 title 」に代入されます。

次に、def plot_pixels( ): 内の以下のソースコードについて説明します。

fig, ax = plt.subplots(1, 2, figsize=(16, 6))

plt.subplots( ) の第一、第二引数の部分(数字の1と2)は、グラフ作成時に1行2列のグラフを作成する指示です。

また、第三引数の figsize = ( ) はグラフサイズを示しています。

横が16、縦が6の大きさになるよう指示しています。

次に、以下のソースコードについて説明します。

ax[0].scatter(R, G, color=colors, marker='.')

ax[0] は1行1列目にグラフを表示しなさいという指示です。

scatter( ) は散布図を作成する指示です。

カッコの中の第一、第二引数は、色の R と G のデータを示しています。

R のデータを横軸に、G のデータを縦軸に見てその交点に marker=’ . ' で指定した点をプロットするよう指示しています。

なお、第三引数は色の指定です。

次に、以下のソースコードについて説明します。

ax[1].scatter(R, B, color=colors, marker='.')

先ほどの繰り返しになりますが、今度は ax[1] となっているので、1行2列目にグラフを表示しなさいという指示です。

また、今度は scatter( ) のカッコの中の第一、第二引数は、色の R と B のデータを示しています。

後は先ほどと同じです。

なお、plt.show( ) を記載することで各グラフが画面に表示されます。

これを記載しておかないと画面にグラフは表示されません。

実行結果

RGB色空間におけるピクセルの分布状態

以下に、RGB色空間におけるピクセルの分布状態を示します。

上のグラフが1600万色の場合、下のグラフが16色の場合です。

明らかにグラデーションが違います。

1600万色のグラフでは色の変化が滑らかに推移しています。

一方、16色のグラフでは色の変化が階段状になっています。

減色数を変化させた場合

上記では1600万色と16色の比較を行いました。

今度は、2色、8色、32色、64色、128色に減色させた場合を見てみます。

2色、8色では色数が少なくグラデーションがかなり粗い感じです。

32色以降はグラデーションも大分滑らかになり、128色では1600万色とほぼ見分けがつきません。

記事のトップに示した画像は128色に減色した画像です。

オリジナルの1600万色と比べても違和感の少ない画像になっていると思います。

RGB色空間におけるピクセルの分布状態を見ても納得の結果だと思います。

減色画像の表示

次に減色画像を見てみます。

ソースコードの ### 減色画像の表示 ### 以降が実行されると、減色画像が表示されます。

左側が1600万色の画像、右側が16色に減色させた画像です。

画像をよく見ると、16色に減色させた画像はグラデーションが階段状になっていることが分かります。

ネコの顔だけ見ると違和感は少ないですが、首のあたりや背景の部分で違いが分かります。

次に、減色数を変化させた場合の画像を見てみます。

2色、8色に減色させた画像では、グラデーションが階段状になっており明らかに違和感があります。

一方、32色に減色させた画像以降は、かなりオリジナルに近い画像が表示されています。

特に、128色に減色させた画像では背景の一部でグラデーションが階段状になっていることが分かる程度で、ネコの顔はオリジナルと遜色ないレベルになっています。

まとめ

  • 画像の読み込みに plt.imread( ) を使用する
  • k平均法でクラスタリングして色を減色する
  • RGB色空間におけるピクセルの分布を可視化する
  • 可視化することで生成される画像の傾向がつかめる。
  • 減色数が32色以降はオリジナルの1600万色にかなり近い画像が表示できる。
  • 特に減色数が128色程度ならオリジナルと遜色ないレベルの画像が表示できる。

参考図書