Pythonを用いた回転行列（２）

# ３Ｄ空間における座標変換例
**任意の位置(ｘ,ｙ,ｚ)の点をＸ軸・Ｙ軸・Ｚ軸まわりの
入力した任意の角度で回転するPythonプログラム**

matplotlibパッケージを利用して３Ｄグラフを描画すると，
くるくるとマウスドラッグでビューを回転できるので，
空間上でどのように移動（回転）したのか分かりやすい．

下図は，画面のスクリーンキャブチャなので，ビューの回転はできませんが，
自身のPCでPythonプログラムを実行すると，プロットを回転させることができます．

![スクリーンショット 2025-05-31 11.26.02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1979979/70226f32-9f7d-484c-8f5b-fa1d10d51694.jpeg)

```rot_matrix_input.py
import numpy as np
import matplotlib.pyplot as plt
import tkinter as tk
from tkinter import simpledialog, messagebox

def rotate_x(point, angle_deg):
    """X軸周りの回転行列を適用"""
    angle_rad = np.radians(angle_deg)
    rotation_matrix = np.array([
        [1, 0, 0],
        [0, np.cos(angle_rad), -np.sin(angle_rad)],
        [0, np.sin(angle_rad), np.cos(angle_rad)]
    ])
    return np.dot(rotation_matrix, point)

def rotate_y(point, angle_deg):
    """Y軸周りの回転行列を適用"""
    angle_rad = np.radians(angle_deg)
    rotation_matrix = np.array([
        [np.cos(angle_rad), 0, np.sin(angle_rad)],
        [0, 1, 0],
        [-np.sin(angle_rad), 0, np.cos(angle_rad)]
    ])
    return np.dot(rotation_matrix, point)

def rotate_z(point, angle_deg):
    """Z軸周りの回転行列を適用"""
    angle_rad = np.radians(angle_deg)
    rotation_matrix = np.array([
        [np.cos(angle_rad), -np.sin(angle_rad), 0],
        [np.sin(angle_rad), np.cos(angle_rad), 0],
        [0, 0, 1]
    ])
    return np.dot(rotation_matrix, point)

def get_user_inputs():
    """Tkinterのダイアログボックスを使ってユーザーから座標値と各軸の回転角度を取得する関数"""
    root = tk.Tk()
    root.withdraw() # メインウィンドウを表示しない
    
    # 座標の入力
    while True:
        try:
            x_str = simpledialog.askstring("入力", "元の点の X 座標を入力してください:")
            if x_str is None: return None, None, None, None
            x = float(x_str)

            y_str = simpledialog.askstring("入力", "元の点の Y 座標を入力してください:")
            if y_str is None: return None, None, None, None
            y = float(y_str)

            z_str = simpledialog.askstring("入力", "元の点の Z 座標を入力してください:")
            if z_str is None: return None, None, None, None
            z = float(z_str)
            
            original_point = np.array([x, y, z])
            break
        except ValueError:
            messagebox.showerror("エラー", "無効な入力です。数値を入力してください。")
        except TypeError:
            messagebox.showerror("エラー", "入力がキャンセルされました。")
            return None, None, None, None
            
    # 各軸の回転角度の入力
    angles = {}
    axes = ['X', 'Y', 'Z']
    for axis in axes:
        while True:
            try:
                angle_str = simpledialog.askstring("入力", f"{axis}軸周りの回転角度（度数法）を入力してください:")
                if angle_str is None: return None, None, None, None
                angles[axis] = float(angle_str)
                break
            except ValueError:
                messagebox.showerror("エラー", "無効な入力です。数値を入力してください。")
            except TypeError:
                messagebox.showerror("エラー", "入力がキャンセルされました。")
                return None, None, None, None

    return original_point, angles['X'], angles['Y'], angles['Z']

## メイン処理

if __name__ == "__main__":
    original_point, angle_x, angle_y, angle_z = get_user_inputs()

    if original_point is None or angle_x is None or angle_y is None or angle_z is None:
        print("ユーザーによって入力がキャンセルされました。プログラムを終了します。")
    else:
        # 順番に回転を適用
        # X軸周りの回転
        current_point = rotate_x(original_point, angle_x)
        # Y軸周りの回転
        current_point = rotate_y(current_point, angle_y)
        # Z軸周りの回転
        rotated_point = rotate_z(current_point, angle_z)

        print(f"元の点: {original_point}")
        print(f"X軸周り {angle_x}度, Y軸周り {angle_y}度, Z軸周り {angle_z}度 回転後の点: {rotated_point}")

        # --- 3Dプロットの生成 ---
        fig = plt.figure(figsize=(8, 8))
        ax = fig.add_subplot(111, projection='3d')

        ax.set_xlabel('X')
        ax.set_ylabel('Y')
        ax.set_zlabel('Z')

        # 軸の範囲を動的に設定
        min_coord = np.min([original_point, rotated_point], axis=0) - 0.5
        max_coord = np.max([original_point, rotated_point], axis=0) + 0.5
        ax.set_xlim([min_coord[0], max_coord[0]])
        ax.set_ylim([min_coord[1], max_coord[1]])
        ax.set_zlim([min_coord[2], max_coord[2]])

        # 元の点をプロット
        ax.plot(
            [original_point[0]], [original_point[1]], [original_point[2]], 
            'ro', markersize=8, label=f'Original Point ({original_point[0]:.2f}, {original_point[1]:.2f}, {original_point[2]:.2f})'
        )
        # 回転後の点をプロット
        ax.plot(
            [rotated_point[0]], [rotated_point[1]], [rotated_point[2]], 
            'bo', markersize=8, label=f'Rotated Point: ({rotated_point[0]:.2f}, {rotated_point[1]:.2f}, {rotated_point[2]:.2f})'
        )

        # 元の点から回転後の点への線分をプロット
        ax.plot(
            [original_point[0], rotated_point[0]],
            [original_point[1], rotated_point[1]],
            [original_point[2], rotated_point[2]],
            'g--', linewidth=1.5, label='Path of Rotation'
        )
        
        ax.legend()

        plt.title(f'3D Rotation of Point ({original_point[0]:.2f}, {original_point[1]:.2f}, {original_point[2]:.2f})\n'
                  f'X-axis: {angle_x} deg, Y-axis: {angle_y} deg, Z-axis: {angle_z} deg')
        plt.show()
```