mieki256's diary



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 に変換しているのだけど、ここがとにかく遅い。どうにかしたい。

ベンチマークを取ってみた。 :

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
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 - How can I struct.unpack many numbers at once - Stack Overflow

メカっぽいテクスチャを生成する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ドット少なくなっているのかもしれない…。でもまあ、それっぽい見た目になっていれば十分だろうし、これでもいいだろう…。

余談。Windows版GIMPにはNumPyが入ってない。 :

この手の処理をPythonでする場合、NumPy を使うのが当たり前っぽいのだけど…。Windows版GIMP 2.8.x や 2.10.x には、NumPy が入ってないので使えないのだった…。

これがLinux版GIMPなら、システムに NumPy をインストールしておけば GIMP からも使えるらしい。

2022/05/02追記。 :

リスト内包表記を試してみたら、もうちょっとだけ処理を速くできた。とメモ。

以上です。

過去ログ表示

Prev - 2022/05 - Next
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project