2022/05/01(日) [n年前の日記]
#1 [python][gimp] Pythonのstructを使った時の処理時間を調べてた
以前 GIMP の Python-Fuスクリプトを書いた際に、pycairo で描画したサーフェイス内容をGIMPのレイヤーに転送できる状態に変換する処理で時間がかかってしまっていたのだけど、そのあたりを改善できないか実験してみた。
_mieki256's diary - GIMPのPython-fu + pycairoで描いた画像をGIMPのレイヤー用に変換する処理について
_mieki256's diary - Ubuntu Linux 16.04LTS上でGIMPのPython-fuに関して試行錯誤中
上記のメモの中に出てくる、get_rgba_str() という関数が、問題の処理。struct を使って、ARGB(BGRA) で並んでいるデータを、RGBA に変換しているのだけど、ここがとにかく遅い。どうにかしたい。
_mieki256's diary - GIMPのPython-fu + pycairoで描いた画像をGIMPのレイヤー用に変換する処理について
_mieki256's diary - Ubuntu Linux 16.04LTS上でGIMPのPython-fuに関して試行錯誤中
上記のメモの中に出てくる、get_rgba_str() という関数が、問題の処理。struct を使って、ARGB(BGRA) で並んでいるデータを、RGBA に変換しているのだけど、ここがとにかく遅い。どうにかしたい。
◎ ベンチマークを取ってみた。 :
Python のモジュール、Benchmarker を使うと、各処理に対してベンチマークが取れるらしい。pip install Benchmarker でインストールできる。
_Benchmarker - PyPI
_Pythonのベンチマークライブラリ「Benchmarker」 - DISTRICT 37
これを利用して実験用のスクリプトを書いてみた。
環境は、Windows10 x64 21H2 + Python 2.7.18 32bit。CPU は AMD Ryzen 7 1700 (3GHz, 8コア16スレッド)。
_03_argb_rgba_conv.py
結果は以下。
これから分かることは…。
何にせよ、書き方を変えることで2倍ほど処理を速くできる ―― 半分の処理時間にすることができそうだなと分かった。
それでも、2048 x 2048 ドット分の変換処理に2秒以上かかってしまうのは残念だけど…。
でもまあ、一番最初に書いた版では15秒かかってた処理が、2秒にまで短縮できたのだから、これでもマシかもしれない。でも…もっと速くできないものかな…。
_Benchmarker - PyPI
_Pythonのベンチマークライブラリ「Benchmarker」 - DISTRICT 37
これを利用して実験用のスクリプトを書いてみた。
環境は、Windows10 x64 21H2 + Python 2.7.18 32bit。CPU は AMD Ryzen 7 1700 (3GHz, 8コア16スレッド)。
_03_argb_rgba_conv.py
from benchmarker import Benchmarker import struct LOOP = 5 def create_src_data(): print("start.") rgba = [] w, h = 2048, 2048 r, g, b, a = 0, 32, 64, 128 for y in range(h): for x in range(w): rgba.append(struct.pack('4B', b, g, r, a)) r += 1 g += 1 b += 1 a += 1 if r > 255: r = 0 if g > 255: g = 0 if b > 255: b = 0 if a > 255: a = 0 src = ''.join(rgba) print("create source data.") return src def get_rgba_str_slow(src): buf = "" size = len(src) for i in range(size / 4): i0 = i * 4 i1 = i0 + 4 bgra = struct.unpack('=L', src[i0: i1])[0] a = (bgra >> 24) & 0x0ff r = (bgra >> 16) & 0x0ff g = (bgra >> 8) & 0x0ff b = bgra & 0x0ff rgba = struct.pack('4B', r, g, b, a) buf += rgba return buf def get_rgba_str(src): unpack = struct.Struct('=L').unpack_from pack = struct.Struct('>L').pack lmax = len(src) / 4 rgba = [None] * lmax for i in range(lmax): d = unpack(src, i * 4)[0] rgba[i] = pack(((d & 0x00ffffff) << 8) | ((d >> 24) & 0x0ff)) return ''.join(rgba) def get_rgba_str_b(src): unpack = struct.Struct('=L').unpack_from pack = struct.Struct('>L').pack lmax = len(src) / 4 rgba = [] for i in range(lmax): d = unpack(src, i * 4)[0] rgba.append(pack(((d & 0x00ffffff) << 8) | ((d >> 24) & 0x0ff))) return ''.join(rgba) def get_rgba_str_c(src): lmax = len(src) / 4 argb = list(struct.unpack("=%dL" % lmax, src)) spack = struct.Struct('>L').pack rgba = [None] * lmax for i, d in enumerate(argb): rgba[i] = spack(((d & 0x0ffffff) << 8) | ((d >> 24) & 0x0ff)) return ''.join(rgba) def get_rgba_str_d(src): lmax = len(src) / 4 argb = list(struct.unpack("=%dL" % lmax, src)) rgba = [None] * lmax for i, d in enumerate(argb): rgba[i] = ((d & 0x0ffffff) << 8) | ((d >> 24) & 0x0ff) return struct.pack(">%dL" % lmax, *rgba) def get_rgba_str_e(src): lmax = len(src) / 4 argb = list(struct.unpack("=%dL" % lmax, src)) rgba = [] for d in argb: rgba.append(((d & 0x0ffffff) << 8) | ((d >> 24) & 0x0ff)) return struct.pack(">%dL" % lmax, *rgba) src = create_src_data() # dst0 = get_rgba_str_slow(src) # dst1 = get_rgba_str(src) # dst2 = get_rgba_str_b(src) # dst3 = get_rgba_str_c(src) # dst4 = get_rgba_str_d(src) # if dst1 == dst4: # print("Success") # else: # print("Failure") with Benchmarker(LOOP, width=20) as bench: @bench("argb split") def check_use_split(bm): get_rgba_str_slow(src) @bench("rgb shift A") def check_use_rgb_shift(bm): get_rgba_str(src) @bench("rgb shift B") def check_use_rgb_shift_b(bm): get_rgba_str_b(src) @bench("rgb shift C") def check_use_rgb_shift_c(bm): get_rgba_str_c(src) @bench("rgb shift D") def check_use_rgb_shift_d(bm): get_rgba_str_d(src) @bench("rgb shift E") def check_use_rgb_shift_e(bm): get_rgba_str_e(src)
結果は以下。
> py -2 03_argb_rgba_conv.py start. create source data. ## benchmarker: release 4.0.1 (for python) ## python version: 2.7.18 ## python compiler: MSC v.1500 32 bit (Intel) ## python platform: Windows-10-10.0.19041 ## python executable: C:\Python\Python27\python.exe ## cpu model: AMD64 Family 23 Model 1 Stepping 1, AuthenticAMD ## parameters: loop=5, cycle=1, extra=0 ## real (total = user + sys) argb split 15.2580 15.2656 11.4844 3.7812 rgb shift A 4.7980 4.7969 4.7812 0.0156 rgb shift B 5.0360 5.0312 4.9688 0.0625 rgb shift C 2.6200 2.6094 2.5156 0.0938 rgb shift D 2.0820 2.0938 2.0156 0.0781 rgb shift E 2.1110 2.1094 2.0469 0.0625 ## Ranking real rgb shift D 2.0820 (100.0) ******************** rgb shift E 2.1110 ( 98.6) ******************** rgb shift C 2.6200 ( 79.5) **************** rgb shift A 4.7980 ( 43.4) ********* rgb shift B 5.0360 ( 41.3) ******** argb split 15.2580 ( 13.6) *** ## Matrix real [01] [02] [03] [04] [05] [06] [01] rgb shift D 2.0820 100.0 101.4 125.8 230.5 241.9 732.9 [02] rgb shift E 2.1110 98.6 100.0 124.1 227.3 238.6 722.8 [03] rgb shift C 2.6200 79.5 80.6 100.0 183.1 192.2 582.4 [04] rgb shift A 4.7980 43.4 44.0 54.6 100.0 105.0 318.0 [05] rgb shift B 5.0360 41.3 41.9 52.0 95.3 100.0 303.0 [06] argb split 15.2580 13.6 13.8 17.2 31.4 33.0 100.0
これから分かることは…。
- 空リストに .append() で要素を追加していくよりも、事前に必要な分のリストを [None] * lmax で確保しておいて、xxx[i] = xxx で代入していったほうが速い。
- forループで4バイトずつ、struct.unpack_from("=L", src, i * 4)[0] で取り出して処理をしていくより、list(struct.unpack("=%dL" % lmax, src)) のように、フォーマット文字列内にカウント数を入れてしまって、forループを使わずに一気に取り出してリスト化したほうが速い。
- ''.join(rgba) でリストを結合して文字列を作るより、struct.pack(">%dL" % lmax, *rgba) のように、フォーマット文字列内にカウント数を入れて、リストをバイナリデータにしたほうが速い。
- forループの enumerate を無くしても、処理速度はそんなに変わらない。
何にせよ、書き方を変えることで2倍ほど処理を速くできる ―― 半分の処理時間にすることができそうだなと分かった。
それでも、2048 x 2048 ドット分の変換処理に2秒以上かかってしまうのは残念だけど…。
でもまあ、一番最初に書いた版では15秒かかってた処理が、2秒にまで短縮できたのだから、これでもマシかもしれない。でも…もっと速くできないものかな…。
◎ 参考ページ。 :
◎ メカっぽいテクスチャを生成するPython-Fuも更新。 :
メカっぽいテクスチャを生成できる、GIMP の Python-Fuスクリプトも更新しておいた。
_mieki256/sci-fi-texture-generator: Sci-Fi bump mapping texture generator with GIMP Script-fu.
ちなみに、動作確認していたら、以前の版は画像の右端と下端に、1ドットの透明な隙間ができてしまっていることに気が付いたので、そこも修正しておいた。
GIMP の選択範囲作成と、pycairo の矩形範囲の指定は、与えるパラメータの仕様がちょっと違っていたらしい。pycairo版は、GIMP描画機能使用版と比べて、全体的にあらゆる箇所で、右端と下端が1ドット少なくなっているのかもしれない…。でもまあ、それっぽい見た目になっていれば十分だろうし、これでもいいだろう…。
_mieki256/sci-fi-texture-generator: Sci-Fi bump mapping texture generator with GIMP Script-fu.
ちなみに、動作確認していたら、以前の版は画像の右端と下端に、1ドットの透明な隙間ができてしまっていることに気が付いたので、そこも修正しておいた。
GIMP の選択範囲作成と、pycairo の矩形範囲の指定は、与えるパラメータの仕様がちょっと違っていたらしい。pycairo版は、GIMP描画機能使用版と比べて、全体的にあらゆる箇所で、右端と下端が1ドット少なくなっているのかもしれない…。でもまあ、それっぽい見た目になっていれば十分だろうし、これでもいいだろう…。
◎ 余談。Windows版GIMPにはNumPyが入ってない。 :
この手の処理をPythonでする場合、NumPy を使うのが当たり前っぽいのだけど…。Windows版GIMP 2.8.x や 2.10.x には、NumPy が入ってないので使えないのだった…。
これがLinux版GIMPなら、システムに NumPy をインストールしておけば GIMP からも使えるらしい。
これがLinux版GIMPなら、システムに NumPy をインストールしておけば GIMP からも使えるらしい。
◎ 2022/05/02追記。 :
リスト内包表記を試してみたら、もうちょっとだけ処理を速くできた。とメモ。
[ ツッコむ ]
以上、1 日分です。