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

Python

今回は重力レンズエフェクトで
画像を歪ませで遊んでみました。


解説動画はこちら


 

重力レンズとは

光が天体の重力によって曲げられ
天体があたかもレンズとして働く効果のことです

光の曲がり具合はレンズとなる天体の質量が大きいほど強く
背後の銀河から来る光が強く曲げられて像が大きく歪んで見えます


今回はレンズをブラックホールで想定して
擬似的な効果の計算で画像を作成するコードのご紹介です。


基本の計算式

レンズ中心からの距離が小さくなるほど
(レンズに近づくほど)歪みが強くなるような計算です

元々の重力レンズの計算は少し難解かなと思いましたので
簡易な計算でエフェクトをかけます。

スクリーンショット 2024-11-23 17.08.32




処理の流れ

入力画像をNumPy配列に変換し、全ピクセルの座標を計算
重力レンズ効果を適用する領域を特定

レンズ中心からの距離に基づいて歪みを計算し、新しいピクセル座標を算出
新しい座標に基づいてピクセルを再配置し、ブラックホール領域を黒で塗りつぶし
最終的な画像として出力


ipywidgets でUIを作って操作できます。
スライダーの値を変えると、画像が変化します。

Google colabなどで試す事ができます。


コードはこちら
from PIL import Image, ImageDraw
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, IntSlider, FloatSlider

cached_image = None

# 重力レンズエフェクトをかけた画像を生成する
def apply_gravitational_lens(image, lens_radius, black_hole_radius, distortion_factor):
    
    # 画像をNumPy配列に変換
    input_pixels = np.array(image)
    width, height = image.size
    cx, cy = width // 2, height // 2
    
    # 座標グリッドの生成と中心からの相対座標,距離の計算
    y, x = np.meshgrid(np.arange(height), np.arange(width), indexing="ij")
    dx, dy = x - cx, y - cy
    sx, sy = x.copy(),y.copy()
    distance = np.sqrt(dx**2 + dy**2)

    # 歪みを適用 { distortion = 1+distortion_factor * ((lens_radius - distance[lens_mask]) / lens_radius)**2 }
    lens_mask = (black_hole_radius < distance) & (distance < lens_radius)
    distortion = 1 + distortion_factor * ((lens_radius - distance[lens_mask]) / lens_radius)**2
    sx[lens_mask] = cx + dx[lens_mask] * distortion
    sy[lens_mask] = cy + dy[lens_mask] * distortion

    # 座標のクリッピングと出力画像の生成
    sx = np.clip(sx, 0, width - 1).astype(int)
    sy = np.clip(sy, 0, height - 1).astype(int)
    output_pixels = input_pixels[sy, sx]

    # ブラックホール領域の適用
    black_hole_mask = distance <= black_hole_radius
    output_pixels[black_hole_mask] = [0, 0, 0]
    return Image.fromarray(output_pixels)

# インタラクティブUI部
def interactive_gravitational_lens(image_path):

    global cached_image
    if cached_image is None:
        cached_image = Image.open(image_path).convert("RGB") 

    def update(lens_radius, black_hole_radius, distortion_factor):
        output_image = apply_gravitational_lens(
            cached_image,
            lens_radius=lens_radius,
            black_hole_radius=black_hole_radius,
            distortion_factor=distortion_factor,
        )
        plt.figure(figsize=(8, 8))
        plt.imshow(output_image)
        plt.axis('off')
        plt.title(f"Lens Radius: {lens_radius:03}, Black Hole Radius: {black_hole_radius:03}, Distortion: {distortion_factor:.02}")
        plt.show()

    interact(
        update,
        lens_radius=IntSlider(min=50, max=990, step=10, value=50, description="Lens Radius"),
        black_hole_radius=IntSlider(min=10, max=200, step=5, value=10, description="Black Hole Radius"),
        distortion_factor=FloatSlider(min=0.1, max=10.0, step=0.1, value=0.1, description="Distortion Factor"),
    )

interactive_gravitational_lens("画像パス")

download-1

レンズの半径と
歪み具合を調整して
面白画像を作って遊んでみてください。

動画では色々な画像で試しているので
ぜひみてみてくださいね

今回はこれまでです
それでは。




今回は円周率を計算するアルゴリズムを使って
馬鹿でかい円周率を行けるだけ求めてみました

解説動画はこちら





まず最初に問題ですが
円周率の小数点10000桁目の数値は
何になるでしょうか?

こんな問題を解く場合に
馬鹿でかい円周率を求める方法があります。



ガウス=ルジャンドルのアルゴリズム
(Gauss–Legendre algorithm)


円周率を計算する際に用いられる
数学の反復計算アルゴリズム(算術幾何平均法)

円周率計算のアルゴリズムの中でも
トップクラスの収束速度なので、精度が良いらしいです。

アルゴリズムは

スクリーンショット 2024-11-09 16.46.34

となります。


円周率を求める関数のコード

計算精度を設定して、その桁数までを求めていく感じになります。
例えば円周率の小数点100桁まで求めたかったら
計算精度を110-150 とかに設定します。

また反復計算の回数は
log2(x) 以上の回数が必要になるので
計算精度を上げる場合は、反復計算回数も
あわせて上げてください(ここでは20回にしてます)

from decimal import Decimal, getcontext

# 計算精度の設定(小数点以下1万桁以上の精度を確保)
getcontext().prec = 15000

# ガウス=ルジャンドルのアルゴリズムの実装
def gauss_legendre_pi():
    # 初期値設定
    a = Decimal(1)
    b = Decimal(1) / Decimal(2).sqrt()
    t = Decimal(1) / Decimal(4)
    p = Decimal(1)

    # 十分な収束回数で反復計算(math.ceil(math.log2(x))以上)
    for _ in range(20):
        a_next = (a + b) / 2
        b = (a * b).sqrt()
        t -= p * (a - a_next) ** 2
        a = a_next
        p *= 2

    pi_approx = ((a + b) ** 2) / (4 * t)
    return pi_approx

# 円周率の計算
pi_approx = gauss_legendre_pi()
pi_str = str(pi_approx)

# 1万桁目の数値を取得
k = 10000

# 結果を出力
print(f"円周率の小数点以下{k}桁目の数値: {pi_str[k+1]}")
9




まとめ

このPythonコードであれば小数点10万桁くらいまでは余裕です。

それ以上になるとハイスペックな計算資源が必要で
加えてもっと効率の良い高精度計算専用のライブラリを用いたり
並列分散処理などを実装する必要が出てくるかもしれません。

ちなみにこのアルゴリズムでは
2009年に筑波大学の高橋大介という方が
2兆5769億8037万桁
という記録を出しているので
そこから15年以上経った今なら
もっと馬鹿でかい桁まで計算が出来るかもしれません!!

デカいって、いいですよね
男のロマン

どなたか馬鹿でかい円周率の計算
挑戦してみませんか

今回はここまでです
それでは。

 

今回はあの有名な
クイズ番組の問題のシミュレーションです。


解説はこちら


 
モンティホール問題

3つのドアの中に景品の車が一つ有り
プレイヤーは1つのドアを選択できる
3door

プレイヤーがドアを選択した後、司会者モンティーは
そのドア以外のハズレのドアを開く
monty2

最後にプレイヤーはドアを変えるか
そのままかを選ぶ事が出来る


プレーヤーが1つのドアを選択したあと
外れのドアが1つ開放される

残り2枚の当たりの確率は直感的には
それぞれ 50% になるように思えるが
はたしてそれは正しいだろうか...





解説


開けるドアを
変える場合 と 変えない場合
勝率がどうなるのかを計測します。


シミュレーションコードはこちら
import random

def monty_hall_simulation(num_doors=3, num_trials=10000):
    switch_wins,stay_wins = 0,0

    for _ in range(num_trials):
        # 車を隠すドアをランダムに選ぶ
        doors = ['goat'] * (num_doors - 1) + ['car']
        random.shuffle(doors)
        car_index = doors.index('car')

        # プレイヤーが最初に選ぶドアをランダムに選ぶ
        player_choice = random.randint(0, num_doors - 1)

        # モンティが開けるドアを決定する(車とプレイヤーの選択以外)
        monty_choices = [i for i in range(num_doors) if i != player_choice and i != car_index]
        monty_opened = random.sample(monty_choices, num_doors-2)

        # プレイヤーがドアを変更する場合の勝敗
        switch_choice = next(i for i in range(num_doors) if i != player_choice and i not in monty_opened)
        if doors[switch_choice] == "car":
            switch_wins += 1

        # プレイヤーがドアを変更しない場合の勝敗
        if doors[player_choice] == "car":
            stay_wins += 1

    print(f"スイッチした場合の勝率: {switch_wins / num_trials * 100:.4f}%")
    print(f"スイッチしなかった場合の勝率: {stay_wins / num_trials * 100:.4f}%")

# シミュレーション実行
num_doors = 3
num_trials = 10000
monty_hall_simulation(num_doors=num_doors, num_trials=num_trials)
スイッチした場合の勝率: 67.1900%
スイッチしなかった場合の勝率: 32.8100%


この場合
開けるドアを変えた場合は 3分の2
変えない場合は3分の1
の確率で景品を得る事ができます。

つまり、開くドアを変えた方がお得で
約67%の確率で当たりを引けます。

残るのが2個のドアだから50%の確率というのは
まやかしでした。

ちなみにドアの数が増えれば増えるほど
ドアを変えた方が勝率は高くなります。

こんな感じで、直感的に迷う問題でも
プログラムであればシミュレーション出来るので
正しい答えを導き出せる手助けになります。

色々試してみてください
それでは。



こんかいは人工衛星にまつわる
プログラミングのお話です

解説動画はこちら


 


問題

地球から約20,200 kmの高度の中軌道(MEO: Medium Earth Orbit)を
約3.9 km/秒(時速約14,000 km)で周回するGPS衛星は
1日あたりどれくらいの時間のズレが生じているでしょうか?




解説

GPS衛星では、次の2つの要因によって時間のずれが発生します
1.特殊相対性理論(速さによる時間の遅れ)
2.一般相対性理論(重力による時間の進み方の違い)

この2つのズレを計算する事で
時間のズレを求める事ができます。


相対性理論のざっくりな解説は省きます
(それでも知りたい方は動画の方を見てくださいね)


おおよそ、2つの理論から
導き出される時間のズレを求める計算方法は
以下のようになります。

スクリーンショット 2024-10-26 16.27.14

あとはこれをコードで計算してあげれば良いですね。






時間のズレを計算するコード

うまく2つの数式を定義して
計算してあげると答えが出ます。

重力定数などは
scipyで定数が有るので、それを使うと便利です。

import math
from scipy import constants

# 定数の定義
G = constants.G # 重力定数 6.67430e-11
c = constants.c # 光速 (m/s) 299792458
Earth = 5.9722 * (10**24) # 地球の質量 (kg) 5.9722e24 
R_earth = 6.371e6  # 地球の半径 (m)

# パラメータ
altitude = 20200e3  # GPS衛星の軌道高度 (約20,200 km)
satellite_speed = 3900  # 衛星の速度 (m/s)

# 衛星の軌道半径
r_satellite = R_earth + altitude

# 一般相対性理論による時間の進みの近似計算(重力ポテンシャル差)
gravitational_time_dilation = G * Earth * (1 / R_earth - 1 / r_satellite) / c**2

# 特殊相対性理論による時間の遅れの近似計算(速度)
velocity_time_dilation = satellite_speed ** 2 / (2 * c**2)

# 合計の時間のズレ(地上から見た衛星の時計のズレ、秒単位)
total_time_dilation = gravitational_time_dilation - velocity_time_dilation
total_time_dilation_per_day = total_time_dilation * 86400  # 1日あたりのズレ

print("1日あたりの時間のズレ(秒):", f'{total_time_dilation_per_day:.30f}')
print("1日あたりの時間のズレ(マイクロ秒):", f'{total_time_dilation_per_day*1000000:.30f}')
1日あたりの時間のズレ(秒): 0.000038413514669274767825320194
1日あたりの時間のズレ(マイクロ秒): 38.413514669274768209561443654820




まとめ

GPS衛星の時計は地上の時計に対して
1日あたり約38マイクロ秒進みます。

衛星の時刻が1マイクロ秒(1秒の100万分の1)ずれると
約300mの誤差が発生するらしいです・・・


人工衛星プログラミングでは
この辺りも加味してやらないといけないですね。

将来的に宇宙案件とか出てきた場合に
こういた知識が役に立つかもしれませんね

今回は人工衛星に関わる
プログラミングのお話でした。

それでは。


今回はSNSで話題になっていた
直感に反する数学の問題を解く
プログラムに関してです。



解説動画はこちら 



今回の問題は、とある数学者の考案した
「一見シンプルだが直感に反する確率パズル」
というものです。


問題

100個のボールが入ったつぼがあります。
そのうちn個が赤で、100-n個が緑です。
ただし、nは0~100の間で一様分布しています。

つぼからボールをランダムに1つ取り出したところ、赤でした。
それを捨ててから、残り99個からボールを選ぶとき
次に引くボールの色は赤と緑どちらが多いでしょうか?

1.赤
2.緑
3.赤と緑が同じ



さてこの問題を考えてみてください。





解説

こちらの詳細な解説は
ながーいので、割愛します。

動画の中では 3:53 くらいから解説しています。



シミュレーションコード

こちらの確率をもとめるコードは
こんな感じになります。

1.ランダムに赤いボールの個数を決める
2.赤を引いたら、次のボールを引く
3.二回目のボール、赤と緑どちらを引いたか集計する

import numpy as np

# シミュレーションの設定
num_trials = 100000  # 試行回数
n_balls = 100  # ボールの総数
data = []

# 結果を記録するための変数
red_first_count = 0
red_second_count = 0
green_second_count = 0

# シミュレーション
for _ in range(num_trials):
    n_red = np.random.randint(0, n_balls + 1)  # 0から100までのランダムな赤いボールの数
    n_green = n_balls - n_red  # 緑のボールの数

    # 最初のボールをランダムに引く(赤または緑)
    first_draw = np.random.choice(['red'] * n_red + ['green'] * n_green)

    if first_draw == 'red':
        # 最初に赤を引いたので、赤を1つ減らす
        red_first_count += 1
        n_red -= 1

        # 次のボールを引く
        second_draw = np.random.choice(['red'] * n_red + ['green'] * n_green)

        # 次のボールの数を集計
        if second_draw == 'red':
            red_second_count += 1
        else:
            green_second_count += 1
        tmp = [red_first_count,red_second_count,red_second_count/red_first_count,n_red]
        data.append(tmp)

# 結果の計算
total_first_red = red_first_count
total_second_red = red_second_count

# 確率の計算
if total_first_red > 0:
    p_second_red_given_first_red = total_second_red / total_first_red
    print(f"P(second red | first red) = {p_second_red_given_first_red:.4f}")
else:
    print("No trials with the first ball red.")



google colab などで試してみてください。

ベイズの定理などを用いて
机上でかなり大変な計算をしないと
求めることは困難ですが

プログラムであれば
簡単にシミュレーションして
擬似値をもとめる事ができます。

ぜひ色々試してみてください。

それでは。


このページのトップヘ