2022/06/20(月) [n年前の日記]
#2 [python] Pythonで画像にディザリングをかけてみる
_昨日、
Python + Pillow (PIL) を使って、グレースケールの画像に対してディザリング処理(Ordered dithering)をしてみたわけだけど。RGB画像に対してはどのように処理をすればいいのか分からなかったので、そのあたりを試してみた。
環境は、Windows10 x64 21H2 + Python 3.9.13 64bit + Pillow 9.1.1。
実験に使った画像は、昨日と同様、猿(マンドリル)画像。
_mandrill.png
ちなみに、この画像の使用色数を調べてみたら、230427色だった。
環境は、Windows10 x64 21H2 + Python 3.9.13 64bit + Pillow 9.1.1。
実験に使った画像は、昨日と同様、猿(マンドリル)画像。
ちなみに、この画像の使用色数を調べてみたら、230427色だった。
◎ RGBチャンネル別に処理する。 :
RGB画像を、Rチャンネル、Gチャンネル、Bチャンネルに分解して、それぞれに対してディザリングをしてから最後に合成すればそれっぽくならないかなと思えてきたので試してみた。
_06ordered_dithering_8col.py
py 06ordered_dithering_8col.py で実行。output_8col.png というファイル名で、以下のRGB画像が得られる。
見た感じ、一応どうにかカラー画像(RGB画像)に対してもディザがかかっているように見えなくもないなと…。
ただ、RGB各チャンネルが、0 or 1 (0 or 255) の値しか持ってないから、全部で 2 x 2 x 2 = 8色しか使ってないわけで…。当然ながら、結構悲惨な見た目になってるというか。
もっとも、230427色使ってた画像を、たった8色しか使ってない画像に変換して、このぐらいそれらしさが残ってるなら、これはこれでイイ感じなのでは、という気もしてくる。
ちなみに、昨日試した、Pillow の .convert("1") を使った事例 ―― RGBチャンネルを分解して、それぞれ2値化して、合成した結果とかなり近い結果になった気もする。使用色数はどちらも8色なわけで…。
違いは、ディザの種類が Ordered dithering か、Floyd Steinberg dithering か、ぐらいではないのかなと…。たぶん。
_フロイド-スタインバーグ・ディザリング - Wikipedia
_06ordered_dithering_8col.py
from PIL import Image odtbl2x2 = [ [1, 3], [4, 2] ] odtbl3x3 = [ [3, 7, 4], [6, 1, 9], [2, 8, 5] ] # odtbl3x3 = [ # [7, 9, 5], # [2, 1, 4], # [6, 3, 8] # ] odtbl4x4 = [ [1, 9, 3, 11], [13, 5, 15, 7], [4, 12, 2, 10], [16, 8, 14, 6] ] odtbl8x8 = [ [1, 33, 9, 41, 3, 35, 11, 43], [49, 17, 57, 25, 51, 19, 59, 27], [13, 45, 5, 37, 15, 47, 7, 39], [61, 29, 53, 21, 63, 31, 55, 23], [4, 36, 12, 44, 2, 34, 10, 42], [52, 20, 60, 28, 50, 18, 58, 26], [16, 48, 8, 40, 14, 46, 6, 38], [64, 32, 56, 24, 62, 30, 54, 22] ] odtbls = { "2x2": [5, odtbl2x2], "3x3": [10, odtbl3x3], "4x4": [17, odtbl4x4], "8x8": [65, odtbl8x8] } def get_dither_table(odtype): d, tbl = odtbls[odtype] w = len(tbl[0]) h = len(tbl) odtbl = [[0 for i in range(w)] for j in range(h)] for y in range(h): for x in range(w): odtbl[y][x] = tbl[y][x] * 256 / d return w, h, odtbl def get_dither_image(odtype, im): width, height = im.size oim = Image.new("L", im.size) tw, th, od = get_dither_table(odtype) src_pixel = im.load() dst_pixel = oim.load() for y in range(height): dy = y % th for x in range(width): dx = x % tw if src_pixel[x, y] < od[dy][dx]: dst_pixel[x, y] = 0 else: dst_pixel[x, y] = 255 return oim def main(): im = Image.open("mandrill.png") r, g, b = im.split() odtype = "4x4" rim = get_dither_image(odtype, r).convert("L") gim = get_dither_image(odtype, g).convert("L") bim = get_dither_image(odtype, b).convert("L") oim = Image.merge("RGB", (rim, gim, bim)) oim.save("output_8col.png") if __name__ == '__main__': main()
py 06ordered_dithering_8col.py で実行。output_8col.png というファイル名で、以下のRGB画像が得られる。
見た感じ、一応どうにかカラー画像(RGB画像)に対してもディザがかかっているように見えなくもないなと…。
ただ、RGB各チャンネルが、0 or 1 (0 or 255) の値しか持ってないから、全部で 2 x 2 x 2 = 8色しか使ってないわけで…。当然ながら、結構悲惨な見た目になってるというか。
もっとも、230427色使ってた画像を、たった8色しか使ってない画像に変換して、このぐらいそれらしさが残ってるなら、これはこれでイイ感じなのでは、という気もしてくる。
ちなみに、昨日試した、Pillow の .convert("1") を使った事例 ―― RGBチャンネルを分解して、それぞれ2値化して、合成した結果とかなり近い結果になった気もする。使用色数はどちらも8色なわけで…。
違いは、ディザの種類が Ordered dithering か、Floyd Steinberg dithering か、ぐらいではないのかなと…。たぶん。
_フロイド-スタインバーグ・ディザリング - Wikipedia
◎ 諧調に段階を持たせてみる。 :
RGB各チャンネルに対して0/1しか使ってない状態にしてしまうから厳しい見た目になるのではないか、もっと段階を持たせたら改善しないかなと思えてきた。もちろん、段階を持たせた分、使用色数は増えるわけだけど。
つまり、以下のような感じで処理できないかな、と。
そんなわけで試してみた。
_07ordered_dithering_ncol.py
py 07ordered_dithering_ncol.py で実行。output_ncol_4x4_lvl4.png、といった感じのファイル名で生成画像が得られる。
まずは、ディザ配列の大きさは4x4、256諧調を分割しない状態で試してみた。
前述の、8色しか使ってないディザ画像と同じ結果が得られた。
では、256諧調を2分割して、それぞれにディザをかけてみるとどうなるか…。
かなり改善できた気がする。RGB各チャンネルが3段階を持ってる状態になってるから、使用できる色数は 3 x 3 x 3 = 27色のはず。8色から27色に増えると、見た目もこのぐらい違ってくるのだな…。
ただ、上記の画像は、本来27色まで使えるはずなのに、21色しか使ってなかった。おそらく、元画像がどんな色を使っているかで、変換後の画像の使用色数も変わってくるのではないかと。
この調子で、分割数を増やしてみる。
256諧調を3分割。最大64色中、41色を使用。
4分割。最大125色中、75色を使用。
5分割。最大216色中、112色を使用。
6分割。最大343色中、170色を使用。
7分割。最大512色中、234色を使用。
8分割。最大729色中、309色を使用。
9分割。最大1000色中、402色を使用。
ということで、使用色数8色でディザをかけると悲惨な見た目になるけれど、256諧調の分割数を増やして、色数も増やせば、その分見た目もそれなりに改善されていくことが確認できた。ただ、分割数が増えれば増えるほど、差を感じにくくなってくる気もする。
一応、元画像も再掲。230427色。
ところで…。ImageMagick の Ordered dithering も、おそらくはコレと似たような処理をしているのではないかと想像してみたりもして。あちらも、何かの値を増やしていくと見た目がどんどん改善されていくので…。
_Quantization - ImageMagick Examples
「Ordered Dither with Uniform Color Levels」の項で、ディザ配列サイズの後ろに何かを指定してる。
(※2022/06/21追記。ImageMagick 7.1.0-37 Q16-HDRI x64 で試してみたら、予想通り似た感じの変換結果が得られた。全く同じ結果にはならなかったけど、おそらくしきい値の取り方が微妙に違ってるから違う結果になっただけで、やってることは大体同じなのだろう、と…。追記終わり。)
さておき。今回試したこの減色処理(?)は、ImageMagick の Ordered dithering と同様の問題を抱えている。最終的に残る色が固定パレット相当になるあたりがよろしくない。もし、ちゃんとした減色処理をしたいなら、画像毎に使っている色を分析して、その画像特有のパレットを生成して、そのパレットを使いながらディザをかけないといけないはず。
しかし、任意のパレットを指定しながら、この種のディザをかける方法が分からないなと…。何せ、あの ImageMagick ですら、その実装はしていないらしいので…。 *1 もっとも、例えば GIMP や EDGE2 は任意のパレットを指定しながらディザをかけて減色ができてるので、仕組みさえ分かれば実装できるはずだよなと…。
それはそれとして。Pillow関係の解説記事を眺めてたら、そもそも Pillow は減色機能も持ってるようで。メディアンカットとかkmeans(k平均法)とかサポート済みっぽい。
_Pythonで画像の減色をする
そのあたりを使いつつ、この手のディザをかけたら、もっとそれっぽくなるのだろうか。どうなんだろう。
つまり、以下のような感じで処理できないかな、と。
そんなわけで試してみた。
_07ordered_dithering_ncol.py
from PIL import Image import sys odtbl2x2 = [ [1, 3], [4, 2] ] odtbl3x3 = [ [3, 7, 4], [6, 1, 9], [2, 8, 5] ] # odtbl3x3 = [ # [7, 9, 5], # [2, 1, 4], # [6, 3, 8] # ] odtbl4x4 = [ [1, 9, 3, 11], [13, 5, 15, 7], [4, 12, 2, 10], [16, 8, 14, 6] ] odtbl8x8 = [ [1, 33, 9, 41, 3, 35, 11, 43], [49, 17, 57, 25, 51, 19, 59, 27], [13, 45, 5, 37, 15, 47, 7, 39], [61, 29, 53, 21, 63, 31, 55, 23], [4, 36, 12, 44, 2, 34, 10, 42], [52, 20, 60, 28, 50, 18, 58, 26], [16, 48, 8, 40, 14, 46, 6, 38], [64, 32, 56, 24, 62, 30, 54, 22] ] odtbls = { "2x2": [5, odtbl2x2], "3x3": [10, odtbl3x3], "4x4": [17, odtbl4x4], "8x8": [65, odtbl8x8] } def get_dither_table(odtype, vrange, ofs): d, tbl = odtbls[odtype] if d >= vrange: print("Error: dither table range >= vrange") sys.exit() w = len(tbl[0]) h = len(tbl) odtbl = [[0 for i in range(w)] for j in range(h)] for y in range(h): for x in range(w): odtbl[y][x] = tbl[y][x] * vrange / d + ofs return w, h, odtbl def get_dither_image(odtype, im, level): width, height = im.size oim = Image.new("L", im.size) tw, th, _ = get_dither_table(odtype, 256, 0) ods = [] for i in range(level): v0 = (256 * i) // level v1 = (256 * (i + 1)) // level vrange = v1 - v0 _, _, od = get_dither_table(odtype, vrange, v0) ods.append([v0, v1, od]) src_pixel = im.load() dst_pixel = oim.load() for y in range(height): dy = y % th for x in range(width): dx = x % tw v = src_pixel[x, y] for v0, v1, od in ods: if v0 <= v and v <= v1: if v < od[dy][dx]: dst_pixel[x, y] = v0 else: dst_pixel[x, y] = v1 return oim def main(): im = Image.open("mandrill.png") r, g, b = im.split() # odtype = "2x2" odtype = "4x4" level = 4 rim = get_dither_image(odtype, r, level).convert("L") gim = get_dither_image(odtype, g, level).convert("L") bim = get_dither_image(odtype, b, level).convert("L") oim = Image.merge("RGB", (rim, gim, bim)) oim.save("output_ncol_%s_lvl%d.png" % (odtype, level)) if __name__ == '__main__': main()
py 07ordered_dithering_ncol.py で実行。output_ncol_4x4_lvl4.png、といった感じのファイル名で生成画像が得られる。
まずは、ディザ配列の大きさは4x4、256諧調を分割しない状態で試してみた。
前述の、8色しか使ってないディザ画像と同じ結果が得られた。
では、256諧調を2分割して、それぞれにディザをかけてみるとどうなるか…。
かなり改善できた気がする。RGB各チャンネルが3段階を持ってる状態になってるから、使用できる色数は 3 x 3 x 3 = 27色のはず。8色から27色に増えると、見た目もこのぐらい違ってくるのだな…。
ただ、上記の画像は、本来27色まで使えるはずなのに、21色しか使ってなかった。おそらく、元画像がどんな色を使っているかで、変換後の画像の使用色数も変わってくるのではないかと。
この調子で、分割数を増やしてみる。
256諧調を3分割。最大64色中、41色を使用。
4分割。最大125色中、75色を使用。
5分割。最大216色中、112色を使用。
6分割。最大343色中、170色を使用。
7分割。最大512色中、234色を使用。
8分割。最大729色中、309色を使用。
9分割。最大1000色中、402色を使用。
ということで、使用色数8色でディザをかけると悲惨な見た目になるけれど、256諧調の分割数を増やして、色数も増やせば、その分見た目もそれなりに改善されていくことが確認できた。ただ、分割数が増えれば増えるほど、差を感じにくくなってくる気もする。
一応、元画像も再掲。230427色。
ところで…。ImageMagick の Ordered dithering も、おそらくはコレと似たような処理をしているのではないかと想像してみたりもして。あちらも、何かの値を増やしていくと見た目がどんどん改善されていくので…。
_Quantization - ImageMagick Examples
「Ordered Dither with Uniform Color Levels」の項で、ディザ配列サイズの後ろに何かを指定してる。
magick gradient.png -ordered-dither o4x4,6 od_o4x4_6.gifこの値が、たぶんソレなのかなと。
(※2022/06/21追記。ImageMagick 7.1.0-37 Q16-HDRI x64 で試してみたら、予想通り似た感じの変換結果が得られた。全く同じ結果にはならなかったけど、おそらくしきい値の取り方が微妙に違ってるから違う結果になっただけで、やってることは大体同じなのだろう、と…。追記終わり。)
さておき。今回試したこの減色処理(?)は、ImageMagick の Ordered dithering と同様の問題を抱えている。最終的に残る色が固定パレット相当になるあたりがよろしくない。もし、ちゃんとした減色処理をしたいなら、画像毎に使っている色を分析して、その画像特有のパレットを生成して、そのパレットを使いながらディザをかけないといけないはず。
しかし、任意のパレットを指定しながら、この種のディザをかける方法が分からないなと…。何せ、あの ImageMagick ですら、その実装はしていないらしいので…。 *1 もっとも、例えば GIMP や EDGE2 は任意のパレットを指定しながらディザをかけて減色ができてるので、仕組みさえ分かれば実装できるはずだよなと…。
それはそれとして。Pillow関係の解説記事を眺めてたら、そもそも Pillow は減色機能も持ってるようで。メディアンカットとかkmeans(k平均法)とかサポート済みっぽい。
_Pythonで画像の減色をする
そのあたりを使いつつ、この手のディザをかけたら、もっとそれっぽくなるのだろうか。どうなんだろう。
◎ 妄想。 :
今回、256諧調を均等に分割して実験してみたわけだけど。画像によっては256諧調のうち、この範囲は頻繁に使うけどこの範囲はほとんど使ってない、といったものもあるかもしれないなと。そういった画像の場合は、分割位置に偏りを持たせたら、更に結果が改善されるのかもしれないなと妄想したりもして。まあ、そんなの絶対誰かしらが既に思いついて試しているだろうなー、とも思いますけど。
画像によっては、R,G,B成分の使用具合にばらつきがあるかもしれないな、とも。その場合、例えばR成分とG成分は細かく分割するけれど、B成分は粗く分割して使用色数を抑える、といったこともできたりするのかもしれない。まあ、これも絶対誰かしらが既に思いついて試してるやろ、とも思いますが。
RGBじゃなくて、YCbCr(輝度+色差)、HSV(色相、彩度、明度)でこの手の処理をしたらどうなるだろう、という疑問も湧いたりして。まあ、そのあたりも既に誰かが以下略。
画像によっては、R,G,B成分の使用具合にばらつきがあるかもしれないな、とも。その場合、例えばR成分とG成分は細かく分割するけれど、B成分は粗く分割して使用色数を抑える、といったこともできたりするのかもしれない。まあ、これも絶対誰かしらが既に思いついて試してるやろ、とも思いますが。
RGBじゃなくて、YCbCr(輝度+色差)、HSV(色相、彩度、明度)でこの手の処理をしたらどうなるだろう、という疑問も湧いたりして。まあ、そのあたりも既に誰かが以下略。
◎ 余談。レナさん画像を使いたいです。 :
昨日は猿4連発だったけど、今日なんか猿10連発ですよ。気が狂いそう。嗚呼、レナサン画像を使いたい…。レナお婆ちゃんも少しはこの苦しみを理解してくれないものか…。
*1: ImageMagick も、任意のパレットを指定しつつ Floyd Steinberg っぽいディザをかける機能なら実装済み。ただ、Ordered dithering を指定すると固定パレットになってしまう。意外と実装が難しい処理なのだろうか…?
[ ツッコむ ]
以上です。