アールテクニカ地下ガレージ

アールテクニカ株式会社の製品開発・研究開発・日々の活動です

画像の代表的な色を抽出する

アールテクニカ学生スタッフの松下です。
佐賀大学の理工学部で情報系を専攻しています。
大学でC++をやったり、趣味でPythonを書いたりしています。

今回は、画像の代表的な色(以下、「代表色」)をPythonのライブラリを使って色々なアプローチで抽出してみます。

代表色はAdobe Color CCからでも抽出することができます。
color.adobe.com
今回はこの仕組みを目指して再現してみます。

環境

  • Python 3.7.3
  • Pythonのライブラリ
    • numpy 1.16.4
    • Pillow 6.0.0
    • sympy 1.4

macOS 10.13 High Sierraにて動作確認をしていますが、OSに依存する箇所はないはずのなので他のOSでも行けると思います。
また、Pythonも3以降であれば動作するかと思われます。

平均

まずは単純に平均を取るだけのアルゴリズムを作ります。
画像の読み込み、RGBの取得、作成、保存などを Pillow を使ってやっています。

import numpy as np
import PIL.ImageDraw

source_file = 'XXX.jpg'
source = PIL.Image.open(source_file)

small_img = source.resize((100, 100))  # 時間短縮のために解像度を落とす
color_arr = np.array(small_img)
w_size, h_size, n_color = color_arr.shape
color_arr = color_arr.reshape(w_size * h_size, n_color)

color_mean = np.mean(color_arr, axis=0)
color_mean = color_mean.astype(int)
color_mean = tuple(color_mean)

im = PIL.Image.new('RGB', (100, 100), color_mean)
im.save('YYY.png')

結果は以下です。

f:id:matsushita_at:20190826202824j:plainf:id:matsushita_at:20190826202836p:plain
f:id:matsushita_at:20190826202827j:plainf:id:matsushita_at:20190826202840p:plain

f:id:matsushita_at:20190826202832j:plainf:id:matsushita_at:20190826202843p:plain
左:入力画像、右:出力画像

最頻値

今度は最頻値で試してみます。
最頻値を求めるのに scipy を用いました。

import numpy as np
import PIL.ImageDraw
import scipy.stats

source_file = 'XXX.jpg'
source = PIL.Image.open(source_file)

small_img = source.resize((100, 100))
color_arr = np.array(small_img)
w_size, h_size, n_color = color_arr.shape
color_arr = color_arr.reshape(w_size * h_size, n_color)

color_mode, _ = scipy.stats.mode(color_arr, axis=0)[0]
color_mode = tuple(color_mode)

im = PIL.Image.new('RGB', (100, 100), color_mode)
im.save('YYY.png')

R, G, Bそれぞれで最頻値をとって、それらを組み合わせて色を再現します。

結果は以下です。

f:id:matsushita_at:20190826202824j:plainf:id:matsushita_at:20190826203520p:plain
f:id:matsushita_at:20190826202827j:plainf:id:matsushita_at:20190826203524p:plain

f:id:matsushita_at:20190826202832j:plainf:id:matsushita_at:20190826203528p:plain
左:入力画像、右:出力画像

先ほどの最頻値はRGBそれぞれで最頻値をとって色を作ったので、結果の色が画像中に存在するとは限りません。
今度は画像中に存在するピクセルのうちで最頻値をとってみます。

import numpy as np
import PIL.ImageDraw
import scipy.stats

source_file = 'XXX.jpg'
source = PIL.Image.open(source_file)

small_img = source.resize((100, 100))
color_arr = np.array(small_img)
w_size, h_size, n_color = color_arr.shape
color_arr = color_arr.reshape(w_size * h_size, n_color)
color_code = ['{:02x}{:02x}{:02x}'.format(*elem) for elem in color_arr]
mode, _ = scipy.stats.mode(color_code)
r = int(mode[0][0:2], 16)
g = int(mode[0][2:4], 16)
b = int(mode[0][4:6], 16)
color_mode = (r, g, b)

im = PIL.Image.new('RGB', (100, 100), color_mode)
im.save('YYY.png')

全ピクセルの色を一旦「RRGGBB」の形式にし、「RR」「GG」「BB」を整数に変換しています。

結果は以下です。

f:id:matsushita_at:20190826202824j:plainf:id:matsushita_at:20190827131501p:plain
f:id:matsushita_at:20190826202827j:plainf:id:matsushita_at:20190827131504p:plain

f:id:matsushita_at:20190826202832j:plainf:id:matsushita_at:20190827131508p:plain
左:入力画像、右:出力画像

中央値

平均、最頻値ときたので中央値もやっておきます。

import numpy as np
import PIL.ImageDraw

source_file = 'XXX.jpg'
source = PIL.Image.open(source_file)

small_img = source.resize((100, 100))
color_arr = np.array(small_img)
w_size, h_size, n_color = color_arr.shape
color_arr = color_arr.reshape(w_size * h_size, n_color)
r = [elem[0] for elem in color_arr]
g = [elem[1] for elem in color_arr]
b = [elem[2] for elem in color_arr]
color_median = (int(np.median(r)), int(np.median(g)), int(np.median(b)))

im = PIL.Image.new('RGB', (100, 100), color_median)
im.save('YYY.png')

R, G, Bそれぞれで中央値をとっています。

結果は以下です。

f:id:matsushita_at:20190826202824j:plainf:id:matsushita_at:20190827163330p:plain
f:id:matsushita_at:20190826202827j:plainf:id:matsushita_at:20190827163333p:plain

f:id:matsushita_at:20190826202832j:plainf:id:matsushita_at:20190827163337p:plain
左:入力画像、右:出力画像

k平均法

次に試すのは k平均法 と呼ばれる、無数のデータをいくつかのグループ(クラスタ)にわけるアルゴリズムです。
簡単には
1. クラスタ中心 をランダムな値で初期化する。
2. 以下を繰り返す。
2-1. 各データを、一番近い中心のクラスタに属させる。
2-2. クラスタごとに、属しているデータの重心を求め、それをクラスタ中心とする。
2-3. クラスタ中心の変化量が一定の値よりも小さくなったら終了する。

という感じで求めます。

k平均法を用いれば、得られたクラスタ中心が代表色として使えそうです。
クラスタの個数は好きに変えることができるので、前述のColor CCのように複数個抽出することもできます。

k平均法は scipy にメソッドが用意されているので、それに任せます。

import numpy as np
import PIL.ImageDraw
import scipy.cluster

N_CLUSTER = 1

def kmeans_process(img, n_cluster):
    sm_img = img.resize((100, 100))
    color_arr = np.array(sm_img)
    w_size, h_size, n_color = color_arr.shape
    color_arr = color_arr.reshape(w_size * h_size, n_color)
    color_arr = color_arr.astype(np.float)

    codebook, distortion = scipy.cluster.vq.kmeans(color_arr, n_cluster)  # クラスタ中心
    code, _ = scipy.cluster.vq.vq(color_arr, codebook)  # 各データがどのクラスタに属しているか

    n_data = []  # 各クラスタのデータ数
    for n in range(n_cluster):
        n_data.append(len([x for x in code if x == n]))

    desc_order = np.argsort(n_data)[::-1]  # データ数が多い順に「第○クラスタ、第○クラスタ、、、、」

    return ['#{:02x}{:02x}{:02x}'.format(*(codebook[elem].astype(int))) for elem in desc_order]

source_file = 'XXX.jpg'
source = PIL.Image.open(source_file)
colors = kmeans_process(source, N_CLUSTER)

im_size = 100
im = PIL.Image.new('RGB', (im_size, im_size), (255, 255, 255, 255))
draw = PIL.ImageDraw.Draw(im)
single_width = im_size / N_CLUSTER

for i, color in enumerate(colors):
    # 色を描画
    p1 = (single_width * i, 0)
    p2 = (single_width * (i + 1), im_size)
    pos = [p1, p2]
    draw.rectangle(pos, fill=color)

im.save('YYY.png')

5行目の N_CLUSTER でクラスタ数を変えることができます。
クラスタ数が2つ以上のときは、属するデータが多い順にクラスタを左から描画するようにしました。
結果は以下です。

f:id:matsushita_at:20190826202824j:plainf:id:matsushita_at:20190826204002p:plainf:id:matsushita_at:20190826204032p:plain
f:id:matsushita_at:20190826202827j:plainf:id:matsushita_at:20190826204010p:plainf:id:matsushita_at:20190826204036p:plain

f:id:matsushita_at:20190826202832j:plainf:id:matsushita_at:20190826204015p:plainf:id:matsushita_at:20190826204039p:plain
左:入力画像、真ん中:出力画像、右:出力画像(3クラスタ)

まとめ

平均、最頻値、中央値、1クラスタのK平均法はどれも似たような感じでしたが、
それらに比べると3クラスタのk平均法は画像の特徴をしっかり捉えていて、いい感じでした。
クラスタ数をもっといじれば、より良いものになるかもしれません。

最適なクラスタ数を調べるには、 distortion がこれ以上あまり小さくならないというところで
クラスタ数を上げるのをやめる エルボー法 などがありますが、それについてはまた機会があったときに書くことにします。

スポンサーリンク