nao-milkの経験ブログ

25年間の半導体エンジニア経験で知り得た内容を記載したブログです。

Pythonで画像描画

f:id:nao-milk:20210408013747p:plain
Pythonを独学して3日目ですが、数式からハートマークを作成する画像描画プログラムを作成してみました。

修正履歴

facebookである方より、アドバイス頂いたので修正しました。
※アドバイスありがとうございました。

[アドバイス内容]
 pow()なのか**なのか統一しません?
 つーか、なんで混在しているんですか?
 違いはわかっていますか?

[修正内容]
 「ソースコード」の章に記述した「HeartSymbol()」内のべき乗を「**」に統一しました。
 尚、
 混在した理由は、べき乗の演算を「**」にすべきか、
 「pow」にするべきかを迷っており、そのまま投稿してしまいました。

[新たな発見]
今回のアドバイスで「べき乗」は、「**」と「pow」と「math.pow」があることが分かりました。
この3つの演算の演算速度を測りました。(time.perf_counter_ns()で計測)
 「**」 :2,103,908,740 ns (2.103秒)
 「pow」:2,153,195,800 ns (2.153秒)
 「math.pow」:2,123,512,380 ns (2.123秒)
 ※各5回実行して、平均した秒数となります。
今回の修正は、実行速度の速い「**」に統一しました。

[新たな疑問]
dis()を使ってコードを見たのですが、「math.pow」が一番多いのに、なぜ速度が2番目に速いのか?

本文の構成(目次)は、以下のようになります。
(出力画像は、下の方に添付しています。)

Window構成

勉強を始めて3日目なので、簡単になりますがWindowは以下のような構成になります。

f:id:nao-milk:20210408101706p:plain
Window構成

メインフレームの初期サイズは、640x640になります。

ソース構成

ソース構成は、以下の通りです。

f:id:nao-milk:20210408103516p:plain
ソース構成

演算式

ハートマークを生成するための演算式は、以下の通りです。
私がキレイと思うハートマークを作るため、変数xとyに係数を掛けています。

f:id:nao-milk:20210408103841p:plain
演算式

動作説明

プログラムの主な動作について説明します。
ソースコードは、後述でそのまま添付してしています。

1. 下地を生成

変数xとyの値(座標)からとして色成分(範囲:0~255)を置いていくため、オブジェクトPhotoImage()を使って512×512の下地を作っています。
これにより、メソッドput()を使用してxとy座標に色を設定していきます。

オブジェクトPhotoImageは、PNGなどのファイルを読み込み使用するみたいですが、幅と高さを指定するだけだと、画像を置くための下地を作ってくれるみたいでした。
tkinterの__init__.plを見ながら施行錯誤しました。

2. 画像生成

出力画像サイズを512×512としたため、X方向とY方向の位置を管理する変数pxとpyを準備しています。
また、
演算式ではX及びY座標を-32~32の範囲にしたかったため、変数txとtyで実際の座標位置を管理しています。
従って、座標精度(座標点の間隔)は0.125となります。(精度=64÷512)

変数txとtyから上記演算式の計算を行い(関数:HeartSymbol)、算出値の整数部を画素値(色値)としています。
ただし、
算出値が255を超える場合は255とし(関数:ClipValue)、"255-算出値"の値をR成分に設定しています。
※中心(座標x,y=0,0)に行くほど算出値が0になる性質の式のため、255から算出値を引いています。
 (ハートマークなので、中心に近づくほど「赤色」が良いかな?と思った次第です)
 尚、G成分及び及びB成分は0にしています。背景色が黒、ハートマークは赤となり、
 中心に近づくほど、赤色が濃くなっていきます。

3. Windowの表示

メインフレームCanvas領域(512×512)を作成し、上記「2.画像生成」で作成した画像を乗せています。
また、
ユーザーがメインフレームサイズを小さくした場合でも、視覚範囲で画像が見れるようスクロールバーを追加しました。

ファイル保存

画像表示だけでなくファイルにも保存したかったため、メソッドwriteを使ってPNG形式で保存しています。

補足)

オブジェクトPhotoImage()に「幅」と「高さ」のみを設定すると、データタイプは「photo」になりました。(おそらく、初期値だと思っています。)
RGBの設定がしたかったため、そのままにしていますが、「bitmap」(2値画像)にする方法までは確認していません。

ソースコード

ソースコードを以下に示します。
モジュール「csv」をインポートしていますが、演算結果などをExcelに保存して確認したかったために使用しました。下記ソース内では、モジュールcsvを使用したコードは削除しています。

import math
import tkinter as tk
import csv

#--
#-- 関数
#-----------------------------------------------------------

# @@ 座標からハートマークを生成する。
# @@
# @@    f(x,y) = (x*a)^2 + ((y*b) - ((x*c)^2)^(1/3))^2
# @@        a,b,c : 係数
# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
def HeartSymbol(
        PointX      ,   # X座標
        PointY          # Y座標
    ):
    # べき乗を「**」に統一
    #########################################
    ResVal = (PointX*0.60)**2 + (PointY*0.75 - (((PointX*1.5)**2)**(1/3)))**2

#    ResVal = pow( PointX*0.60                                ,2) +    \
#             pow((PointY*0.75 - pow(pow((PointX*1.5),2),1/3)),2)

#    ResVal = math.pow( PointX*0.60                                          ,2) +    \
#             math.pow((PointY*0.75 - math.pow(math.pow((PointX*1.5),2),1/3)),2)
    return  ResVal

# @@ クリップ処理
# @@  最大値から最小値内のValueを返す
# @@  変数Valueが最大値以上、最大値を返す。
# @@  変数Valueが最小値未満、最小値を返す。
# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
def ClipValue(
        Value       ,   # 入力値
        MaxVal      ,   # 最大値
        MinVal          # 最小値
    ):
    if(     Value > MaxVal ):   ResVal = MaxVal
    elif(   Value < MinVal ):   ResVal = MinVal
    else:                       ResVal = Value

    return  ResVal

#--
#-- Window 生成
#-----------------------------------------------------------
class WindowApp(tk.Frame):
    def __init__(self,
            master      = None          ,   # メインWindow
            title       = "Nao-Milk"    ,   # Windowタイトル
            width       = 64            ,   # Canvasの幅
            height      = 32            ,   # Canvasの高さ
            filename    = "play.png"        # 出力画像ファイル名 
        ):
        super().__init__(master)
        self.pack()
        self.master.title(title)
        self.master.geometry("640x640")

        # [ Create Screen ]
        self.image = tk.PhotoImage(width=width,height=height)   #++ 下地作成
        self.sizex = self.image.width()                         #++ 下地の幅(確認)
        self.sizey = self.image.height()                        #++ 下地の高さ(確認)
        self.type  = self.image.type()                          #++ 下地のType(確認)
                                                                #++   [e.g. "photo" or "bitmap".]
        print(" --> W x H = ",self.sizex, self.sizey , "type[",self.type ,"]")

        # [ Generate Image ]  R成分のみ演算
        #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
        HalfTX = 32.0;                                          #++ X方向 座標領域の半分
        HalfTY = 32.0;                                          #++ Y方向 座標領域の半分

        HalfPX = int(self.sizex/2);     HalfPY = int(self.sizey/2);     #++ 出力領域の半分
        StartX = HalfTX*-1;             StartY = HalfTY* 1;             #++ 座標の開始位置
        ResolX = (1/HalfPX*HalfTX)* 1;  ResolY = (1/HalfPY*HalfTY)*-1   #++ 座標間隔

        ty = StartY
        for py in range(0,self.sizey):
            tx = StartX
            for px in range(0,self.sizex):
                # ----- 演算 -----
                fr = HeartSymbol(tx,ty)
                fg = 0
                fb = 0
                r = 255-ClipValue(math.floor(fr),255,0)         #++ R成分
                g =     ClipValue(math.floor(fg),255,0)         #++ G成分
                b =     ClipValue(math.floor(fb),255,0)         #++ B成分

                # ----- 出力位置に色を設定 -----
                color = "#" + format(r, '02x') + format(g, '02x') + format(b, '02x')
                self.image.put(color,to=(px,py))

                tx = tx + ResolX

            ty = ty + ResolY

        #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        # [ display graphical elements ]
        self.canvas = tk.Canvas(self.master,width=self.sizex,height=self.sizey,bg="black")
        self.canvas.create_image(0,0,image=self.image,anchor=tk.NW)

        # [ Scrollbar ]
        bar_y = tk.Scrollbar(self.master, orient=tk.VERTICAL)
        bar_x = tk.Scrollbar(self.master, orient=tk.HORIZONTAL)
        bar_y.pack(side=tk.RIGHT , fill=tk.Y)
        bar_x.pack(side=tk.BOTTOM, fill=tk.X)
        bar_y.config(command=self.canvas.yview)
        bar_x.config(command=self.canvas.xview)
        self.canvas.config(yscrollcommand=bar_y.set, xscrollcommand=bar_x.set)
        self.canvas.config(scrollregion=(0, 0, self.sizex, self.sizey))
        self.canvas.pack(anchor=tk.NW, side=tk.LEFT)
        
        # [ File Save ]
        self.image.write(filename=filename)

#--
#-- Main
#-----------------------------------------------------------
if __name__ == "__main__":
    Frame1 = tk.Tk()                                # ++ Toplevel widget
    WinAp1 = WindowApp(
                master      = Frame1            ,   # ++ メインWindow
                title       = "Nao-Milk Play2"  ,   # ++ Windowタイトル
                width       = 512               ,   # ++ Canvasの幅
                height      = 512               ,   # ++ Canvasの高さ
                filename    = "play2.png"           # ++ 出力画像ファイル名
            )
    WinAp1.mainloop()

出力画像

プログラムを実行して生成した画像を以下に示します。

f:id:nao-milk:20210408012848p:plain
出力画像

実行環境

最後に

全てのモジュールが分かってないため、もっとスムーズなやり方があるかもしれませんが、今の知識ではここまででした。
尚、「やりたいこと」の思考からネットで「記述例」を調べ参考にしつつ、モジュールのソースも見ながら、試行錯誤しながら作成してみました。
(各モジュールの気持ち[意図]を知るため。)

Instagramでは、緑のハート、青のハートも載せています。
https://www.instagram.com/nao_nari0504/