0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python: 色塗り処理の効率化実験(2024.12.19)

Last updated at Posted at 2024-12-19

はじめに

FEMで応力計算を行った際、応力値に応じて要素を塗りつぶした出力図を作成するのだが、この処理が非常に遅い。
このため、色塗り処理の効率化方法を試してきたのだが、現在私が採用している手法について紹介することにする。

採用しているのか、以下の2つの手法である。

  1. PatchCollectionの利用
  2. ProcessPool(並列処理)の利用

テストプログラム作成に際し、以下の記事を参考にさせていただいた。

並列処理、並行処理、非同期処理をPythonで学ぶ
Python, Pillowで画像を縦・横に連結(結合)

また自分の記事で恐縮だが、以下も参考に。
matplotlib ポリゴン塗りつぶし高速化(2023.07.30投稿)

当方の環境は以下の通り。

MacBook PRo 14" M1 Pro
macOS Sequoia 15.2
Pytho 3.13.0
matplotlib 3.9.3

テストプログラムの概要

やっていることは、領域を正方形に分割して、指定した色で塗りつぶしていくものである。
作例を以下に示す。6枚の画像を作成し、これを結合したもの。時間計測を目的としたものなので、物理的な意味は特にない。下図は、正方形領域を 50 x 50 の領域に分割し、乱数を用いて色を指定し、塗りつぶし処理を行なったもの。

fig_50x50.jpg

実験では、領域分割を、10x10, 20x20, 50x50, 100x100, 200x200, 500x500 としている。

用いた処理手法の概要

手法1

最も単純な方法。領域を1つづつ、fillで塗りつぶしていく。コードは比較的単純である。

    for i in range(0,mm):
        for j in range(0,nn):
            icol=random.randrange(0,len(col),1)
            x=np.array([j*dx,(j+1)*dx,(j+1)*dx,j*dx])
            y=np.array([i*dy,i*dy,(i+1)*dy,(i+1)*dy])
            plt.fill(x,y,color=col[icol])

手法2

作図情報をリストにして溜め込み、PatchCollectionを用いて一気に書き出す。fillを用いる方法に比べ、コードは少し面倒になるが、効率化の効果は大きい。

    patches=[]
    lcol=[]
    for i in range(0,mm):
        for j in range(0,nn):
            icol=random.randrange(0,len(col),1)
            x1,x2,x3,x4=j*dx,(j+1)*dx,(j+1)*dx,j*dx
            y1,y2,y3,y4=i*dy,i*dy,(i+1)*dy,(i+1)*dy
            points=[[x1,y1],[x2,y2],[x3,y3],[x4,y4],[x1,y1]]            
            polygon=Polygon(xy=points,closed=True)
            patches.append(polygon)
            lcol.append(col[icol])
    p=PatchCollection(patches,facecolor=lcol,edgecolor='#aaaaaa',lw=0.5)
    plt.gca().add_collection(p)

手法3

ProcessPoolExecutorを用いた並列処理。6枚の図面描画を一気に投げ込んであとは機械に任せる。

with ProcessPoolExecutor(max_workers=8) as executor:
    executor.submit(fig_b,1,nn,mm,'fig1',col1)
    executor.submit(fig_b,2,nn,mm,'fig2',col2)
    executor.submit(fig_b,3,nn,mm,'fig3',col3)
    executor.submit(fig_b,4,nn,mm,'fig4',col1)
    executor.submit(fig_b,5,nn,mm,'fig5',col2)
    executor.submit(fig_b,6,nn,mm,'fig6',col3)

実験結果

ケースの説明

Case-1:(手法1)fillを用いた単純な方法。並列処理なし。
Case-2:(手法2)PatchCollectionを用いた方法。並列処理なし。
Case-3:(手法1+手法3)fillを用いた単純な方法。並列処理あり。
Case-4:(手法2+手法3)PatchCollectionを用いた方法。並列処理あり。
Case-5:(参考)画像結合だけの処理。

Case Description
Case-1 fill
Case-2 PatchCollection
Case-3 fill + ProcessPool
Case-4 PatchCollection + ProcessPool
Case-5 Only combine

処理時間の比較

実験結果を以下に示す。div は領域の分割数を示し、各ケースの数値は処理に要した時間(秒)を示す。
表より、以下のことがわかる。

  • PatchCollectionを用いる効果は、並列処理より大きい。
  • 処理量が増えるとPatchCollectionと並列処理の併用により大きな時間短縮効果を得ることができる。
  • 画像の結合に要する時間は、計算処理時間に比較して0.2秒程度と小さく、(私の場合)待ち時間としては許容できる。
div Case-1 Case-2 Case-3 Case-4 Case-5
10x10 0.589 0.394 0.795 0.712 0.105
20x20 0.988 0.421 0.868 0.775 0.116
50x50 4.108 0.839 1.504 0.849 0.131
100x100 15.558 2.057 3.561 1.096 0.156
200x200 63.498 7.197 12.396 1.909 0.190
500x500 396.090 44.006 123.572 8.395 0.215

テストコード

関数fig_aは、fillを用いて領域を1つづつ塗りつぶす関数。
関数fig_bは、PatchCollectionを用いて一気に書き出す関数。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
from matplotlib.collections import PatchCollection
import time
from PIL import Image
from concurrent.futures import ProcessPoolExecutor
import random
import os


def combine(fnameF):
    def get_concat_h(im1, im2):
        dst = Image.new('RGB', (im1.width + im2.width, im1.height))
        dst.paste(im1, (0, 0))
        dst.paste(im2, (im1.width, 0))
        return dst
    def get_concat_v(im1, im2):
        dst = Image.new('RGB', (im1.width, im1.height + im2.height))
        dst.paste(im1, (0, 0))
        dst.paste(im2, (0, im1.height))
        return dst
    im1 = Image.open('_fig1.jpg')
    im2 = Image.open('_fig2.jpg')
    im3 = Image.open('_fig3.jpg')
    im4 = Image.open('_fig4.jpg')
    im5 = Image.open('_fig5.jpg')
    im6 = Image.open('_fig6.jpg')
    temp1=get_concat_v(im1,im4)
    temp2=get_concat_v(im2,im5)
    temp3=get_concat_v(im3,im6)
    temp=get_concat_h(temp1,temp2)
    get_concat_h(temp,temp3).save(fnameF)


def figpre():
    fsz=14
    xmin,xmax=0,10
    ymin,ymax=0,10
    ga=np.array([xmin,xmax,ymin,ymax])
    iw=10
    ih=iw/(xmax-xmin)*(ymax-ymin)
    plt.figure(figsize=(iw,ih),facecolor='w')
    plt.rcParams['font.size']=fsz
    plt.rcParams['font.family']='sans-serif'
    plt.xlim([xmin,xmax])
    plt.ylim([ymin,ymax])
    plt.axis('off')
    plt.gca().set_aspect('equal',adjustable='box')
    return ga


def fig_a(icase,nn,mm,tstr,col):
    fnameF='_fig{0}.jpg'.format(icase)
    ga=figpre()
    xmin,xmax=ga[0],ga[1]
    ymin,ymax=ga[2],ga[3]
    plt.title(tstr,loc='left')
    dx,dy=(xmax-xmin)/nn,(ymax-ymin)/mm
    for i in range(0,mm):
        for j in range(0,nn):
            icol=random.randrange(0,len(col),1)
            x=np.array([j*dx,(j+1)*dx,(j+1)*dx,j*dx])
            y=np.array([i*dy,i*dy,(i+1)*dy,(i+1)*dy])
            plt.fill(x,y,color=col[icol])
    plt.tight_layout()
    plt.savefig(fnameF, dpi=200, bbox_inches="tight", pad_inches=0.1)
    plt.close()
 

def fig_b(icase,nn,mm,tstr,col):
    fnameF='_fig{0}.jpg'.format(icase)
    ga=figpre()
    xmin,xmax=ga[0],ga[1]
    ymin,ymax=ga[2],ga[3]
    plt.title(tstr,loc='left')
    dx,dy=(xmax-xmin)/nn,(ymax-ymin)/mm
    patches=[]
    lcol=[]
    for i in range(0,mm):
        for j in range(0,nn):
            icol=random.randrange(0,len(col),1)
            x1,x2,x3,x4=j*dx,(j+1)*dx,(j+1)*dx,j*dx
            y1,y2,y3,y4=i*dy,i*dy,(i+1)*dy,(i+1)*dy
            points=[[x1,y1],[x2,y2],[x3,y3],[x4,y4],[x1,y1]]            
            polygon=Polygon(xy=points,closed=True)
            patches.append(polygon)
            lcol.append(col[icol])
    p=PatchCollection(patches,facecolor=lcol,edgecolor='#aaaaaa',lw=0.5)
    plt.gca().add_collection(p)
    plt.tight_layout()
    plt.savefig(fnameF, dpi=200, bbox_inches="tight", pad_inches=0.1)
    plt.close()


def fig(nn,mm,num):
    col1=['#cc0000','#ff0066','#ff9966','#ffcc66','#ffffaa','#00ffff','#00ccff','#0099ff','#0000ff','#000088']
    col2=['#000000','#333333','#666666','#999999','#cccccc','#ffffff']
    col3=['#000000','#000080','#0000ff','#008000','#008080','#00ff00','#00ffff','#800000','#800080','#808000','#808080','#c0c0c0','#ff0000','#ff00ff','#ffff00','#ffffff']

    match num:
        case 1:
            fig_a(1,nn,mm,'fig1',col1)
            fig_a(2,nn,mm,'fig2',col2)
            fig_a(3,nn,mm,'fig3',col3)
            fig_a(4,nn,mm,'fig4',col1)
            fig_a(5,nn,mm,'fig5',col2)
            fig_a(6,nn,mm,'fig6',col3)
            fnameF='fig_test1.jpg'
        case 2:
            fig_b(1,nn,mm,'fig1',col1)
            fig_b(2,nn,mm,'fig2',col2)
            fig_b(3,nn,mm,'fig3',col3)
            fig_b(4,nn,mm,'fig4',col1)
            fig_b(5,nn,mm,'fig5',col2)
            fig_b(6,nn,mm,'fig6',col3)
            fnameF='fig_test2.jpg'
        case 3:
            with ProcessPoolExecutor(max_workers=8) as executor:
                executor.submit(fig_a,1,nn,mm,'fig1',col1)
                executor.submit(fig_a,2,nn,mm,'fig2',col2)
                executor.submit(fig_a,3,nn,mm,'fig3',col3)
                executor.submit(fig_a,4,nn,mm,'fig4',col1)
                executor.submit(fig_a,5,nn,mm,'fig5',col2)
                executor.submit(fig_a,6,nn,mm,'fig6',col3)
            fnameF='fig_test3.jpg'
        case 4:
            with ProcessPoolExecutor(max_workers=8) as executor:
                executor.submit(fig_b,1,nn,mm,'fig1',col1)
                executor.submit(fig_b,2,nn,mm,'fig2',col2)
                executor.submit(fig_b,3,nn,mm,'fig3',col3)
                executor.submit(fig_b,4,nn,mm,'fig4',col1)
                executor.submit(fig_b,5,nn,mm,'fig5',col2)
                executor.submit(fig_b,6,nn,mm,'fig6',col3)
            fnameF='fig_test4.jpg'
        case 5:
            fnameF='fig_test5.jpg'
    combine(fnameF)


def main():
    for ndiv in [10,20,50,100,200,500]:
        nn,mm=ndiv,ndiv
        fnameTF='fig_{0}x{1}.jpg'.format(nn,mm)
        ss=''
        for num in [1,2,3,4,5]:
            start=time.time()
            fig(nn,mm,num)
            ctime=time.time()-start
            ss=ss+'{0:8.3f}|'.format(ctime)
        print('#|{0}x{1}|{2}'.format(nn,mm,ss))
        os.rename('fig_test5.jpg',fnameTF)


#---------------
# Execute
#---------------
if __name__ == '__main__': main()

以 上

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?