2022/07/07(木) [n年前の日記]
#1 [python] Pillowのgetpixel()は本当に遅いのか気になったので確認してみた
Python で画像を扱える Pillow (PIL)モジュールについて、1ドット単位で値を読んだり書いたりできる .getpixel()/.putpixel() というメソッドがあるのだけど。巷の各種記事では処理速度が遅いと書いてあって、本当にそうなのかなと気になってきた。
そんなわけで、ベンチマークを取ってみた。環境は、Windows10 x64 21H2 + Python 3.9.13 64bit + Pillow 9.1.1。CPU は AMD Ryzen 5 5600X (6コア12スレッド、ベースクロック3.7GHz)。
4288x2848ドットの画像に対してひたすらドットを読むだけの処理をしてみた。ちなみに利用した画像は以下。
_十代の少女 可愛い 肖像画 - Pixabayの無料写真
ソースは以下。
_05getpixel_bench.py
動作には、Pillow と benchmarker が必要。
結果は以下のような感じになった。
.getdata() を使えばたしかに速くなるけれど、一次元配列に対してアクセスするような書き方になるので、可読性はほんのちょっと、若干かすかに、ビミョーに悪くなるような気もする。
対して、.load() を使ったアクセスなら、.getpixel() と同様に x, y を指定してアクセスできるし、しかも .getdata() を使った場合とそれほど処理速度も変わらないわけで…。
個人的には、.getdata() より、可読性と処理速度の両方をそこそこ得られる .load() を使ったほうがいいのではないかと思えてきた。
余談。手元で実験に使ってるスクリプトが、とにかく遅くて…。.load() を使って画像の各ドットにアクセスしていたのだけど、.getdata() を使ったらもっと速く処理できないかと少し期待しながらベンチマークを取ったわけで。ある意味、残念な結果になってしまった。今回、そこらへんを変えてみても結果は変わらないようだなと…。
そんなわけで、ベンチマークを取ってみた。環境は、Windows10 x64 21H2 + Python 3.9.13 64bit + Pillow 9.1.1。CPU は AMD Ryzen 5 5600X (6コア12スレッド、ベースクロック3.7GHz)。
4288x2848ドットの画像に対してひたすらドットを読むだけの処理をしてみた。ちなみに利用した画像は以下。
_十代の少女 可愛い 肖像画 - Pixabayの無料写真
ソースは以下。
_05getpixel_bench.py
from PIL import Image from benchmarker import Benchmarker infile = "teen-girl-4467541_4288x2848.jpg" def main(): im = Image.open(infile) width, height = im.size print("%d x %d" % (width, height)) with Benchmarker(100) as bench: @bench(".getdata()") def _(bm): im = Image.open(infile) dt = im.getdata() for y in range(height): idx = width * y for x in range(width): _ = dt[idx + x] @bench(".load()") def _(bm): im = Image.open(infile) src = im.load() for y in range(height): for x in range(width): _ = src[x, y] @bench(".getpixel()") def _(bm): im = Image.open(infile) for y in range(height): for x in range(width): _ = im.getpixel((x, y)) if __name__ == '__main__': main()
動作には、Pillow と benchmarker が必要。
pip install Pillow -U pip install Benchmarker -U
結果は以下のような感じになった。
> py 05getpixel_bench.py 4288 x 2848 ## benchmarker: release 4.0.1 (for python) ## python version: 3.9.13 ## python compiler: MSC v.1929 64 bit (AMD64) ## python platform: Windows-10-10.0.19044-SP0 ## python executable: C:\Python\Python39-64\python.exe ## cpu model: AMD64 Family 25 Model 33 Stepping 2, AuthenticAMD ## parameters: loop=100, cycle=1, extra=0 ## real (total = user + sys) .getdata() 0.7155 0.7188 0.6875 0.0312 .load() 0.7961 0.7969 0.7969 0.0000 .getpixel() 6.2659 6.2656 6.2656 0.0000 ## Ranking real .getdata() 0.7155 (100.0) ******************** .load() 0.7961 ( 89.9) ****************** .getpixel() 6.2659 ( 11.4) ** ## Matrix real [01] [02] [03] [01] .getdata() 0.7155 100.0 111.3 875.8 [02] .load() 0.7961 89.9 100.0 787.1 [03] .getpixel() 6.2659 11.4 12.7 100.0
- .getpixel() は、たしかに遅かった。他のアクセス方法より数倍遅い。
- .getdata() は、たしかに一番速かった。
- ただ、Image.load() をしてからアクセスする方法も、.getdata() 並みの速さでアクセスできるように見える。
.getdata() を使えばたしかに速くなるけれど、一次元配列に対してアクセスするような書き方になるので、可読性はほんのちょっと、若干かすかに、ビミョーに悪くなるような気もする。
対して、.load() を使ったアクセスなら、.getpixel() と同様に x, y を指定してアクセスできるし、しかも .getdata() を使った場合とそれほど処理速度も変わらないわけで…。
個人的には、.getdata() より、可読性と処理速度の両方をそこそこ得られる .load() を使ったほうがいいのではないかと思えてきた。
余談。手元で実験に使ってるスクリプトが、とにかく遅くて…。.load() を使って画像の各ドットにアクセスしていたのだけど、.getdata() を使ったらもっと速く処理できないかと少し期待しながらベンチマークを取ったわけで。ある意味、残念な結果になってしまった。今回、そこらへんを変えてみても結果は変わらないようだなと…。
[ ツッコむ ]
#2 [python][cg_tools] ディザリング処理をするプログラムをPythonに移植中その7
任意のパレットを指定してディザリングをかけるサンプルプログラム群を ―― C++ で書かれてるソレを Python で書き直しているところ。
_Arbitrary-palette positional dithering algorithm
ある程度動くようになったので、一応アップロード。元記事内の、アルゴリズム1、2、3、及び Photoshopっぽいアルゴリズムを、全部1つのファイルにまとめてみた。
_Yliluoma's ordered dithering algorithm 1, 2, 3. Python version.
動作には、Pillow と tqdm が必要。
動作確認環境は、Windows10 x64 21H2 + Python 3.9.13 64bit + Pillow 9.1.1 + tqdm 4.64.0。
_Arbitrary-palette positional dithering algorithm
ある程度動くようになったので、一応アップロード。元記事内の、アルゴリズム1、2、3、及び Photoshopっぽいアルゴリズムを、全部1つのファイルにまとめてみた。
_Yliluoma's ordered dithering algorithm 1, 2, 3. Python version.
動作には、Pillow と tqdm が必要。
pip install Pillow -U pip install tqdm -U
動作確認環境は、Windows10 x64 21H2 + Python 3.9.13 64bit + Pillow 9.1.1 + tqdm 4.64.0。
◎ 使い方。 :
使い方は以下のような感じ。--mode N でアルゴリズムを選べる。
一応、ヘルプ表示も載せておく。
mode の違いは以下のような感じ。元記事を読まないと分からないとは思うけど…。
py yliluoma_ordered_dither.py -i scene.png -o scenedither_m0_8x8.png --mode 0 py yliluoma_ordered_dither.py -i scene.png -o scenedither_m1_8x8.png --mode 1 py yliluoma_ordered_dither.py -i scene.png -o scenedither_m2_8x8.png --mode 2 py yliluoma_ordered_dither.py -i scene.png -o scenedither_m3_8x8.png --mode 3 py yliluoma_ordered_dither.py -i scene.png -o scenedither_m4_8x8.png --mode 4 py yliluoma_ordered_dither.py -i scene.png -o scenedither_m5_8x8.png --mode 5 py yliluoma_ordered_dither.py -i scene.png -o scenedither_m6_8x8.png --mode 6 py yliluoma_ordered_dither.py -i scene.png -o scenedither_m7_8x8.png --mode 7 py yliluoma_ordered_dither.py -i scene.png -o scenedither_m8_8x8.png --mode 8
一応、ヘルプ表示も載せておく。
> py yliluoma_ordered_dither.py --help usage: yliluoma_ordered_dither.py [-h] -i INPUT -o OUTPUT [-p PALETTE] [-d DITHER] [-m MODE] [-c] Yliluoma ordered dithering 1, 2, 3, 4 optional arguments: -h, --help show this help message and exit -i INPUT, --input INPUT Input png filename -o OUTPUT, --output OUTPUT Output png filename -p PALETTE, --palette PALETTE Palette file (.png or .gpl) -d DITHER, --dither DITHER Dither type 2,4,8 (2x2,4x4,8x8). default: 8 -m MODE, --mode MODE job kind 0 - 8. default: 3 -c, --ciede2000 Enable CIEDE2000 (mode 6 only
mode の違いは以下のような感じ。元記事を読まないと分からないとは思うけど…。
- mode 0: アルゴリズム1。2色の組み合わせを意識して差し替えるべき色を求める。かなり遅い。
- mode 1: アルゴリズム1。mode 0 の改良版。突飛な色の組み合わせは除外する。かなり遅い。
- mode 2: アルゴリズム1。mode 1 の改良版。ガンマ値を正しく反映させる。かなり遅い。
- mode 3: アルゴリズム1。mode 2 の処理高速化版。ループを回さずに数式で近似値を求める。少し早い。
- mode 4: アルゴリズム1。3色の組み合わせを意識して処理する。かなり遅い。
- mode 5: アルゴリズム2。N色の組み合わせを意識しないで色を求める。かなり遅い。
- mode 6: アルゴリズム2。mode 5 の改良版。ガンマ値を正しく反映させる。かなり遅い。
- mode 7: アルゴリズム3。mode 6 の改良版。かなり遅い。
- mode 8: Adobe Photoshop っぽいアルゴリズム。結構早い。
◎ 問題点。 :
このスクリプト、とにかく処理が遅い…。CPU: AMD Ryzen 5 5600X を使って、289x176ドットの画像に対して、16色でディザリングをかけるのに、最悪8分ぐらいかかる…。もちろん、使うアルゴリズムにもよるのだけど…。
比較的変換処理が速いのは、--mode 3 と --mode 8。
それ以外のアルゴリズムは、まあ、遅い。とんでもなく遅い。
比較的変換処理が速いのは、--mode 3 と --mode 8。
- mode 3 は、ループを回して総当たりで相応しい色を探さずに、数式で大まかに合いそうな色を探すアルゴリズム。ただ、速くはなるけれど、生成画像の品質は落ちる。
- mode 8 は、Adobe Photoshop で使われていたらしいアルゴリズム。品質と処理速度のバランスが取れている。ただ、生成画像は全体的にボケ気味な印象を受けた。
それ以外のアルゴリズムは、まあ、遅い。とんでもなく遅い。
◎ 改善策。 :
改善策は、いくつか思いつく。
元記事のソースは 8x8 のディザで処理しているけれど、見た感じ、4x4 のディザでも十分かなと思えた。8x8のディザだと、一番深いところで64回ループが回るけど、4x4なら16回のループで済むので、単純計算で約4倍の速さになりそう。
並列処理を導入するのも手かもしれない。Python で並列処理をさせる方法が分からなかったので今回は試してないけど、今時のCPUなら4コアだの6コアだの持ってるだろうから、1/4、1/6 の処理時間で済むのではなかろうか。実際、元々のC++版は、OpenMP(?) を有効にしてビルドして、並列処理する実行バイナリにしたら爆速になった。
画像の全てのドットに対して、毎回ループを回して、適切なパレットカラーを求めているあたりがアレだろうなという気もする。どれかしらの要素を事前に計算してキャッシュしておいて、そのキャッシュを使い回すようにすれば、もうちょっと改善されるかもしれない。例えばベタ部分が多い画像の場合、画像内に出てくる色の数は限定されるはずで、以前計算したことがある色が出現したら、その時の計算結果を再度使う、という作りにするだけでも効果はありそう。
元記事にも書いてあるけど、kd-tree 等を導入すれば改善される可能性もあるのだろうなと…。画像内のとある色について、差し替えに相応しい色はどれか、と探すあたりで時間がかかっているので、探索時間が短くなれば効果はあるはず。
元記事のソースは 8x8 のディザで処理しているけれど、見た感じ、4x4 のディザでも十分かなと思えた。8x8のディザだと、一番深いところで64回ループが回るけど、4x4なら16回のループで済むので、単純計算で約4倍の速さになりそう。
並列処理を導入するのも手かもしれない。Python で並列処理をさせる方法が分からなかったので今回は試してないけど、今時のCPUなら4コアだの6コアだの持ってるだろうから、1/4、1/6 の処理時間で済むのではなかろうか。実際、元々のC++版は、OpenMP(?) を有効にしてビルドして、並列処理する実行バイナリにしたら爆速になった。
画像の全てのドットに対して、毎回ループを回して、適切なパレットカラーを求めているあたりがアレだろうなという気もする。どれかしらの要素を事前に計算してキャッシュしておいて、そのキャッシュを使い回すようにすれば、もうちょっと改善されるかもしれない。例えばベタ部分が多い画像の場合、画像内に出てくる色の数は限定されるはずで、以前計算したことがある色が出現したら、その時の計算結果を再度使う、という作りにするだけでも効果はありそう。
元記事にも書いてあるけど、kd-tree 等を導入すれば改善される可能性もあるのだろうなと…。画像内のとある色について、差し替えに相応しい色はどれか、と探すあたりで時間がかかっているので、探索時間が短くなれば効果はあるはず。
◎ 余談。 :
このスクリプトで実験した後、減色ツールの OPTPiX や Yukari を使って減色をしてみたら、どれも一瞬で結果が返ってきて…。もしかすると1秒もかかってないのでは…。
それらはおそらくC/C++で書かれているのかなと想像するのだけど、それにしても処理時間が違い過ぎる。おそらく、アルゴリズムからして、もっと上手いやり方があるのだろう…。
それらはおそらくC/C++で書かれているのかなと想像するのだけど、それにしても処理時間が違い過ぎる。おそらく、アルゴリズムからして、もっと上手いやり方があるのだろう…。
[ ツッコむ ]
以上、1 日分です。