乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

Python

プログラミング未経験の方のための
プログラミング学習講座を作成しました

その名も
「1時間で学べるPythonプログラミング」


講義動画はこちら




この講座は初学者の方が
短時間でPython言語を学ぶことのできる
プログラミング学習用の講座です

プログラミングが分からないない方は
Python言語を通じて
プログラミングの基礎を学習できます

講座は動画に加えてGoogle Colabを用いて
手元でコードを動かすことのできます
コードがどう動くのかを確認をしながら
進めていってください

資料はここ:
Google Colabの資料


00:00 1.はじめに
02:13 2.導入方法
02:55 3.GoogleColaboratoryの操作方法
06:19 4.Pythonの計算の基礎
27:27 5.Pythonの制御文
42:14 6.Pythonのクラス
49:11 7.Pythonのその他構文
64:30 8.まとめ

なおPythonチートシートを作成しています。

コーディングに迷った際に役に立ち

WEB検索する時間を無くして

作業時間を効率化できます。

note
Pythonチートシート


 

今回は色々な図形を組み合わせて
デジタルアートを作ってみました


解説動画はこちら


 
はじめに

今回はPythonで作図できる図形を組み合わせて
デジタルアートを作るものです

今回紹介する図形は以下です。

リサージュ図形
ローズ曲線
スピログラフ
ヒルベルト曲線
ドラゴンカーブ
ペンローズタイル(没)
トロコイド曲線
曼荼羅

早速図形を描いてみましょう。


ライブラリのインストール 

今回はGoogle Colabを用いて作図します。
日本語は表示できないので
日本語表示のライブラリをインストールしておいてください。
pip install japanize_matplotlib







リサージュ図形

二つの正弦波を組み合わせてできる曲線です
周波数や位相の違いで様々なパターンが生成されます
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib

# パラメータ設定
A = 1
B = 1
a = 5
b = 4
delta = np.pi / 2

# tの範囲
t = np.linspace(0, 2 * np.pi, 1000)

# x, y の計算
x = A * np.sin(a * t + delta)
y = B * np.sin(b * t)

# 描画
plt.figure(figsize=(6, 6))
plt.plot(x, y)
plt.axis('off')
plt.show()
download


import numpy as np
import matplotlib.pyplot as plt

# 図形の数と色の設定
num_shapes = 10
colors = plt.cm.viridis(np.linspace(0, 1, num_shapes))

# 描画設定
plt.figure(figsize=(8, 8))

# 各リサージュ図形の描画
for i in range(num_shapes):
    A = 1
    B = 1
    a = np.random.randint(1, 10)
    b = np.random.randint(1, 10)
    delta = np.random.uniform(0, 2 * np.pi)
    
    t = np.linspace(0, 2 * np.pi, 1000)
    x = A * np.sin(a * t + delta)
    y = B * np.sin(b * t)
    
    plt.plot(x, y, color=colors[i], alpha=0.7)

# グラフの設定
plt.title("複雑なリサージュアート")
plt.axis('equal')
plt.axis('off')
plt.show()
download-1


ローズ曲線

極座標を使って描く花のような形の曲線です
import numpy as np
import matplotlib.pyplot as plt

# パラメータ設定
k = 5  # kは整数または分数
a = 1  # 振幅

# tの範囲
t = np.linspace(0, 2 * np.pi, 1000)

# x, y の計算
x = a * np.cos(k * t) * np.cos(t)
y = a * np.cos(k * t) * np.sin(t)

# 描画
plt.figure(figsize=(6, 6))
plt.plot(x, y, color='magenta')
plt.title("ローズ曲線")
plt.axis('equal')
plt.axis('off')
plt.show()
download-2


import numpy as np
import matplotlib.pyplot as plt

# パラメータのリスト
k_values = np.arange(1, 6)
a_values = np.linspace(0.5, 2.5, 5)

# プロット設定
fig, axes = plt.subplots(5, 5, figsize=(6,6))
fig.suptitle("ローズ曲線のグリッド", fontsize=16)

# 各プロットの描画
for i, a in enumerate(a_values):
    for j, k in enumerate(k_values):
        t = np.linspace(0, 2 * np.pi, 1000)
        x = a * np.cos(k * t) * np.cos(t)
        y = a * np.cos(k * t) * np.sin(t)
        
        ax = axes[i, j]
        ax.plot(x, y, color='magenta')
        ax.set_title(f'k={k}, a={a:.1f}')
        ax.axis('equal')
        ax.set_xlim(-2.5, 2.5)
        ax.set_ylim(-2.5, 2.5)
        ax.axis('off')  # 軸を非表示

plt.tight_layout(rect=[0, 0, 1, 0.96])
plt.show()
download-3



スピログラフ

歯車を使って描くような複雑な曲線です
パラメータを変えることで多様な形ができます

小学生の時とかに作図キットを持っている人もいたかも
あれをPythonで再現します
import numpy as np
import matplotlib.pyplot as plt

# スピログラフのパラメータ
R = 100  # 大きい円の半径
r = 30   # 小さい円の半径
d = 160   # 描画する点の距離

# 最大公約数を使って完全なループを描画
gcd = np.gcd(R, r)
lcm = (R * r) // gcd
t = np.linspace(0, 2 * np.pi * (r // gcd), 1000 * (r // gcd))

# 描画設定
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_xlim(-R - r - d, R + r + d)
ax.set_ylim(-R - r - d, R + r + d)
ax.set_aspect('equal')
ax.axis('off')

# スピログラフの描画
x = (R - r) * np.cos(t) + d * np.cos((R - r) / r * t)
y = (R - r) * np.sin(t) - d * np.sin((R - r) / r * t)
ax.plot(x, y, color='blue')
plt.show()
download-4


import numpy as np
import matplotlib.pyplot as plt

# スピログラフのパラメータ
r = 30  # 小さい円の半径

# 描画設定
fig, axs = plt.subplots(5, 5, figsize=(8,8))
fig.subplots_adjust(hspace=0.3, wspace=0.3)

# Rとdを変化させてプロット
R_values = np.linspace(50, 150, 5)
d_values = np.linspace(20, 80, 5)

for i, R in enumerate(R_values):
    for j, d in enumerate(d_values):
        ax = axs[i, j]
        gcd = np.gcd(int(R), r)
        t = np.linspace(0, 2 * np.pi * (r // gcd), 1000 * (r // gcd))
        
        x = (R - r) * np.cos(t) + d * np.cos((R - r) / r * t)
        y = (R - r) * np.sin(t) - d * np.sin((R - r) / r * t)
        
        ax.plot(x, y, color='blue')
        ax.set_xlim(-R - r - d, R + r + d)
        ax.set_ylim(-R - r - d, R + r + d)
        ax.set_aspect('equal')
        ax.axis('off')
        ax.set_title(f"R={int(R)}, d={int(d)}", fontsize=8)

plt.show()
download-5

import numpy as np
import matplotlib.pyplot as plt

# スピログラフのパラメータ
R = 150  # 大きい円の半径
r1 = 30  # 中間の円の半径
r2 = 15  # 小さい円の半径
d = 50   # 描画する点の距離

# 描画設定
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_xlim(-R - r1 - r2 - d, R + r1 + r2 + d)
ax.set_ylim(-R - r1 - r2 - d, R + r1 + r2 + d)
ax.set_aspect('equal')
ax.axis('off')

# スピログラフの描画
gcd = np.gcd(np.gcd(R, r1), r2)
t = np.linspace(0, 2 * np.pi * (r2 // gcd), 1000 * (r2 // gcd))

x = (R - r1) * np.cos(t) + d * np.cos((R - r1) / r1 * t) + r2 * np.cos((R - r1) / r2 * t)
y = (R - r1) * np.sin(t) - d * np.sin((R - r1) / r1 * t) - r2 * np.sin((R - r1) / r2 * t)

ax.plot(x, y, color='blue')
plt.show()
download-6

import numpy as np
import matplotlib.pyplot as plt

# パラメータ設定
R = 100  # 大きい円の半径
r = 30   # 小さい円の半径
d = 60   # 描画する点の距離
a = 5    # リサージュのx軸周波数
b = 4    # リサージュのy軸周波数
delta = np.pi / 2  # 位相差

# 描画設定
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_xlim(-R - r - d, R + r + d)
ax.set_ylim(-R - r - d, R + r + d)
ax.set_aspect('equal')
ax.axis('off')

# スピログラフの方程式
gcd = np.gcd(R, r)
t = np.linspace(0, 2 * np.pi * (r // gcd), 1000 * (r // gcd))

x_spiro = (R - r) * np.cos(t) + d * np.cos((R - r) / r * t)
y_spiro = (R - r) * np.sin(t) - d * np.sin((R - r) / r * t)

# リサージュの方程式
x_lissajous = np.sin(a * t + delta)
y_lissajous = np.sin(b * t)

# 合成
x = x_spiro + x_lissajous * 10
y = y_spiro + y_lissajous * 10

# 図形の描画
ax.plot(x, y, color='blue')
plt.show()
download-7




ヒルベルト曲線

空間を埋め尽くすフラクタルの一種で
曲線が空間を埋めるように描かれます
import matplotlib.pyplot as plt

def hilbert_curve(x0, y0, xi, xj, yi, yj, n):
    if n <= 0:
        x_mid = x0 + (xi + yi) // 2
        y_mid = y0 + (xj + yj) // 2
        points.append((x_mid, y_mid))
    else:
        hilbert_curve(x0, y0, yi // 2, yj // 2, xi // 2, xj // 2, n - 1)
        hilbert_curve(x0 + xi // 2, y0 + xj // 2, xi // 2, xj // 2, yi // 2, yj // 2, n - 1)
        hilbert_curve(x0 + xi // 2 + yi // 2, y0 + xj // 2 + yj // 2, xi // 2, xj // 2, yi // 2, yj // 2, n - 1)
        hilbert_curve(x0 + xi // 2 + yi, y0 + xj // 2 + yj, -yi // 2, -yj // 2, -xi // 2, -xj // 2, n - 1)

# 描画設定
order = 6  # ヒルベルト曲線の階数
points = []
hilbert_curve(0, 0, 2**order, 0, 0, 2**order, order)

# プロット
x_vals, y_vals = zip(*points)
plt.plot(x_vals, y_vals)
plt.title(f'Hilbert Curve of Order {order}')
plt.axis('equal')
plt.axis('off')
plt.show()
download-8





ドラゴンカーブ

繰り返し折り返すことで生成されるフラクタル曲線です
中華のどんぶりに描かれている模様っぽいものです
import matplotlib.pyplot as plt
from math import cos, sin, radians

def dragon_curve(order, step=1):
    def recursive_dragon(n, angle, sign):
        if n == 0:
            forward(step)
        else:
            recursive_dragon(n - 1, angle, 1)
            turn(sign * angle)
            recursive_dragon(n - 1, angle, -1)

    def forward(dist):
        nonlocal x, y
        x += dist * directions[0]
        y += dist * directions[1]
        points.append((x, y))

    def turn(angle):
        cos_theta = cos(angle)
        sin_theta = sin(angle)
        directions[0], directions[1] = (
            directions[0] * cos_theta - directions[1] * sin_theta,
            directions[0] * sin_theta + directions[1] * cos_theta
        )

    # 初期化
    x, y = 0, 0
    directions = [1, 0]  # 初期方向
    points = [(x, y)]

    # ドラゴンカーブの生成
    recursive_dragon(order, radians(90), 1)
    return points

# 描画
order = 10  # ドラゴンカーブの階数
points = dragon_curve(order)
x_vals, y_vals = zip(*points)

plt.plot(x_vals, y_vals)
plt.title(f'Dragon Curve of Order {order}')
plt.axis('equal')
plt.axis('off')
plt.show()
download-9





ペンローズタイル(没)

本来は非周期的に平面を埋め尽くすタイルのパターンですが
これはちょいとちがっちゃってます
import matplotlib.pyplot as plt
import numpy as np

def draw_kite(ax, x, y, angle, size):
    # カイトの4つの頂点を計算
    points = np.array([
        [0, 0],
        [np.cos(angle), np.sin(angle)],
        [np.cos(angle + np.pi / 5), np.sin(angle + np.pi / 5)],
        [np.cos(angle - np.pi / 5), np.sin(angle - np.pi / 5)]
    ]) * size
    points += [x, y]
    ax.fill(points[:, 0], points[:, 1], edgecolor='black', fill=False)

def draw_dart(ax, x, y, angle, size):
    # ダートの4つの頂点を計算
    points = np.array([
        [0, 0],
        [np.cos(angle), np.sin(angle)],
        [np.cos(angle + np.pi / 5) / 2, np.sin(angle + np.pi / 5) / 2],
        [np.cos(angle - np.pi / 5) / 2, np.sin(angle - np.pi / 5) / 2]
    ]) * size
    points += [x, y]
    ax.fill(points[:, 0], points[:, 1], edgecolor='black', fill=False)

def draw_penrose_tiling(order, size):
    fig, ax = plt.subplots()
    ax.set_aspect('equal')
    ax.axis('off')

    for i in range(order):
        angle = i * 2 * np.pi / order
        draw_kite(ax, 0, 0, angle, size)
        draw_dart(ax, 0, 0, angle, size)

    plt.show()

# 描画
order = 30
size = 1000
draw_penrose_tiling(order, size)
download-10


import matplotlib.pyplot as plt
import numpy as np

def draw_penrose(ax, x, y, angle, size, depth):
    if depth == 0:
        return
    
    golden_ratio = (1 + np.sqrt(5)) / 2
    new_size = size / golden_ratio

    # パチ形の描画
    points = np.array([
        [0, 0],
        [np.cos(angle) * size, np.sin(angle) * size],
        [np.cos(angle + 2 * np.pi / 5) * size, np.sin(angle + 2 * np.pi / 5) * size],
        [np.cos(angle - 2 * np.pi / 5) * new_size, np.sin(angle - 2 * np.pi / 5) * new_size]
    ])
    points += np.array([x, y])
    ax.fill(points[:, 0], points[:, 1], edgecolor='black', fill=False)

    # 再帰的に描画
    for i in range(5):
        new_angle = angle + i * 2 * np.pi / 5
        draw_penrose(ax, points[i % 4, 0], points[i % 4, 1], new_angle, new_size, depth - 1)

def plot_penrose_tiling(size, depth):
    fig, ax = plt.subplots()
    ax.set_aspect('equal')
    ax.axis('off')
    draw_penrose(ax, 0, 0, 0, size, depth)
    plt.show()

# 描画
size = 300
depth = 2
plot_penrose_tiling(size, depth)
download-11


トロコイド曲線

円が他の円の内側や外側を転がることで描かれる曲線です
ほぼほぼスピログラフですね
import matplotlib.pyplot as plt
import numpy as np

# トロコイド曲線のパラメータ
R = 5  # 大きな円の半径
r = 3  # 小さな円の半径
d = 5  # 描画する点の距離

# tの範囲
t = np.linspace(0, 10 * np.pi, 1000)

# トロコイド曲線の方程式
x = (R - r) * np.cos(t) + d * np.cos((R - r) / r * t)
y = (R - r) * np.sin(t) - d * np.sin((R - r) / r * t)

# 描画
plt.plot(x, y)
plt.title('Trochoid Curve')
plt.axis('equal')
plt.axis('off')
plt.show()
download-12

import matplotlib.pyplot as plt
import numpy as np

# 固定パラメータ
R = 6  # 大きな円の半径

# tの範囲
t = np.linspace(0, 10 * np.pi, 1000)

# プロットの準備
fig, axes = plt.subplots(5, 5, figsize=(8,8))
fig.suptitle('Trochoid Curves')

# rとdを変化させてプロット
r_values = np.linspace(1, 5, 5)
d_values = np.linspace(1, 5, 5)

for i, r in enumerate(r_values):
    for j, d in enumerate(d_values):
        x = (R - r) * np.cos(t) + d * np.cos((R - r) / r * t)
        y = (R - r) * np.sin(t) - d * np.sin((R - r) / r * t)
        
        ax = axes[i, j]
        ax.plot(x, y)
        ax.set_title(f'r={r:.1f}, d={d:.1f}')
        ax.set_aspect('equal')
        ax.axis('off')

plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()
download-13
import matplotlib.pyplot as plt
import numpy as np

# パラメータ設定
R = 5  # スピログラフ用の大きな円の半径
r = 3  # スピログラフ用の小さな円の半径
d = 5  # トロコイド用の点の距離

A = 5  # リサージュ曲線の振幅
B = 4
a = 3  # リサージュ曲線のx軸方向の周波数
b = 2  # リサージュ曲線のy軸方向の周波数
delta = np.pi / 2  # リサージュ曲線の位相差

# tの範囲
t = np.linspace(0, 10 * np.pi, 1000)

# スピログラフ(トロコイド)曲線
x_spiro = (R - r) * np.cos(t) + d * np.cos((R - r) / r * t)
y_spiro = (R - r) * np.sin(t) - d * np.sin((R - r) / r * t)

# リサージュ曲線
x_lissajous = A * np.sin(a * t + delta)
y_lissajous = B * np.sin(b * t)

# 組み合わせた曲線
x_combined = x_spiro + x_lissajous
y_combined = y_spiro + y_lissajous

# 描画
plt.figure(figsize=(8, 8))
plt.plot(x_combined, y_combined, label='Combined Curve')
plt.title('Combined Curve of Spirograph, Lissajous, and Trochoid')
plt.axis('equal')
plt.axis('off')
plt.show()
download-14

import matplotlib.pyplot as plt
import numpy as np

# パラメータ設定
R = 5  # スピログラフ用の大きな円の半径
r = 3  # スピログラフ用の小さな円の半径
d = 5  # トロコイド用の点の距離

A = 5  # リサージュ曲線の振幅
B = 4
a = 3  # リサージュ曲線のx軸方向の周波数
b = 2  # リサージュ曲線のy軸方向の周波数
delta = np.pi / 2  # リサージュ曲線の位相差

n = 36 # 円周上の個数

# tの範囲
t = np.linspace(0, 10 * np.pi, 1000)

# スピログラフ(トロコイド)曲線
x_spiro = (R - r) * np.cos(t) + d * np.cos((R - r) / r * t)
y_spiro = (R - r) * np.sin(t) - d * np.sin((R - r) / r * t)

# リサージュ曲線
x_lissajous = A * np.sin(a * t + delta)
y_lissajous = B * np.sin(b * t)

# 組み合わせた曲線
x_combined = x_spiro + x_lissajous
y_combined = y_spiro + y_lissajous

# 円周上に配置するための角度
angles = np.linspace(0, 2 * np.pi, n, endpoint=False)

# 描画
plt.figure(figsize=(8, 8))

for angle in angles:
    # 回転行列を適用
    x_rotated = x_combined * np.cos(angle) - y_combined * np.sin(angle)
    y_rotated = x_combined * np.sin(angle) + y_combined * np.cos(angle)
    
    # 配置する中心を決定
    center_x = 15 * np.cos(angle)
    center_y = 15 * np.sin(angle)
    plt.plot(x_rotated + center_x, y_rotated + center_y, label=f'Angle {angle:.2f} rad')

plt.title(f'{n} Combined Curves on Circle')
plt.axis('equal')
plt.axis('off')
plt.show()
download-15




曼荼羅

同心円状や同心方形状に
円や方形の図形を配置した図形のことです
import matplotlib.pyplot as plt
import numpy as np

# 円の数とそれぞれの半径
num_circles = 12
radii = np.linspace(0.1, 5, num_circles)

# 角度の設定
angles = np.linspace(0, 2 * np.pi, 100)

# 描画
plt.figure(figsize=(8, 8))

for radius in radii:
    for angle in np.linspace(0, 2 * np.pi, num_circles, endpoint=False):
        # 各円の中心
        center_x = radius * np.cos(angle)
        center_y = radius * np.sin(angle)
        
        # 円を描画
        x = center_x + radius * np.cos(angles)
        y = center_y + radius * np.sin(angles)
        plt.plot(x, y, color='purple')

plt.title('Mandala Pattern')
plt.axis('equal')
plt.axis('off')
plt.show()
download-16


import matplotlib.pyplot as plt
import numpy as np

# 円の数とそれぞれの半径
num_layers = 36
num_circles_per_layer = 12
radii = np.linspace(0.5, 5, num_layers)

# 使用する色
colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple']

# 描画
plt.figure(figsize=(8, 8))

for i, radius in enumerate(radii):
    for angle in np.linspace(0, 2 * np.pi, num_circles_per_layer, endpoint=False):
        # 各円の中心
        center_x = radius * np.cos(angle)
        center_y = radius * np.sin(angle)
        
        # 円を描画
        angles_circle = np.linspace(0, 2 * np.pi, 100)
        x = center_x + radius * 0.3 * np.cos(angles_circle)
        y = center_y + radius * 0.3 * np.sin(angles_circle)
        plt.plot(x, y, color=colors[i % len(colors)])

plt.title('Complex Mandala Pattern')
plt.axis('equal')
plt.axis('off')
plt.show()

download-17


import matplotlib.pyplot as plt
import numpy as np

# 円の数とそれぞれの半径
num_layers = 24
num_circles_per_layer = 24
radii = np.linspace(0.5, 5, num_layers)

# 使用する色
colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple']

# 描画
plt.figure(figsize=(8, 8))

for i, radius in enumerate(radii):
    for angle in np.linspace(0, 2 * np.pi, num_circles_per_layer, endpoint=False):
        # 各円の中心
        center_x = radius * np.cos(angle)
        center_y = radius * np.sin(angle)
        
        # 円を描画
        angles_circle = np.linspace(0, 2 * np.pi, 100)
        size_variation = 0.2 + 0.1 * np.sin(angle * num_layers)  # サイズに変化をつける
        x = center_x + radius * size_variation * np.cos(angles_circle)
        y = center_y + radius * size_variation * np.sin(angles_circle)
        
        # ランダムな線幅
        # linewidth = np.random.uniform(0.5, 2.5)
        linewidth = 1
        plt.plot(x, y, color=colors[i % len(colors)], linewidth=linewidth)

plt.title('Complex Mandala Pattern with Variations')
plt.axis('equal')
plt.axis('off')
plt.show()
download-18


こんな感じで、図形を組み合わせることで
様々な紋様を描く事ができます。

複雑な図形を組み合わせれば
それなりにアートっぽく
なって行くんではないでしょうか

気になった方はコピペして
色々パラメータ変えて
試してみてください

それでは。


今回はインスタ風のQRコードを作成する
方法についてです。

解説動画はこちら



通常のQRコード

sample

普通のQRコードはこんな感じです。
四角い黒で表現され、お堅い感じです。

インスタ風は、丸みがあり少しポップな色味です。

これを再現するには通常のPythonライブラリでは
難しいようでした。



インスタ風QRコードを再現する

ということで、自作することにしました。

Google Colabで実行できるコードを作成しました。


ライブラリのインストール

実行にはqrcodeライブラリが必要なのでインストールしましょう。
pip install qrcode

QRコード作成コード
import qrcode
from PIL import Image, ImageDraw

def create_round_qr(data, file_path):
    # QRコードを生成
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_H,
        box_size=10,
        border=4,
    )
    qr.add_data(data)
    qr.make(fit=True)

    # QRコードをイメージに変換
    img = qr.make_image(fill='black', back_color='white').convert('RGB')
    pix = img.load()

    # 新しいイメージを作成
    width, height = img.size
    round_img = Image.new('RGB', (width, height), 'white')
    draw = ImageDraw.Draw(round_img)

    # 各ピクセルをチェックして形状を描画
    cell_size = 10  # box_sizeと同じにする
    pink = (255, 105, 180)
    white = (255, 255, 255)

    # 描画開始位置を計算(余白を除く)
    start_pos = cell_size * 4

    # 形状を変換
    for y in range(start_pos, height, cell_size):
        for x in range(start_pos, width, cell_size):
            if pix[x, y] == (0, 0, 0):
                draw.ellipse((x, y, x + cell_size, y + cell_size), fill=pink)

    # 隅の四角
    corner_size = cell_size * 7
    for dx, dy in [(0, 0), (width - corner_size - start_pos * 2, 0), (0, height - corner_size - start_pos * 2)]:
        x0, y0 = start_pos + dx, start_pos + dy
        x1, y1 = x0 + corner_size, y0 + corner_size
        draw.rounded_rectangle((x0, y0, x1, y1), radius=0, fill=white)
        draw.rounded_rectangle((x0, y0, x1, y1), radius=15, fill=pink)
        draw.rounded_rectangle((x0 + 1 * cell_size, y0 + 1 * cell_size, x1 - 1 * cell_size, y1 - 1 * cell_size), radius=7, fill=white)
        draw.rounded_rectangle((x0 + 2 * cell_size, y0 + 2 * cell_size, x1 - 2 * cell_size, y1 - 2 * cell_size), radius=5, fill=pink)

    # 保存
    round_img.save(file_path)

一旦QRコードを作成して画像化し
それを加工する内容になっています。

色は pink で指定していますが
変えたい場合はコードのRGB値を変えて貰えば変わります。



実際に使ってみましょう
from PIL import Image
from IPython.display import display

# QRコードを作成(URL , ファイルパス)
create_round_qr("http://www.otupy.net/", "insta.png")

# 画像を読み込む
img = Image.open("insta.png")

# 画像を表示
display(img)
download
すごくpopな形のQRコードが作成できました。

これを配布したら、オシャレですね

今回はインスタ風の
QRコードを作成するコードについてでした

色々試して遊んでみてください。

それでは。

今回は大谷選手の
ホームラン記録を可視化してみました。


解説動画はこちら



先日2024年09月20日
6打数3安打3ホームランなど
前人未到の記録を打ち立てた
ドジャース大谷選手

2004年シーズンの
今日までの成績を可視化してみました。


データの取得


可視化の元となるデータを取得します。

2024/09/21日現在
今季通算 603打数179安打 打率.297 52本塁打
122打点 125得点 52盗塁
という驚異的な成績です

この結果になるように加工していきます。

import requests
from bs4 import BeautifulSoup
import pandas as pd
import warnings
warnings.filterwarnings('ignore')

url = "https://times.abema.tv/articles/-/10018233"

res = requests.get(url)
soup = BeautifulSoup(res.content,"lxml")
table = soup.table

columns = ['日付', '対戦相手', '打順', '第1', '第2', '第3', '第4', '第5', '第6', '第7']
data = []
trs = table.find_all("tr")
for tr in trs[1:]:
    tds = tr.find_all(["td","th"])
    if "なし" in str(tds) or "中止" in str(tds):
        continue
    tmp = [td.text.replace("\xa0","").replace("\n","").replace("\r","").replace("\t","") for td in tds]
    tmp = tmp[0:3]+[t.replace("ニ","二").replace("2","二").replace("3","三") for t in tmp[3:]]
    if len(tmp)>=11:
        tmp = tmp[:10]
    data.append(tmp)

data2 = []
for d in data:
    t1,t2 = d[0:3],d[3:]
    for t in t2:
        if ""!=t:
            data2.append(t1+[t])

# データの確認
data2[0:2]
[['3月20日', 'パドレス', '2指', '遊ゴ'], ['3月20日', 'パドレス', '2指', '右安']]


これを集計しやすいようにデータフレームに加工していきます。
df = pd.DataFrame(data2,columns=["date","match","order","result"])

# 日本語の日付を変換する関数
def convert_date(date_str):
    month,day = date_str.replace("日","").split("月")
    return f"2024-{int(month):02}-{int(day):02}"

df['date'] = df['date'].apply(convert_date)
df['date'] = pd.to_datetime(df['date'])

# 本塁打
df['home_run'] = df['result'].apply(lambda x: 1 if '本' in x else 0)

# 安打
df['hit1'] = df['result'].apply(lambda x: 1 if '安' in x else 0)

# 2塁打
df['hit2'] = df['result'].apply(lambda x: 1 if x.endswith('二') else 0)

# 3塁打
df['hit3'] = df['result'].apply(lambda x: 1 if x.endswith('三') else 0)

# 指定された用語のリスト
terms = ['四球', '敬遠', '死球', '打妨', '中犠', '右犠', '左犠']

# 打数計算
df['flag'] = df['result'].apply(lambda x: 0 if x in terms else 1)

# 打席数合計
df['strokes'] = df['flag'].cumsum()

# ヒット数合計
df['hit_total'] = df[['hit1','hit2','hit3']].sum(axis=1).cumsum()

# ホームラン数合計
df['home_run_total'] = df['home_run'].cumsum()

# 打率
df['average'] = df[['hit_total','home_run_total']].sum(axis=1) / df['strokes']

# 603打数179安打 打率.297 52本塁打
df.tail()
index,date,match,order,result,home_run,hit1,hit2,hit3,flag,strokes,hit_total,home_run_total,average
688,2024-09-20 00:00:00,マーリンズ,1指,右本,1,0,0,0,1,599,125,51,0.2938230383973289
689,2024-09-21 00:00:00,ロッキーズ,1指,空振,0,0,0,0,1,600,125,51,0.29333333333333333
690,2024-09-21 00:00:00,ロッキーズ,1指,中安,0,1,0,0,1,601,126,51,0.2945091514143095
691,2024-09-21 00:00:00,ロッキーズ,1指,中本,1,0,0,0,1,602,126,52,0.2956810631229236
692,2024-09-21 00:00:00,ロッキーズ,1指,一安,0,1,0,0,1,603,127,52,0.296849087893864

これで同じ結果になりました。



Altairでプロットする

ここからこのデータを用いて
可視化を行います。

import pandas as pd
import altair as alt

# Altairで折れ線グラフを作成
chart = alt.Chart(df).mark_line().encode(
    x='date',
    y='average'
).properties(
    title='Data vs Average',
    width=800
)
chart.display()
visualization (1)



打率とホームラン数の推移を
2軸でプロットするとこうなります。
# averageの折れ線グラフ
average_line = alt.Chart(df).mark_line(color='blue').encode(
    x='date',
    y=alt.Y('average', axis=alt.Axis(title='Average'))
)

# home_run_totalの折れ線グラフ
home_run_line = alt.Chart(df).mark_line(color='red').encode(
    x='date',
    y=alt.Y('home_run_total', axis=alt.Axis(title='Home Run Total'))
)

# 二重軸のグラフを作成
chart = alt.layer(
    average_line,
    home_run_line
).resolve_scale(
    y='independent'
).properties(
    title='Data vs Average and Home Run Total',
    width=800
)
chart.display()
visualization


途中から急にホームラン数のピッチが上がっているようです。

ここからどれだけ伸びるのか
残り試合のホームラン数の予測もしてみます。


ホームラン数を時系列予測

残り試合は8試合です。
ARIMAという時系列予測モデルを用いて
予測をしてきます。

先ほどのデータフレームを加工して
予測データを作っていきます。

import pandas as pd
from statsmodels.tsa.arima.model import ARIMA
import matplotlib.pyplot as plt

# 日付をインデックスに設定
df2 = df.copy()
df2 = df.groupby('date').max()

# 欠損日を埋めて連続した日付にする
df2 = df2.asfreq('D')

# ARIMAモデルのフィッティング
model = ARIMA(df2['home_run_total'], order=(1, 1, 1))
model_fit = model.fit()

# 8日先までの予測
forecast = model_fit.forecast(steps=8)

# 結果のプロット
plt.figure(figsize=(12, 4))
plt.plot(df2.index, df2['home_run_total'], label='Actual')
plt.plot(pd.date_range(df2.index[-1] + pd.Timedelta(days=1), periods=8, freq='D'), forecast, label='Forecast', color='red')
plt.legend()
plt.show()
download

実測と予測を組み合わせて可視化しました。

残り8試合だとすると
およそ55本くらいまでは伸びそうです。

予測が外れて60本塁打くらいまで
行って欲しいですね!!!!

今回は大谷選手の打席結果を用いた
ホームラン成績の可視化でした

それでは。

今回はレイトン教授シリーズに出てきた
パズルゲームを解くプログラムについてです。


解説動画はこちら


 



レイトン教授シリーズとは

レベルファイブから発売された
ニンテンドーDSのアドベンチャーゲームシリーズ

謎解きと称した
たくさんの小ゲームで構成されており
これを解いてクリアしていく

パズルゲームなどもかなりたくさん有る

今回はその中でも代表的な
スライドパズル系のパズルを解くものです。


1.ナイトツアー

チェスのナイトの駒で旅をしよう

ナイトは将棋の桂馬のように、2つ先のマス目の左右どちらかに移動できる
ただし、上下左右、どちらの方向にも移動可能である
ただし、1度訪れたマス目には2度と入ることができない

さあ、すべてのマス目を旅してみよう(8aがスタート)
knight-movement


こういったパズルを特には
次のような考え方が重要になってきます。

・探索の深さと幅:

バックトラッキング: 深さ優先探索(DFS)の一種で
 ある経路を可能な限り深く探索する

幅優先探索(BFS): 各レベルのノードを順に探索し
 より広く浅く探索する

・データ構造:

バックトラッキング: 再帰呼び出しやスタックを使って探索を進める

幅優先探索(BFS): キューを使って探索を進める

これらを用いて解くコードを
考えてみました。

Google colabで実行できるようになっています。
日本語表示ように先にこれをインストールしておいてください。
pip install japanize_matplotlib


ナイトツアーを解くコード

バックトラッキングを用いて再帰で探索を行うコードです

colab上ではwidgetsでスライダーを用いて
画像を変更できるようにしています。

import matplotlib.pyplot as plt
import numpy as np
import ipywidgets as widgets
from IPython.display import display

# ナイトの移動可能な8つの方向
dx = [2, 1, -1, -2, -2, -1, 1, 2]
dy = [1, 2, 2, 1, -1, -2, -2, -1]

def is_valid_move(x, y, board):
    return 0 <= x < N and 0 <= y < N and board[x][y] == -1

def solveKnightTour(x, y, move_count, board):
    if move_count == N * N:
        return True
    for i in range(8):
        next_x = x + dx[i]
        next_y = y + dy[i]
        if is_valid_move(next_x, next_y, board):
            board[next_x][next_y] = move_count
            if solveKnightTour(next_x, next_y, move_count + 1, board):
                return True
            board[next_x][next_y] = -1
    return False

def knightTour(board):
    if solveKnightTour(0, 0, 1, board):
        return board
    else:
        print("解が見つかりませんでした")
        return None

# ステップごとのボード状態を画像として保存
def make_img(N):
    img_data = []
    for step in range(N * N):
        fig, ax = plt.subplots(figsize=(8, 8))
        ax.set_xticks(np.arange(N+1)-0.5, minor=True)
        ax.set_yticks(np.arange(N+1)-0.5, minor=True)
        ax.grid(which="minor", color="black", linestyle='-', linewidth=2)
        ax.set_xticks(np.arange(N), minor=False)
        ax.set_yticks(np.arange(N), minor=False)
        ax.grid(which="major", color="black", linestyle='-', linewidth=0)
        
        for i in range(N):
            for j in range(N):
                if board[i][j] <= step:
                    color = 'pink' if board[i][j] == step else ('bisque' if (i + j) % 2 == 0 else 'saddlebrown')
                    ax.text(j, i, str(board[i][j]), ha='center', va='center', fontsize=12)
                    ax.add_patch(plt.Rectangle((j-0.5, i-0.5), 1, 1, fill=True, color=color))
        
        ax.set_xlim(-0.5, N-0.5)
        ax.set_ylim(N-0.5, -0.5)
        ax.set_xticklabels([])
        ax.set_yticklabels([])
        
        fig.canvas.draw()
        img_data.append(np.frombuffer(fig.canvas.buffer_rgba(), dtype=np.uint8).reshape(fig.canvas.get_width_height()[::-1] + (4,)))
        plt.close(fig)
    return img_data

N = 8
board = [[-1 for _ in range(N)] for _ in range(N)]
board[0][0] = 0 # スタート位置

# 解読スタート
board = knightTour(board)
img_data = make_img(N)

# スライダーウィジェットの作成
step_selector = widgets.IntSlider(min=0, max=N*N-1, step=1, description='Step:')

def update_board(step):
    plt.figure(figsize=(8, 8))
    plt.imshow(img_data[step])
    plt.axis('off')
    plt.show()

# インタラクティブなプロットの作成と表示
interactive_plot = widgets.interactive(update_board, step=step_selector)
display(interactive_plot)
スクリーンショット 2024-09-07 17.03.31





2.箱入り娘

じゃまするブロックをスライドさせて
赤い大きなブロックを右側の出口から出してほしい

これは少し実問題と内容が違います。
下記のような構造のパズルを解くものです。

pazzru1

同じ色のブロックはスライドできます。
互いに重ね合わせは出来ず、出口までスライドさせます。
これをデータに定義して解くコードになります。

ここでは
幅優先探索(BFS): キューを使って探索を進める
を用いて探索を行います。

キュー : データを先入れ先出しのリスト構造で保持するもの

from collections import deque
import copy
import japanize_matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import io
from IPython.display import display
import ipywidgets as widgets
from PIL import Image

# ボードのサイズを定義
BOARD_WIDTH = 4
BOARD_HEIGHT = 5

# 駒の初期配置を定義
INITIAL_STATE = [
    ['父', '娘', '娘', '母'],
    ['父', '娘', '娘', '母'],
    ['下', '番', '番', '小'],
    ['下', '中', '', ''],
    ['', '', '', '']
]

# 駒のサイズを定義
PIECE_SIZES = {
    '娘': (2, 2),
    '父': (1, 2),
    '母': (1, 2),
    '番': (2, 1),
    '小': (1, 1),
    '中': (1, 1),
    '下': (2, 1)
}

# 駒の色を定義
PIECE_COLORS = {
    '娘': 'red',
    '父': 'blue',
    '母': 'green',
    '番': 'purple',
    '小': 'orange',
    '中': 'gray',
    '下': 'brown'
}

DIRECTIONS = {
    'up': (-1, 0),
    'down': (1, 0),
    'left': (0, -1),
    'right': (0, 1)
}

### 画像を描画

def draw_board(state):
    fig, ax = plt.subplots()
    ax.set_xlim(0, BOARD_WIDTH)
    ax.set_ylim(0, BOARD_HEIGHT)
    ax.set_xticks([])
    ax.set_yticks([])
    ax.set_aspect('equal')

    for x in range(BOARD_HEIGHT):
        for y in range(BOARD_WIDTH):
            piece = state[x][y]
            if piece:
                width, height = 1, 1
                color = PIECE_COLORS.get(piece, 'gray')
                rect = patches.Rectangle((y, BOARD_HEIGHT - x - 1), width, height, linewidth=1, edgecolor='black', facecolor=color)
                ax.add_patch(rect)
                cx, cy = y + width / 2, BOARD_HEIGHT - x - 1 + height / 2
                ax.text(cx, cy, piece, color='white', ha='center', va='center', fontsize=12, fontweight='bold')

    # 画像をバイトデータとして保存
    buf = io.BytesIO()
    plt.savefig(buf, format='png')
    buf.seek(0)
    plt.close(fig)
    return buf

def generate_images(solution):
    images = []
    for state in solution:
        buf = draw_board(state)
        image = Image.open(buf)
        images.append(image)
    return images

### パズルを解く

def is_goal(state):
    """ゴール状態のチェック"""
    # 例として、特定の位置に特定の駒があるかをチェック
    return state[4][1] == '娘' and state[4][2] == '娘'

def can_move(state, piece, direction):
    """駒が指定の方向に動けるかをチェック"""
    dx, dy = DIRECTIONS[direction]
    for i in range(BOARD_HEIGHT):
        for j in range(BOARD_WIDTH):
            if state[i][j] == piece:
                ni, nj = i + dx, j + dy
                if ni < 0 or ni >= BOARD_HEIGHT or nj < 0 or nj >= BOARD_WIDTH or (state[ni][nj] != '' and state[ni][nj] != piece):
                    return False
    return True

def move_piece(state, piece, direction):
    """駒を指定の方向に動かす"""
    dx, dy = DIRECTIONS[direction]
    new_state = copy.deepcopy(state)
    positions = [(i, j) for i in range(BOARD_HEIGHT) for j in range(BOARD_WIDTH) if state[i][j] == piece]
    for i, j in positions:
        new_state[i][j] = ''
    for i, j in positions:
        new_state[i + dx][j + dy] = piece
    return new_state

def solve(initial_state):
    """幅優先探索でパズルを解く"""
    queue = deque([(initial_state, [])])
    visited = set()
    visited.add(tuple(tuple(row) for row in initial_state))

    while queue:
        current_state, path = queue.popleft()

        if is_goal(current_state):
            return path + [current_state]

        for piece in PIECE_SIZES.keys():
            for direction in DIRECTIONS.keys():
                if can_move(current_state, piece, direction):
                    new_state = move_piece(current_state, piece, direction)
                    state_tuple = tuple(tuple(row) for row in new_state)
                    if state_tuple not in visited:
                        visited.add(state_tuple)
                        queue.append((new_state, path + [current_state]))

    return None

# パズルを解く
solution_path = solve(INITIAL_STATE)

# solution_pathから各ステップの状態を取得
if solution_path:
    solution = solution_path
else:
    solution = []

# 画像データを生成
images = generate_images(solution)

# 画像を表示する関数
def show_image(index):
    display(images[index])

# スライダーウィジェットの作成
slider = widgets.IntSlider(min=0, max=len(images)-1, step=1, description='Step:')
output = widgets.Output()
def on_slider_change(change):
    with output:
        output.clear_output(wait=True)
        show_image(change['new'])

slider.observe(on_slider_change, names='value')

# ウィジェットの表示
with output:
    show_image(0)
display(slider, output)
スクリーンショット 2024-09-07 17.03.11





3.玉を出せ

赤い玉を出口(一番下のマス目)まで持っていけるかな?
じゃまなブロックをうまく動かして道を作ろう。
(灰色のブロックは動かせない)

pazzru2


箱入り娘と違って動かせないブロックが
存在する部分の違いがあります。
これも先ほどと同様、幅優先探索(BFS)で解きます。
from collections import deque
import copy
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import io
from IPython.display import display
import ipywidgets as widgets
from PIL import Image

# ボードのサイズ
BOARD_WIDTH = 6
BOARD_HEIGHT = 6

# 駒の初期配置
INITIAL_STATE = [
    ['0', '0', '10', '0', '0', '0'],
    ['', '1', '1', '7', '5', ''],
    ['2', '2', '9', '9', '5', '0'],
    ['0', '6', '9', '9', '3', '3'],
    ['', '6', '8', '4', '4', ''],
    ['0', '0', '0', '', '0', '0']
]

# 駒のサイズ
PIECE_SIZES = {
    '1': (2, 1),
    '2': (2, 1),
    '3': (2, 1),
    '4': (2, 1),
    '5': (1, 2),
    '6': (1, 2),
    '7': (1, 1),
    '8': (1, 1),
    '9': (2, 2),
    '10': (1, 1),
}

DIRECTIONS = {
    'up': (-1, 0),
    'down': (1, 0),
    'left': (0, -1),
    'right': (0, 1)
}

# 駒の色を定義
PIECE_COLORS = {
    "0": 'gray',
    "1": 'green',
    "2": 'blue',
    "3": 'green',
    "4": 'blue',
    "5": 'magenta',
    "6": 'magenta',
    "7": 'purple',
    "8": 'purple',
    "9": 'yellow',
    "10": 'red'
}

### 画像を描画

def draw_board(state):
    fig, ax = plt.subplots()
    ax.set_xlim(0, BOARD_WIDTH)
    ax.set_ylim(0, BOARD_HEIGHT)
    ax.set_xticks([])
    ax.set_yticks([])
    ax.set_aspect('equal')

    for x in range(BOARD_HEIGHT):
        for y in range(BOARD_WIDTH):
            piece = state[x][y]
            if piece:
                width, height = 1, 1
                color = PIECE_COLORS.get(piece, 'gray')
                rect = patches.Rectangle((y, BOARD_HEIGHT - x - 1), width, height, linewidth=1, edgecolor='black', facecolor=color)
                ax.add_patch(rect)
                cx, cy = y + width / 2, BOARD_HEIGHT - x - 1 + height / 2
                ax.text(cx, cy, piece, color='white', ha='center', va='center', fontsize=12, fontweight='bold')

    # 画像をバイトデータとして保存
    buf = io.BytesIO()
    plt.savefig(buf, format='png')
    buf.seek(0)
    plt.close(fig)
    return buf

def generate_images(solution):
    images = []
    for state in solution:
        buf = draw_board(state)
        image = Image.open(buf)
        images.append(image)
    return images

def is_goal(state):
    """ゴール状態のチェック"""
    return state[5][3] == '10'

def can_move(state, piece, direction):
    """駒が指定の方向に動けるかをチェック"""
    dx, dy = DIRECTIONS[direction]
    for i in range(BOARD_HEIGHT):
        for j in range(BOARD_WIDTH):
            if state[i][j] == piece:
                ni, nj = i + dx, j + dy
                if ni < 0 or ni >= BOARD_HEIGHT or nj < 0 or nj >= BOARD_WIDTH or (state[ni][nj] != '' and state[ni][nj] != piece):
                    return False
    return True

def move_piece(state, piece, direction):
    """駒を指定の方向に動かす"""
    dx, dy = DIRECTIONS[direction]
    new_state = copy.deepcopy(state)
    positions = [(i, j) for i in range(BOARD_HEIGHT) for j in range(BOARD_WIDTH) if state[i][j] == piece]
    for i, j in positions:
        new_state[i][j] = ''
    for i, j in positions:
        new_state[i + dx][j + dy] = piece
    return new_state

def solve(initial_state):
    """幅優先探索でパズルを解く"""
    queue = deque([(initial_state, [])])
    visited = set()
    visited.add(tuple(tuple(row) for row in initial_state))

    while queue:
        current_state, path = queue.popleft()

        if is_goal(current_state):
            return path + [current_state]

        for piece in PIECE_SIZES.keys():
            if piece == '0':
                continue
            for direction in DIRECTIONS.keys():
                if can_move(current_state, piece, direction):
                    new_state = move_piece(current_state, piece, direction)
                    state_tuple = tuple(tuple(row) for row in new_state)
                    if state_tuple not in visited:
                        visited.add(state_tuple)
                        queue.append((new_state, path + [current_state]))

    return None

# パズルを解く
solution_path = solve(INITIAL_STATE)

# solution_pathから各ステップの状態を取得
if solution_path:
    solution = solution_path
else:
    solution = []

#画像データを生成する関数を再利用して描画(この関数はユーザーが提供するものと仮定)
images = generate_images(solution)

#画像を表示する関数(この関数はユーザーが提供するものと仮定)
def show_image(index):
   display(images[index])

#スライダーウィジェットの作成
slider = widgets.IntSlider(min=0, max=len(images)-1, step=1, description='Step:')
output = widgets.Output()

def on_slider_change(change):
   with output:
       output.clear_output(wait=True)
       show_image(change['new'])

slider.observe(on_slider_change, names='value')

#初期表示
with output:
   show_image(0)

#ウィジェットの表示
display(slider, output)
スクリーンショット 2024-09-07 17.02.56


動画内では1枚ずつ解く過程が見られるので
答えを見たい方は最後のおまけを見てみてください。


おわりに

レイトン教授の最後の方のパズルはかなり難しく
人力ではなかなか解けずに、諦めた方も多いのではないでしょうか

そんなパズルもPythonプログラムであれば
解く事ができるコードも作成できるので
パズルゲームに困ったら
プログラミングの力を借りると良いかもしれません。

それでは。



このページのトップヘ