【Python/Pillow】線や円などの図形の描画

Pythonで線や円などの図形を書く方法としては、OpenCVやtkinterなどを用いても出来ますが、今回はPillowを用いて描画する方法です。tkinterの場合はこちらを参照ください。

Pillowで図形を描画するには、描画先のImageオブジェクトからImageDrawオブジェクトを作成し、このImageDrawオブジェクトに対して線などを描画を行います。

ImageDrawオブジェクトに線などを描画すると、書いたデータはImageオブジェクトに反映されます。

簡単なサンプル

from PIL import Image, ImageDraw

# カラーの画像データ(Imageオブジェクト)の作成
img = Image.new("RGB", (300, 100), "White")
# ImageDrawオブジェクトの作成
draw = ImageDraw.Draw(img)

# 直線の描画
draw.line([(10, 90), (290, 10)], fill = "Blue", width = 10)

# 画像の表示
img.show()

図形の種類

● 直線、折れ線

ImageDraw.line(xy, fill=None, width=0, joint=None)
xy 始点と終点、もしくは折れ線を構成する交点の座標をタプルのリスト[(x, y), (x, y), (x, y),・・・]
もしくは x,y座標のリスト[x, y, x, y, x, y,・・・]で指定します。
fill 線色を指定します。
width 線幅を指定します。
joint 折れ線の場合の、つなぎ目の状態を指定します。

何も指定しない(None)と線の継ぎ目に切れ目ができます。

“curve”を指定すると、丸く繋いでくれます。

● 矩形(四角形)

ImageDraw.rectangle(xy, fill=None, outline=None, width=1)
xy 左上と右下の座標をタプルのリスト[(x0, y0), (x1, y1)]
もしくは x,y座標のリスト[x0, y0, x1, y1]で指定します。
fill 領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。
width 線幅を指定します。

● 角の丸い矩形

ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1)
xy 左上と右下の座標をタプルのリスト[(x0, y0), (x1, y1)]
もしくは x,y座標のリスト[x0, y0, x1, y1]で指定します。
radius 角の半径を指定します。
fill 領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。
width 線幅を指定します。

● 楕円、円

ImageDraw.ellipse(xyfill=Noneoutline=Nonewidth=1)
xy 左上と右下の座標をタプルのリスト[(x0, y0), (x1, y1)]
もしくは x,y座標のリスト[x0, y0, x1, y1]で指定します。
fill 領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。
width 線幅を指定します。

● ポリゴン(折れ線の始点と終点を結んだ図形)

ImageDraw.polygon(xy, fill=None, outline=None)
xy 折れ線を構成する交点の座標をタプルのリスト[(x, y), (x, y), (x, y),・・・]
もしくは x,y座標のリスト[x, y, x, y, x, y,・・・]で指定します。
fill 閉じた領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。

● 正n角形

ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None)
bounding_circle 多角形に外接する円の中心(x, y)および半径r を指定します。
指定方法
(x, y, r) および ((x, y), r)
n_sides  何角形かを指定します。 3以上
rotation  回転角度を度数で指定します。
fill 閉じた領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。

● 円弧

ImageDraw.arc(xy, start, end, fill=None, width=0)
xy 円弧を構成する楕円を囲む矩形の左上と右下の座標をタプルのリスト[(x0, y0), (x1, y1)]
もしくは x,y座標のリスト[x0, y0, x1, y1]で指定します。
start 円弧の始点の角度(3時方向が0度、時計周りが正)を指定します。
end 円弧の終点の角度(3時方向が0度、時計周りが正)を指定します。
fill 線色を指定します。
width 線幅を指定します。

● 円弧(始点と終点が直線で結ばれた状態)

ImageDraw.chord(xy, start, end, fill=None, outline=None, width=1)
xy 円弧を構成する楕円を囲む矩形の左上と右下の座標をタプルのリスト[(x0, y0), (x1, y1)]
もしくは x,y座標のリスト[x0, y0, x1, y1]で指定します。
start 円弧の始点の角度(3時方向が0度、時計周りが正)を指定します。
end 円弧の終点の角度(3時方向が0度、時計周りが正)を指定します。
fill 閉じた領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。
width 線幅を指定します。

● 円弧(始点と終点がそれぞれ原点と直線で結ばれた状態)

ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=1)
xy 円弧を構成する楕円を囲む矩形の左上と右下の座標をタプルのリスト[(x0, y0), (x1, y1)]
もしくは x,y座標のリスト[x0, y0, x1, y1]で指定します。
start 円弧の始点の角度(3時方向が0度、時計周りが正)を指定します。
end 円弧の終点の角度(3時方向が0度、時計周りが正)を指定します。
fill 閉じた領域を塗りつぶす色を指定します。
outline 輪郭線の色を指定します。
width 線幅を指定します。

● 点

ImageDraw.point(xy, fill=None)
xy 点の座標をタプルのリスト[(x, y), (x, y), (x, y),・・・]
もしくは x,y座標のリスト[x, y, x, y, x, y,・・・]で指定します。
fill 点の色を指定します。

 

備考

色の指定は”Red”, ”Green”などの色の名前の文字列を使うか、R,G,Bの順で16進数で指定(#FF0000だと赤)するか、(R,G,B)のタプルで指定します。

(参考)

https://www.w3schools.com/colors/colors_names.asp

楕円や円弧の座標指定位置は、下図のようになります。

サンプルプログラム

from PIL import Image, ImageDraw
import random # 点のランダム表示用

# カラーの画像データ(Imageオブジェクト)の作成
img = Image.new("RGB", (600, 700), "White")
# ImageDrawオブジェクトの作成
draw = ImageDraw.Draw(img)

# 直線
draw.line([(10, 60), (290, 10)], fill = "Blue", width = 3)

# 折れ線
draw.line([(10, 100), (150, 180), (290, 100)], fill = "Green", width = 20)
draw.line([(10, 200), (150, 280), (290, 200)], fill = "Red", width = 20, joint="curve")

# 四角形
draw.rectangle([(10, 300), (290, 380)], fill = "#FF8C00", outline="#DC143C", width = 5)

# 角の丸い四角形
draw.rounded_rectangle([(10, 400), (290, 480)], radius = 15, fill = (0, 0, 255), outline="Red", width = 5)

# 楕円
draw.ellipse([(10, 500), (290, 580)], fill = "Magenta", outline="Cyan", width = 5)
# 円
draw.ellipse([(110, 600), (190, 680)], fill = "Green", outline="Yellow", width = 5)

# ポリゴン(始点と終点を結んだ多角形)
draw.polygon([(310, 0), (450, 30), (590, 0), (590, 80), (450, 50), (310, 80)], fill = "Red", outline="FireBrick")
draw.polygon([(310, 100), (410, 180), (490, 100), (590, 180)], fill = "blue", outline="red")

# 正n角形
draw.regular_polygon((450, 240, 40), 5, 0, fill = "blue", outline="red")

# 円弧
draw.arc([(310, 300), (590, 380)], 0, 240, fill = "Blue", width = 5)

# 円弧(始点と終点を結ぶ)
draw.chord([(310, 400), (590, 480)], 0, 240, fill = "Blue", outline="Red", width = 5)

# 円弧(始点・終点と原点を結ぶ)
draw.pieslice([(310, 500), (590, 580)], 0, 240, fill = "Blue", outline="Red", width = 5)

# 点
for i in range(1000):
    x = random.randrange(310, 590)
    y = random.randrange(600, 680)
    # 点の描画
    draw.point((x,y), fill = "Blue")

# 画像の保存
img.save("image.png")

# 画像の表示
img.show()

(実行結果)

参照ページ

https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html

https://www.w3schools.com/colors/colors_names.asp

関連記事

【Python/tkinter】線や円などの図形の描画

【Windows11】電卓の新機能

Windows11に標準的に搭載されている電卓のアプリですが、Windows11ではグラフ計算の機能が追加されていました。

Windows10の電卓の機能では、左上の3本線の部分をクリックすると、標準、関数電卓、プログラマー、日付の計算、各種コンバーターの機能がありましたが、ここにグラフ計算が追加されました。

右上の式を入力してくださいの部分に x と y を使った関数を書くと、グラフで表示してくれます。

上図のグラフはシグモイド関数とその微分ですが、以下のように書きました。

y=1/(1+e^(-x))

y=e^(-x)/(1+e^(-x))^2

 

その他にもWindows10にも同じ機能がありますが、個人的に気になった機能を書いておきます。

Windows11の電卓ではこれだけの機能があります。

通貨

その日のレートを取得して、通貨の変換を行います。

日付の計算

開始と終了の日にちを指定し、2つの日にちの差を計算してくれます。

時間

マイクロ秒、ミリ秒、秒、分、時間、日、週、年のそれぞれの時間を変換します。

ハフ変換

ハフ変換そのものは座標の変換処理なのですが、画像処理では、ハフ変換を用いて画像中の直線部分を抽出するのに用いられます。

ハフ変換と言うだけで、あんに直線検出を指している事が多くあります。

また、ハフ変換を拡張して、円の検出に用いられる場合もあります。

ハフ変換で出来ること

画像の中から、直線が途切れていても、直線らしき部分を抽出したり、前処理でエッジ抽出を行い、輪郭部分を検出する事もできます。

(元画像)

(直線検出)

(元画像)

(輪郭検出)

直線検出のしくみ

まず初めに直線として検出したい場所を二値化して抽出します。

被写体の輪郭を検出したい場合は、SobelCannyなどの前処理を行います。

二値化された座標(XY座標)に関して、X座標をいくつかに分割し、Y軸方向に二値化された座標をカウントします。

このカウントを点を原点に基点に回転させながら、繰り返しカウントを行います。

すると、点が直線的に並んだ時にカウント数が最大となります。

直線なので、逆向き(回転角度が180度反対)の時もカウント数が最大となります。

このカウント数を横軸を回転角度、縦軸をX座標にして、二次元的に表示すると、下図のようになり、カウント値がピークとなるXとθの値から、直線を検出することができます。

ハフ変換のアルゴリズム

点を回転して特定方向に積算することで、直線を見つける事もできるのですが、ハフ変換では、各点を通る直線を回転させ、直線が重なりあう場所をみつける事で、直線を検出します。

点(x, y)を通る線を、下図のようにρθで表すと

$$\rho=x cos\theta+y sin\theta$$

となります。

これは、(x, y)座標が決まっていて、θを与えると、ρが計算できる事になります。

点(x,y)を通る直線をθを0~360°の範囲で刻むと下図のようになります。

この直線をρとθで表すと、下図のようにsinカーブとなります。

 

この処理を各点に関して行うと、点が直線的に並んでいる部分では特定のρとθに集中します。

このρとθの値が集中している点を抽出し、ρとθの値から直線を検出します。

実際のハフ変換

ハフ変換の角度は0~360°で計算すると、必ず180°離れた2か所でρとθが集中するので、0~180°までを計算します。

実際のハフ変換では、エッジ上のxy座標から、θの値を例えば1°おきに0~180°までに対応したρの値を計算し、ρの値の分解能を例えば1などに落として、ρーθの座標系へ投票し、投票した値のカウント数が高い部分が直線となる部分のρ、θとなります。

OpenCV(Python)のサンプルプログラム

import cv2
import math
import numpy as np

# 画像読込
src = cv2.imread('keyboard.jpg', cv2.IMREAD_GRAYSCALE);
# 二値化
_, src = cv2.threshold(src, 200, 255, cv2.THRESH_BINARY)

cv2.imshow("edge image", src)

# ハフ変換
lines = cv2.HoughLines(src, 3, np.pi / 180, 400)

# 結果表示用の画像を作成
dst = cv2.cvtColor(src, cv2.COLOR_GRAY2BGR);

# 直線を描画
line_length = 1000
for line in lines:
    rho = line[0][0]
    theta = line[0][1]
    a = math.cos(theta)
    b = math.sin(theta)
    x0 = a * rho
    y0 = b * rho
    cv2.line(
        dst, 
        (int(x0 - line_length * b), int(y0 + line_length * a)), 
        (int(x0 + line_length * b), int(y0 - line_length * a)), 
        (0, 0, 255), thickness=2, lineType=cv2.LINE_4 )

cv2.imshow("result", dst)

cv2.waitKey()

(処理前の画像)

(処理後画像)

注意事項

ハフ変換を行うと、直線らしい部分が抽出できますが、この直線の位置の精度を高めようと、ハフ変換時のθとρの分解能を高め過ぎると、直線検出の安定性が悪くなります。

そのため、直線の位置精度を高めるには、ハフ変換で直線付近の座標を抽出し、その座標から回帰直線などを求めるようにします。