🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

1回答

7777閲覧

Pythonで、いくつかの指定した色に似た色になるように、画像のピクセルを置き換えたい

dotter

総合スコア1

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

0クリップ

投稿2021/02/17 05:29

編集2021/02/17 13:45

前提・実現したいこと

Pythonにおいて、PILで画像処理を行なっています。
画像全体をピクセルごとに、予め用意したカラーパレットの色に変換したいと考えています。
ソースコードにある通り、(r,g,b)の順に並べたpaletteというタプルのリストを作り、画像内で1ピクセルごとにどの色を示すタプルが最も近い色なのかを判定しようとしています。
以下のソースコードではbase.jpgというファイルを処理してoutput.jpgを作ろうとしています。

私はpython初心者のため、ソースコードに汚い部分がると思います。
よろしくお願い致します。

該当のソースコード

Python

1from PIL import Image 2import numpy as np 3 4 5im=Image.open("base.jpg") #元画像base.jpg 6new_im=Image.new('RGB',(im.size[0],im.size[1]), (255,255,255)) 7#元画像と同じ大きさ、背景白の新たな画像 8 9 10def rgb_to_xyz(rgb): #rgb(255まで)→xyzの割合(0<=return<=1)を出す 11 a=[0.4124,0.3575,0.1805] 12 b=[0.2127,0.7152,0.0722] 13 c=[0.0193,0.1192,0.9504] 14 k=np.array([a,b,c]) #変換用倍率の行列 15 16 t=k.dot(np.array([x/255 for x in rgb])) 17 return (t[0]/0.9504,t[1]/1.0001,t[2]/1.0889) 18 #↑白(255,255,255)で(0.9504,1.0001,1.0889)なので割合として返す 19 20def F(t): #L*a*b*用の関数 21 if t>216/24389: 22 return 116*pow(t,1/3)-16 23 else: 24 return 24389/27*t 25 26def Lab(x,y,z): #xyz方式をL*a*b*に変換 27 L=F(y) 28 a=125/29*(F(x)-F(y)) 29 b=50/29*(F(y)-F(z)) 30 return (L,a,b) 31 32def dis(a,b): #ユークリッド距離(2乗での比較) 33 return sum([pow(Lab(*rgb_to_xyz(a))[x]-Lab(*rgb_to_xyz(b))[x],2) for x in range(2)]) 34 35 36palette=[(60, 60, 60), (120, 120, 120), (130, 130, 130), (90, 90, 90), (255, 255, 255), (130, 135, 145), (70, 175, 170), (190, 100, 130), (140, 60, 170), (100, 50, 40), (130, 130, 200), (40, 60, 40), (60, 100, 120), (80, 120, 170), (120, 40, 40), (90, 0, 0), (200, 0, 0), (170, 100, 40), (200, 185, 130), (120, 85, 60), (100, 70, 40), (80, 60, 40), (100, 140, 45), (115, 150, 70), (0, 170, 45), (100, 160, 20), (180, 180, 40), (200, 190, 60), (80, 100, 40), (0, 100, 0), (50, 70, 30), (40, 65, 40)] 37#↑用意した色のテンプレート(rgb) 38 39 40for x in range(im.size[0]): 41 for z in range(im.size[1]): 42 rgb=im.getpixel((x,z))[0:3] 43 m=[dis(rgb,p) for p in palette] 44 #↑現在指定しているピクセルの色(rgb)とパレットそれぞれを比較したリスト作成 45 new_im.putpixel((x,z),palette[m.index(min(m))]) #作成したリストの最小値に値するタプルで色付け 46 47 48new_im.save("output.jpg",quarity=90) 49

2つ目のソースコード

Python

1from PIL import Image 2import numpy as np 3 4 5im=Image.open("base.jpg") #元画像base.jpg 6new_im=Image.new('RGB',(im.size[0],im.size[1]), (255,255,255)) 7#元画像と同じ大きさ、背景白の新たな画像 8 9 10def rgb_to_xyz(rgb): #rgb(255まで)→xyzの割合(0<=return<=1)を出す 11 a=[0.4124,0.3575,0.1805] 12 b=[0.2127,0.7152,0.0722] 13 c=[0.0193,0.1192,0.9504] 14 k=np.array([a,b,c]) #変換用倍率の行列 15 16 t=k.dot(np.array([x/255 for x in rgb])) 17 return (t[0]/0.9504,t[1]/1.0001,t[2]/1.0889) 18 #↑白(255,255,255)で(0.9504,1.0001,1.0889)なので割合として返す 19 20def F(t): #L*a*b*用の関数 21 if t>216/24389: 22 return 116*pow(t,1/3)-16 23 else: 24 return 24389/27*t 25 26def Lab(x,y,z): #xyz方式をL*a*b*に変換 27 L=F(y) 28 a=125/29*(F(x)-F(y)) 29 b=50/29*(F(y)-F(z)) 30 return (L,a,b) 31 32def cos(a,b): 33 return sum([change(*a)[x]*change(*b)[x] for x in range(2)]) 34 35def C94(L1,a1,b1,L2,a2,b2): 36 k1=0.045 37 k2=0.015 38 kL=1 39 C1=(a1**2+b1**2)**(0.5) 40 C2=(a2**2+b2**2)**(0.5) 41 dC=C1-C2 42 dH=((a1-a2)**2+(b1-b2)**2-dC**2)**(0.5) 43 Sc=1+k1*C1 44 Sh=1+k2*C2 45 return (((L1-L2)/kL)**2+(dC/Sc)**2+(dH/Sh)**2)**(0.5) 46 47 48def dis(a,b): 49 return sum([C94(*Lab(*rgb_to_xyz(a)),*Lab(*rgb_to_xyz(b))) for x in range(2)]) 50 51 52palette=[(60, 60, 60), (120, 120, 120), (130, 130, 130), (90, 90, 90), (255, 255, 255), (130, 135, 145), (70, 175, 170), (190, 100, 130), (140, 60, 170), (100, 50, 40), (130, 130, 200), (40, 60, 40), (60, 100, 120), (80, 120, 170), (120, 40, 40), (90, 0, 0), (200, 0, 0), (170, 100, 40), (200, 185, 130), (120, 85, 60), (100, 70, 40), (80, 60, 40), (100, 140, 45), (115, 150, 70), (0, 170, 45), (100, 160, 20), (180, 180, 40), (200, 190, 60), (80, 100, 40), (0, 100, 0), (50, 70, 30), (40, 65, 40)] 53#↑用意した色のテンプレート(rgb) 54 55 56 57f![イメージ説明](9792dc25b0893bd07159f748839d1633.png)n range(im.size[0]): 58 for z in range(im.size[1]): 59 rgb=im.getpixel((x,z))[0:3] 60 m=[dis(rgb,p) for p in palette] 61 #↑現在指定しているピクセルの色(rgb)とパレットそれぞれを比較したリスト作成 62 new_im.putpixel((x,z),palette[m.index(min(m))]) #作成したリストの最小値に値するタプルで色付け 63 64 65new_im.save("output.jpg",quarity=90) 66 67 68

試したこと

近い色を判定するために、まずrgbのタプル同士でのユークリッド距離として近い色を検出させました。
次に、rgb配色のままだとあまり同系色を検出できないことを理解して、rgbをhsv形式に変換して同様にユークリッド距離で判定しました。
ユークリッド距離での計算が思うようにいかなかったため、コサイン類似度(空間ベクトル内での2ベクトルの内積の最小値)によって判定しました。
あまりいい結果が得られないため、次にrgbをxyz方式にして同様にユークリッド距離、コサイン類似度で計算しました。
最後に、現在のソースコードのようにrgb→xyz→Lab*方式に変換してユークリッド距離での判定をしました。
これらのことを行なったのですが似たような色へと変換することが困難でした。

《追記分》

ユークリッド距離での判定がイマイチだったので、2つ目のソースコードでは以下のURLの「CIE94」を試しています。
https://ja.m.wikipedia.org/wiki/色差

ユークリッド距離はそもそもの色が若干違う点、CIE94は大まかには合っているものの黒い斑点が多く入ってしまうこと、が問題点です。

左(base.jpg)→右(output.jpg)です。それぞれ2つ試しています。
イメージ説明

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

tiitoi

2021/02/17 11:41 編集

base.jpg を質問欄に貼っていただけますか。現状どうなるのか、どのような結果を期待しているのかがわからないので、コードだけ見てなにかアドバイスをするのは困難です。
dotter

2021/02/17 13:43

コメントありがとうございます。 対応させて頂きました。 よろしくお願い致します。
tiitoi

2021/02/17 14:40 編集

減色処理 (画像をN色で表すこと) ではなく、パレットから色を選ぶことは必須なのでしょうか?パレットの色の中身を確認しましたが、画像によっては元の色に近い色がパレットにないことも考えられ、パレットの色を使って元画像と同じ見た目の画像を生成するというのがそもそも無理だと思います。単にN色に減色したいということであれば、綺麗にやる方法が存在します。
guest

回答1

0

ベストアンサー

色差の計算は skimage にある LAB Delta E の CIE76, CIE94, CIEDE2000 で試したところ、以下のような結果になりました。

Module: color — skimage v0.19.0.dev0 docs

python

1from matplotlib import pyplot as plt 2from PIL import Image 3from skimage import color, io 4 5# パレット一覧 (R, G, B) 6palette_rgb = np.array( 7 [ 8 (60, 60, 60), 9 (120, 120, 120), 10 (130, 130, 130), 11 (90, 90, 90), 12 (255, 255, 255), 13 (130, 135, 145), 14 (70, 175, 170), 15 (190, 100, 130), 16 (140, 60, 170), 17 (100, 50, 40), 18 (130, 130, 200), 19 (40, 60, 40), 20 (60, 100, 120), 21 (80, 120, 170), 22 (120, 40, 40), 23 (90, 0, 0), 24 (200, 0, 0), 25 (170, 100, 40), 26 (200, 185, 130), 27 (120, 85, 60), 28 (100, 70, 40), 29 (80, 60, 40), 30 (100, 140, 45), 31 (115, 150, 70), 32 (0, 170, 45), 33 (100, 160, 20), 34 (180, 180, 40), 35 (200, 190, 60), 36 (80, 100, 40), 37 (0, 100, 0), 38 (50, 70, 30), 39 (40, 65, 40), 40 ], 41 dtype=np.uint8, 42) 43 44 45def show_pallets(palette): 46 fig = plt.figure(figsize=(5, 10)) 47 for i, color in enumerate(palette, 1): 48 color_img = np.full((1, 10, 3), color, dtype=np.uint8) 49 50 ax = fig.add_subplot(len(palette), 1, i) 51 ax.imshow(color_img, aspect="auto") 52 ax.set_axis_off() 53 ax.text(-1, 0, i, va="center", ha="right", fontsize=10) 54 55 plt.show() 56 57# パレットの色を確認 58show_pallets(palette) 59 60 61# 画像を読みこむ。 62img_rgb = io.imread("sample.jpg") 63 64# Lab 色空間に変換する。 65img_lab = color.rgb2lab(img_rgb) 66palette_lab = color.rgb2lab(palette_rgb) 67 68# 色差を計算する。 69diff = color.deltaE_ciede2000(np.expand_dims(img_lab, axis=2), palette_lab) 70 71# 一番近い色のインデックスを求める。 72indices = diff.argmin(axis=-1) 73 74# 一番近い色で出力画像を生成する。 75dst_rgb = palette_rgb[indices] 76 77# 比較するために元画像と結果画像を結合 78merged = np.concatenate((img_rgb, dst_rgb)) 79 80# 保存する。 81io.imsave("result.jpg", merged)

deltaE_ciede2000 の結果が以下の2枚

イメージ説明

イメージ説明

CIE76, CIE94, CIEDE2000 の比較結果

イメージ説明

元画像に使われている色に近い色がパレットになければどうしようもないので、これ以上綺麗にするのは無理でしょう。
見た目を損なわず減色処理したいだけであれば、色を固定せず、K-means など使って画像ごとに色を動的に選ぶようにしたほうがよいかと思います。

投稿2021/02/17 15:11

編集2021/02/17 15:15
tiitoi

総合スコア21956

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

dotter

2021/02/18 01:03

ご意見、ソースコード等大変助かりました。 ありがとうございました! モジュール関連で少し質問が残っているのですがそちらは別にしたいと思います。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問