配列(リスト)の計算方法とチューニング

配列(リスト)の計算方法とチューニング

画像を処理するとき、ピクセルに対して操作を行うことは多いですが、ピクセル捜査の処理は時間がかかり、処理の書き方ひとつで処理時間が大きく変わります。

Pythonはリストの全てを計算することにかけては速いのですが、特定の条件の値のみ操作する場合、極端に遅くなります。

そこでどう記述すると速くなるのか考えてみましょう。

全ての値を同じ計算で処理する場合

まずはこちらの記事で使用したプログラムを元に、チューニングを試してみましょう。

テストに使用する画像(1920×1280)

まずはこの画像の色を反転する処理で比較検証してみましょう。サンプルプログラムを動作させる場合は、この画像をinput.jpgという名前でスクリプトと同じフォルダに配置してください。

まずはほかの言語で一般的な、for文の書き方で処理時間を見てみましょう。

なおこのサンプルプログラムを動作させるには、Pillow.Image、numpy、CoushLibをインポートしてください。

from PIL import Image
import numpy as np
import CoushLib as cl

# 最後の計測名で配列を画像に変換して保存する
def saveimage(a, rap):
    img = Image.fromarray(a)
    img.save(rap.history[len(rap.history) - 1][0] + ".jpg")

# 画像を開く
img = Image.open('input.jpg')

# 処理時間計測開始
rap = cl.RapTime()

# 画像をループで色を反転
a = np.array(img))
rap.reset()
# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
for col in a:
    for p in col:
        for i in range(3):
            p[i] = 255 - p[i]
rap.rapandprint("画像をループで色を反転")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
saveimage(a, rap)

# 処理時間の記録を全て出力
rap.printhistory()

処理時間の計測クラス定義があるためちょっと長いですが、肝心なのは計測開始~計測完了までの処理の書き方です。

# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
for col in a:
    for p in col:
        for i in range(3):
            p[i] = 255 - p[i]
rap.rapandprint("画像をループで色を反転")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
# 画像をループで色を反転 : 36.0547758 sec

画像のリストは、a[y][x][rgb]という構造の3次元配列で、RGBの明るさがそれぞれ入っています。

この処理ではほかの言語のfor文(C#だとforeach)のように、次元ごとにループを重ねて処理しています。

これにかかった処理時間はおよそ36秒。かなり遅いですね。

# ダメな例
# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
for col in a:
    for p in col:
        for c in p:
            c = 255 - c
rap.rapandprint("画像をループで色を反転")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
# 画像をループで色を反転 : 36.0547758 sec

ちなみに最初はこう書いていました。これでも動作はするのですが、最後のforループで計算しているcは、このループのために作られた変数で、値を変更してもp[n]が変更されるわけではありません。これで保存しても、画像の色は変わっていませんでした。

リストを渡されるループではポインタ渡しのような動作になり、int型を渡されるループではコピーが渡されるようです。c#でも同じなのですが、ポインタが渡されるものだと勝手に思い込んでいました。こういう思い込みがバグの要因ですね。

numpyでまとめて計算する

pythonの場合、配列を直接計算することができます。次のように処理を変えてみましょう。

# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
a = 255-a
rap.rapandprint("配列まるごと反転")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
# 配列まるごと反転 : 0.008621700000006172 sec

0.008秒という凄まじく短い時間で終わりました。ちなみにこの記述はnumpyのndarrayだからできる方法で、もしaがlistの場合はエラーになります。

forループはコストが非常に高く、Pythonにおいてはあまり良い記述ではないようです。

リスト内表記でndarrayを処理する(失敗例)

さらにPythonの場合、リスト内表記というものがあります。これは配列のループ計算を式の中でするもので、単純な計算処理の場合は1行で記述することができます。Python特有の記法ですが、処理時間に影響はあるのでしょうか。

# 失敗例
# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
a = [[[255 - a[y][x][c] for c in range(3)] for x in range(img.width)] for y in range(img.height)]
rap.rapandprint("リスト内表記で反転")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
# リスト内表記で反転 : 31.4740491 sec

結果は31秒で、forループとほとんど変わりません。

…と思ったら画像の保存でエラーが出ていました。

リスト内表記でfor文を使うと新しいリストが生成されるのですが、python標準のlist型になってしまうため、画像に変換できないのです。

また、ndarrayでリスト内表記をすると、結局forループで回すのと速度的に変わりません。内部的にforループと同じ処理がされ、最適化がされていないようです。

リスト内表記でlist配列に変換してから処理する(成功例)

そこで、一度リスト型に変換してから、リスト内表記のforで画像の色を反転してみました。

# リスト内表記で反転
a = np.array(img)
rap.reset()
a = a.tolist()
rap.rapandprint("リスト内表記で反転:ndarrayからlistに変換")
# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
a = [[[255 - a[y][x][c] for c in range(3)] for x in range(img.width)] for y in range(img.height)]
rap.rapandprint("リスト内表記で反転")
a = np.array(a, dtype=np.uint8)  # ndarrayにuint8型で変換
rap.rapandprint("リスト内表記で反転:listからndarrayに変換")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
saveimage(a, rap)
 リスト内表記で反転:ndarrayからlistに変換 : 1.4973572 sec
 リスト内表記で反転 : 4.203150600000001 sec
 リスト内表記で反転:listからndarrayに変換 : 1.4563525999999998 sec
 [total] : 7.1568604 sec

Python標準のlistで処理すると、内部で最適化されるのか、処理時間は4.2秒に短縮されました。ただし、ndarray、listを相互変換するのに3秒かかっていて、トータルでは7.2秒かかっています。

しかしそれでも、forループに比べるとだいぶ短い時間で終わりました。標準の型でリスト内表記でforを使うと、処理速度はかなり最適化されるようです。

しかし、チャンネル単位で変更するならndarrayが最速ですが、リスト内表記においてはlistの方が速いのは悩ましいものです。

変換コストがかかるので、できればどちらかで高速に処理したいのですが…。

おまけ:リスト内表記で赤と緑のチャンネルを入れ替える

おまけで、以前のサンプルで作ったRとGのチャンネルを入れ替える処理もやってみましょう。

# リスト内表記で緑赤入れ替え
a = np.array(img)
rap.reset()
a = a.tolist()
rap.rapandprint("リスト内表記で反転:ndarrayからlistに変換")
# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
a = [[[a[y][x][1], a[y][x][0], a[y][x][2]] for x in range(img.width)] for y in range(img.height)]
a = np.array(a, dtype=np.uint8)  # ndarrayにuint8型で変換
rap.rapandprint("リスト内表記で緑赤入れ替え")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
saveimage(a, rap)
リスト内表記で緑赤入れ替え:ndarrayからlistに変換 : 1.5375379 sec
リスト内表記で緑赤入れ替え : 5.8775731 sec
[total] : 7.4151110000000005 sec

こちらもだいぶ速くなりました。

おまけ:リスト内表記で画像をモノクロに変換する

以前の記事では遅かったモノクロ変換も、リスト内表記でだいぶ速くなりました。

# リスト内表記でモノクロ化
a = np.array(img)
rap.reset()
a = a.tolist()
rap.rapandprint("リスト内表記でモノクロ化:ndarrayからlistに変換")
# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
a = [[[sum(a[y][x]) // 3]*3 for x in range(img.width)] for y in range(img.height)]
rap.rapandprint("リスト内表記でモノクロ化")
a = np.array(a, dtype=np.uint8)  # ndarrayにuint8型で変換
rap.rapandprint("リスト内表記でモノクロ化:listからndarrayに変換")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
saveimage(a, rap)
リスト内表記でモノクロ化:ndarrayからlistに変換 : 1.5772261999999997 sec
 リスト内表記でモノクロ化 : 3.3156159 sec
 リスト内表記でモノクロ化:listからndarrayに変換 : 1.9466020000000004 sec
 [total] : 6.8394441 sec

ところで変換にかかる時間を細かく測ってみましたが、リストの計算処理よりも、ndarrayとlistの変換にかなり時間がかかっていますね…。

多くの処理を行うのであればこのくらいの速度は目をつむれそうですが、C#などのネイティブ処理に比べるとあまりにも遅くてちょっと画像処理には向いていない感じがしますね。

Pythonの文法は非常にわかりやすいので、これで速さが得られればいいのですが、大量のデータを処理するには向いてないのかもしれません。

サンプルプログラム

この記事の処理をひとまとめにしたサンプルプログラムがこちらです。

from PIL import Image
import numpy as np
import CoushLib as cl

# 最後の計測名で配列を画像に変換して保存する
def saveimage(a, rap):
    img = Image.fromarray(a)
    img.save(rap.history[len(rap.history) - 1][0] + ".jpg")

# 画像を開く
img = Image.open('input.jpg')

# 処理時間計測開始
rap = cl.RapTime()

# 画像をループで色を反転
a = np.array(img)
rap.reset()
# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
for col in a:
    for p in col:
        for i in range(3):
            p[i] = 255 - p[i]
rap.rapandprint("画像をループで色を反転")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
saveimage(a, rap)

# 配列まるごと反転
a = np.array(img)
rap.reset()
# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
a = 255 - a
rap.rapandprint("配列まるごと反転")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
saveimage(a, rap)

# リスト内表記で反転
a = np.array(img)
rap.reset()
a = a.tolist()
rap.rapandprint("リスト内表記で反転:ndarrayからlistに変換")
# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
a = [[[255 - a[y][x][c] for c in range(3)] for x in range(img.width)] for y in range(img.height)]
rap.rapandprint("リスト内表記で反転")
a = np.array(a, dtype=np.uint8)  # ndarrayにuint8型で変換
rap.rapandprint("リスト内表記で反転:listからndarrayに変換")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
saveimage(a, rap)

# リスト内表記で緑赤入れ替え
a = np.array(img)
rap.reset()
a = a.tolist()
rap.rapandprint("リスト内表記で緑赤入れ替え:ndarrayからlistに変換")
# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
a = [[[a[y][x][1], a[y][x][0], a[y][x][2]] for x in range(img.width)] for y in range(img.height)]
rap.rapandprint("リスト内表記で緑赤入れ替え")
a = np.array(a, dtype=np.uint8)  # ndarrayにuint8型で変換
rap.rapandprint("リスト内表記で緑赤入れ替え:listからndarrayに変換")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
saveimage(a, rap)

# リスト内表記でモノクロ化
a = np.array(img)
rap.reset()
a = a.tolist()
rap.rapandprint("リスト内表記でモノクロ化:ndarrayからlistに変換")
# ■■■■■■■■■■■■■■■■■ 計測開始 ■■■■■■■■■■■■■■■■■
a = [[[sum(a[y][x]) // 3]*3 for x in range(img.width)] for y in range(img.height)]
rap.rapandprint("リスト内表記でモノクロ化")
a = np.array(a, dtype=np.uint8)  # ndarrayにuint8型で変換
rap.rapandprint("リスト内表記でモノクロ化:listからndarrayに変換")
# ■■■■■■■■■■■■■■■■■ 計測終了 ■■■■■■■■■■■■■■■■■
saveimage(a, rap)

# 処理時間の記録を全て出力
rap.printhistory()
 画像をループで色を反転 : 39.959175099999996 sec
 配列まるごと反転 : 0.009160200000003726 sec
 リスト内表記で反転:ndarrayからlistに変換 : 1.264570599999999 sec
 リスト内表記で反転 : 5.2214613999999955 sec
 リスト内表記で反転:listからndarrayに変換 : 2.2530677000000026 sec
 リスト内表記で緑赤入れ替え:ndarrayからlistに変換 : 1.1972741000000013 sec
 リスト内表記で緑赤入れ替え : 4.353135600000002 sec
 リスト内表記で緑赤入れ替え:listからndarrayに変換 : 2.1219071999999954 sec
 リスト内表記でモノクロ化:ndarrayからlistに変換 : 1.460712700000002 sec
 リスト内表記でモノクロ化 : 3.0650376999999978 sec
 リスト内表記でモノクロ化:listからndarrayに変換 : 2.144742299999997 sec
 [total] : 63.05024459999999 sec

変換後画像

元画像
反転後
赤と緑を入れ替え後
モノクロ化後