Python绘制椭圆眼睛跟随鼠标交互算法配图详解

摘要

本文详细讲解如何使用Python绘制椭圆眼睛跟随鼠标交互算法的配图,包括仿射变换原理、边界条件分析和完整算法演示。通过matplotlib和numpy库,我们将生成专业的可视化图像,帮助理解这一计算机图形学算法。

环境准备

首先安装所需的Python包:

pip install matplotlib numpy pillow

完整代码实现

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Circle
import matplotlib.patches as patches
import os
from matplotlib.animation import FuncAnimation

# 设置中文字体(可选)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 创建专门的图像文件夹
image_folder = "processing_images"
if not os.path.exists(image_folder):
    os.makedirs(image_folder)
print(f"图像将保存在: {os.path.abspath(image_folder)}")

def get_save_path(filename):
    """生成完整的保存路径"""
    return os.path.join(image_folder, filename)

代码详解:基础设置

知识点讲解

  • matplotlib.pyplot 是Python中最常用的绘图库,提供类似MATLAB的绘图接口
  • numpy 是科学计算基础库,用于高效的数组操作
  • matplotlib.patches 包含各种形状的绘制工具,如椭圆、圆形等
  • FuncAnimation 用于创建动画效果
  • rcParams 用于设置matplotlib的全局参数,如字体等

第一部分:椭圆到圆的映射关系图

在这里插入图片描述

def plot_ellipse_to_circle_mapping():
    """
    图1:椭圆坐标系和圆形坐标系的映射关系
    
    功能说明:
    - 展示原始椭圆坐标系和变换后圆形坐标系的关系
    - 标注关键参数a(半长轴)和b(半短轴)
    - 显示仿射变换的基本概念
    """
    # 创建画布和子图
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
    
    # 参数设置
    a = 60  # 椭圆半长轴(x轴方向)
    b = 40  # 椭圆半短轴(y轴方向)
    xe, ye = 0, 0  # 椭圆中心坐标
    
    # 生成椭圆和圆的边界点
    # 使用参数方程表示椭圆和圆
    theta = np.linspace(0, 2*np.pi, 100)  # 0到2π的100个等分点
    x_ellipse = xe + a * np.cos(theta)    # 椭圆x坐标:x = xe + a*cos(θ)
    y_ellipse = ye + b * np.sin(theta)    # 椭圆y坐标:y = ye + b*sin(θ)
    x_circle = a * np.cos(theta)          # 圆x坐标(变换后)
    y_circle = a * np.sin(theta)          # 圆y坐标(变换后)
    
    # 子图1:原始椭圆坐标系
    ax1.plot(x_ellipse, y_ellipse, 'b-', linewidth=2, label='椭圆边界')
    # 绘制坐标轴
    ax1.axhline(y=0, color='k', linestyle='-', linewidth=0.5)  # x轴
    ax1.axvline(x=0, color='k', linestyle='-', linewidth=0.5)  # y轴
    
    # 标注参数
    ax1.text(a/2, 0, 'a', fontsize=12, ha='center', va='top')      # 标注半长轴a
    ax1.text(0, b/2, 'b', fontsize=12, ha='left', va='center')     # 标注半短轴b
    
    ax1.set_xlabel('x')
    ax1.set_ylabel('y')
    ax1.set_title('原始椭圆坐标系')
    ax1.grid(True, alpha=0.3)
    ax1.axis('equal')  # 保证坐标轴比例相等
    ax1.set_xlim(-a-10, a+10)
    ax1.set_ylim(-b-10, b+10)
    
    # 子图2:变换后的圆形坐标系
    ax2.plot(x_circle, y_circle, 'r-', linewidth=2, label='圆形边界')
    ax2.axhline(y=0, color='k', linestyle='-', linewidth=0.5)
    ax2.axvline(x=0, color='k', linestyle='-', linewidth=0.5)
    
    # 标注半径
    ax2.text(a/2, a/2, f'半径 = a = {a}', fontsize=12)
    ax2.set_xlabel('X')
    ax2.set_ylabel('Y')
    ax2.set_title('变换后圆形坐标系')
    ax2.grid(True, alpha=0.3)
    ax2.axis('equal')
    ax2.set_xlim(-a-10, a+10)
    ax2.set_ylim(-a-10, a+10)
    
    plt.suptitle('图1:椭圆到圆的仿射变换映射', fontsize=16, fontweight='bold')
    plt.tight_layout()
    plt.savefig(get_save_path('ellipse_to_circle_mapping.png'), dpi=300, bbox_inches='tight')
    plt.show()

代码详解:椭圆到圆映射

核心概念

  • 参数方程:椭圆使用参数方程表示,避免了复杂的隐式方程处理
  • np.linspace(0, 2*np.pi, 100) 生成0到2π的100个等分点,用于绘制平滑曲线
  • axis('equal') 确保x轴和y轴比例相同,避免图像变形

仿射变换原理

  • 通过缩放y坐标:Y = (a/b) * (y - ye) 将椭圆变换为圆形
  • 变换后的圆半径为原始椭圆的半长轴a

第二部分:仿射变换过程示意图

在这里插入图片描述

def plot_affine_transformation_process():
    """
    图2:仿射变换过程示意图
    
    功能说明:
    - 显示网格在变换前后的变化
    - 展示变换公式和逆变换公式
    - 直观展示仿射变换对几何形状的影响
    """
    fig = plt.figure(figsize=(10, 8))
    
    # 参数设置
    a = 60
    b = 40
    
    # 创建原始网格
    # 在椭圆范围内创建均匀分布的网格点
    x_orig = np.linspace(-a, a, 15)
    y_orig = np.linspace(-b, b, 15)
    x_orig, y_orig = np.meshgrid(x_orig, y_orig)  # 生成网格坐标
    
    # 应用仿射变换到网格
    x_trans = x_orig                    # x坐标不变
    y_trans = (a/b) * y_orig           # y坐标按比例缩放
    
    # 椭圆和圆的边界
    theta = np.linspace(0, 2*np.pi, 100)
    x_ellipse = a * np.cos(theta)
    y_ellipse = b * np.sin(theta)
    x_circle = a * np.cos(theta)
    y_circle = a * np.sin(theta)
    
    # 子图1:原始椭圆和网格
    ax1 = plt.subplot(2, 2, 1)
    # 绘制网格线
    for i in range(x_orig.shape[0]):
        ax1.plot(x_orig[i, :], y_orig[i, :], 'b-', linewidth=0.5, alpha=0.7)
    for i in range(x_orig.shape[1]):
        ax1.plot(x_orig[:, i], y_orig[:, i], 'b-', linewidth=0.5, alpha=0.7)
    
    ax1.plot(x_ellipse, y_ellipse, 'r-', linewidth=3, label='椭圆边界')
    ax1.set_title('原始椭圆和网格')
    ax1.set_xlabel('x')
    ax1.set_ylabel('y')
    ax1.grid(True, alpha=0.3)
    ax1.axis('equal')
    
    # 子图2:变换后网格
    ax2 = plt.subplot(2, 2, 2)
    for i in range(x_trans.shape[0]):
        ax2.plot(x_trans[i, :], y_trans[i, :], 'b-', linewidth=0.5, alpha=0.7)
    for i in range(x_trans.shape[1]):
        ax2.plot(x_trans[:, i], y_trans[:, i], 'b-', linewidth=0.5, alpha=0.7)
    
    ax2.plot(x_circle, y_circle, 'r-', linewidth=3, label='圆形边界')
    ax2.set_title('仿射变换后:椭圆变为圆')
    ax2.set_xlabel('X')
    ax2.set_ylabel('Y')
    ax2.grid(True, alpha=0.3)
    ax2.axis('equal')
    
    # 子图3:变换公式
    ax3 = plt.subplot(2, 2, 3)
    ax3.axis('off')  # 关闭坐标轴
    # 显示数学公式
    ax3.text(0.1, 0.8, '仿射变换公式:', fontsize=14, fontweight='bold')
    ax3.text(0.1, 0.6, r'$k = \frac{a}{b}$', fontsize=16)
    ax3.text(0.1, 0.4, r'$X = x - x_e$', fontsize=16)
    ax3.text(0.1, 0.2, r'$Y = k \cdot (y - y_e)$', fontsize=16)
    
    # 子图4:逆变换公式
    ax4 = plt.subplot(2, 2, 4)
    ax4.axis('off')
    ax4.text(0.1, 0.8, '逆变换公式:', fontsize=14, fontweight='bold')
    ax4.text(0.1, 0.6, r'$x = X + x_e$', fontsize=16)
    ax4.text(0.1, 0.4, r'$y = \frac{Y}{k} + y_e$', fontsize=16)
    
    plt.suptitle('图2:仿射变换过程示意图', fontsize=16, fontweight='bold')
    plt.tight_layout()
    plt.savefig(get_save_path('affine_transformation_process.png'), dpi=300, bbox_inches='tight')
    plt.show()

代码详解:仿射变换过程

网格生成技术

  • np.meshgrid() 从一维数组生成二维网格坐标,是绘制3D曲面和2D网格的基础
  • 通过遍历网格点绘制网格线,展示坐标变换效果

数学公式显示

  • 使用LaTeX语法在matplotlib中显示数学公式
  • r'$...$' 中的r表示原始字符串,避免转义字符问题
  • LaTeX语法可以显示复杂的数学符号和公式

第三部分:眼珠位置边界条件图

在这里插入图片描述

def plot_pupil_position_boundary():
    """
    图3:眼珠位置计算边界条件
    
    功能说明:
    - 展示安全区域内外的眼珠位置计算
    - 分别在变换后坐标系和原始椭圆坐标系中显示
    - 演示边界条件判断逻辑
    """
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
    
    # 参数设置
    a = 60
    b = 40
    pupil_scale = 0.3  # 眼珠相对于眼睛大小的比例
    rp = a * pupil_scale  # 眼珠半径
    xe, ye = 0, 0
    
    # 生成测试点:在不同角度和距离上测试
    num_points = 8
    angles = np.linspace(0, 2*np.pi, num_points+1)[:-1]  # 8个等分角度
    distances = [a*0.7, a*1.2]  # 内外两个测试距离
    
    # 圆形和椭圆边界
    theta = np.linspace(0, 2*np.pi, 100)
    x_circle = a * np.cos(theta)
    y_circle = a * np.sin(theta)
    x_ellipse = xe + a * np.cos(theta)
    y_ellipse = ye + b * np.sin(theta)
    
    # 安全区域边界(眼珠不能超过的边界)
    safe_circle_x = (a - rp) * np.cos(theta)
    safe_circle_y = (a - rp) * np.sin(theta)
    safe_ellipse_x = (a - rp) * np.cos(theta)
    safe_ellipse_y = (b/a) * (a - rp) * np.sin(theta)  # 注意逆变换
    
    # 使用颜色映射区分不同角度
    colors = plt.cm.tab10(np.linspace(0, 1, num_points))
    
    # 子图1:变换后坐标系
    ax1.plot(x_circle, y_circle, 'k-', linewidth=2, label='圆形边界 (半径=a)')
    ax1.plot(safe_circle_x, safe_circle_y, 'g--', linewidth=2, 
             color=(0, 0.5, 0), label='安全区域边界')
    
    # 遍历所有测试点
    for i, angle in enumerate(angles):
        for j, dist in enumerate(distances):
            # 鼠标位置(变换后坐标系)
            Xm = dist * np.cos(angle)
            Ym = dist * np.sin(angle)
            
            # 计算眼珠位置
            d = np.sqrt(Xm**2 + Ym**2)  # 到圆心的距离
            
            if d <= a - rp:
                # 安全区域内:眼珠直接跟随鼠标
                Xp, Yp = Xm, Ym
                marker = 'o'  # 圆形标记
            else:
                # 安全区域外:眼珠停留在边界上
                u = np.array([Xm/d, Ym/d])  # 单位方向向量
                Xp, Yp = u * (a - rp)
                marker = 's'  # 方形标记
            
            # 绘制鼠标位置
            label = None
            if i == 0:  # 只为第一个角度添加图例标签
                if j == 0:
                    label = '鼠标在安全区内'
                else:
                    label = '鼠标在安全区外'
                    
            ax1.plot(Xm, Ym, marker, color=colors[i], markersize=8, 
                    linewidth=2, label=label)
            
            # 绘制眼珠
            pupil_circle_x = rp * np.cos(theta) + Xp
            pupil_circle_y = rp * np.sin(theta) + Yp
            ax1.plot(pupil_circle_x, pupil_circle_y, '-', 
                    color=colors[i], linewidth=1.5)
            
            # 绘制连线(仅在安全区外)
            if d > a - rp:
                ax1.plot([0, Xm], [0, Ym], ':', color=colors[i], linewidth=1)
    
    ax1.text(0, a+5, '安全区域外', ha='center', fontsize=12)
    ax1.text(0, (a-rp)/2, '安全区域内', ha='center', fontsize=12)
    ax1.set_xlabel('X')
    ax1.set_ylabel('Y')
    ax1.set_title('变换后坐标系中的眼珠位置计算')
    ax1.grid(True, alpha=0.3)
    ax1.axis('equal')
    ax1.legend(loc='upper left', bbox_to_anchor=(1, 1))
    
    # 子图2:原始椭圆坐标系
    ax2.plot(x_ellipse, y_ellipse, 'k-', linewidth=2, label='椭圆边界')
    ax2.plot(safe_ellipse_x, safe_ellipse_y, 'g--', linewidth=2, 
             color=(0, 0.5, 0), label='安全区域边界')
    
    for i, angle in enumerate(angles):
        for j, dist in enumerate(distances):
            # 变换后坐标系中的位置
            Xm = dist * np.cos(angle)
            Ym = dist * np.sin(angle)
            
            # 计算眼珠位置(变换后坐标系)
            d = np.sqrt(Xm**2 + Ym**2)
            if d <= a - rp:
                Xp, Yp = Xm, Ym
                marker = 'o'
            else:
                u = np.array([Xm/d, Ym/d])
                Xp, Yp = u * (a - rp)
                marker = 's'
            
            # 逆变换到椭圆坐标系
            xm_ellipse = Xm + xe
            ym_ellipse = (b/a) * Ym + ye
            xp_ellipse = Xp + xe
            yp_ellipse = (b/a) * Yp + ye
            
            # 绘制鼠标位置
            label = None
            if i == 0:
                if j == 0:
                    label = '鼠标在安全区内'
                else:
                    label = '鼠标在安全区外'
                    
            ax2.plot(xm_ellipse, ym_ellipse, marker, color=colors[i], 
                    markersize=8, linewidth=2, label=label)
            
            # 绘制眼珠位置(椭圆)
            pupil_ellipse_x = xp_ellipse + rp * np.cos(theta)
            pupil_ellipse_y = yp_ellipse + (b/a) * rp * np.sin(theta)
            ax2.plot(pupil_ellipse_x, pupil_ellipse_y, '-', 
                    color=colors[i], linewidth=1.5)
            
            # 绘制连线(仅在安全区外)
            if d > a - rp:
                ax2.plot([xe, xm_ellipse], [ye, ym_ellipse], ':', 
                        color=colors[i], linewidth=1)
    
    ax2.set_xlabel('x')
    ax2.set_ylabel('y')
    ax2.set_title('逆变换回椭圆坐标系')
    ax2.grid(True, alpha=0.3)
    ax2.axis('equal')
    ax2.legend(loc='upper left', bbox_to_anchor=(1, 1))
    
    plt.suptitle('图3:眼珠位置计算的边界条件', fontsize=16, fontweight='bold')
    plt.tight_layout()
    plt.savefig(get_save_path('pupil_position_boundary.png'), dpi=300, bbox_inches='tight')
    plt.show()

代码详解:边界条件分析

颜色映射技术

  • plt.cm.tab10 提供10种区分度良好的颜色
  • np.linspace(0, 1, num_points) 在0到1之间生成等分数值,用于颜色选择

边界条件算法

  • 安全区域边界:a - rp,确保眼珠不超出眼睛边界
  • 单位向量计算:u = [Xm/d, Ym/d],用于确定眼珠移动方向
  • 距离计算:d = sqrt(Xm² + Ym²),判断鼠标是否在安全区域内

图例优化

  • 使用条件判断避免重复的图例标签
  • bbox_to_anchor=(1, 1) 将图例放置在子图外部

第四部分:完整算法演示动画

在这里插入图片描述

def plot_complete_algorithm_demonstration():
    """
    图4:完整算法演示动画
    
    功能说明:
    - 动态展示鼠标移动时眼珠位置的变化
    - 实时显示仿射变换和逆变换过程
    - 直观演示边界条件的作用
    """
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
    
    # 参数设置
    a = 60
    b = 40
    pupil_scale = 0.3
    rp = a * pupil_scale
    xe, ye = 0, 0
    
    # 圆形和椭圆边界
    theta = np.linspace(0, 2*np.pi, 100)
    x_circle = a * np.cos(theta)
    y_circle = a * np.sin(theta)
    x_ellipse = xe + a * np.cos(theta)
    y_ellipse = ye + b * np.sin(theta)
    
    # 安全区域边界
    safe_circle_x = (a - rp) * np.cos(theta)
    safe_circle_y = (a - rp) * np.sin(theta)
    
    # 初始化图形元素
    # 子图1:变换后坐标系
    ax1.plot(x_circle, y_circle, 'k-', linewidth=2, label='圆形边界')
    ax1.plot(safe_circle_x, safe_circle_y, 'g--', linewidth=2, label='安全区域边界')
    mouse_dot1, = ax1.plot([], [], 'ro', markersize=8, label='鼠标位置')
    pupil1, = ax1.plot([], [], 'b-', linewidth=2, label='眼珠')
    connection1, = ax1.plot([], [], 'r:', linewidth=1)
    
    ax1.set_xlabel('X')
    ax1.set_ylabel('Y')
    ax1.set_title('变换后坐标系')
    ax1.grid(True, alpha=0.3)
    ax1.axis('equal')
    ax1.legend()
    ax1.set_xlim(-a-10, a+10)
    ax1.set_ylim(-a-10, a+10)
    
    # 子图2:原始椭圆坐标系
    ax2.plot(x_ellipse, y_ellipse, 'k-', linewidth=2, label='椭圆边界')
    mouse_dot2, = ax2.plot([], [], 'ro', markersize=8, label='鼠标位置')
    pupil2, = ax2.plot([], [], 'b-', linewidth=2, label='眼珠')
    connection2, = ax2.plot([], [], 'r:', linewidth=1)
    
    ax2.set_xlabel('x')
    ax2.set_ylabel('y')
    ax2.set_title('原始椭圆坐标系')
    ax2.grid(True, alpha=0.3)
    ax2.axis('equal')
    ax2.legend()
    ax2.set_xlim(-a-10, a+10)
    ax2.set_ylim(-b-10, b+10)
    
    plt.suptitle('图4:椭圆眼睛跟随鼠标算法演示', fontsize=16, fontweight='bold')
    
    def animate(frame):
        """
        动画更新函数
        
        参数:
        frame - 当前动画帧数,用于计算鼠标位置
        
        功能:
        - 根据帧数计算模拟鼠标位置
        - 应用仿射变换计算眼珠位置
        - 更新两个子图的图形元素
        """
        # 生成模拟鼠标位置(Lissajous曲线)
        t = frame * 0.1
        mouse_x = 80 * np.cos(t)           # x坐标随时间变化
        mouse_y = 60 * np.sin(2*t)         # y坐标以2倍频率变化
        
        # 计算眼珠位置(核心算法)
        k = a / b  # 缩放因子
        Xm = mouse_x - xe  # 变换后x坐标
        Ym = k * (mouse_y - ye)  # 变换后y坐标
        
        d = np.sqrt(Xm**2 + Ym**2)  # 到圆心距离
        
        # 边界条件判断
        if d <= a - rp:
            Xp, Yp = Xm, Ym  # 安全区域内:直接跟随
        else:
            u = np.array([Xm/d, Ym/d])  # 单位方向向量
            Xp, Yp = u * (a - rp)  # 安全区域外:停留在边界
        
        # 逆变换回椭圆坐标系
        xp = Xp + xe
        yp = Yp / k + ye
        
        # 更新变换后坐标系图形
        pupil_circle_x = rp * np.cos(theta) + Xp
        pupil_circle_y = rp * np.sin(theta) + Yp
        
        mouse_dot1.set_data([Xm], [Ym])
        pupil1.set_data(pupil_circle_x, pupil_circle_y)
        
        # 显示/隐藏连线
        if d > a - rp:
            connection1.set_data([0, Xm], [0, Ym])
            connection1.set_visible(True)
        else:
            connection1.set_visible(False)
        
        # 更新原始椭圆坐标系图形
        pupil_ellipse_x = xp + rp * np.cos(theta)
        pupil_ellipse_y = yp + (b/a) * rp * np.sin(theta)
        
        mouse_dot2.set_data([mouse_x], [mouse_y])
        pupil2.set_data(pupil_ellipse_x, pupil_ellipse_y)
        
        if d > a - rp:
            connection2.set_data([xe, mouse_x], [ye, mouse_y])
            connection2.set_visible(True)
        else:
            connection2.set_visible(False)
        
        return mouse_dot1, pupil1, connection1, mouse_dot2, pupil2, connection2
    
    # 创建动画
    anim = FuncAnimation(fig, animate, frames=100, interval=100, blit=True)
    
    plt.tight_layout()
    
    # 保存动画(需要安装pillow)
    try:
        anim.save(get_save_path('algorithm_demonstration.gif'), writer='pillow', fps=10)
        print("动画已保存为 algorithm_demonstration.gif")
    except Exception as e:
        print(f"无法保存动画: {e}")
        print("请安装pillow: pip install pillow")
    
    plt.show()
    
    return anim

代码详解:动画技术

FuncAnimation使用

  • FuncAnimation(fig, animate, frames=100, interval=100, blit=True)
    • fig: 动画所在的图形对象
    • animate: 更新函数,每帧调用
    • frames: 总帧数
    • interval: 帧间隔(毫秒)
    • blit=True: 只重绘变化的部分,提高性能

Lissajous曲线

  • 使用参数方程生成复杂的鼠标移动轨迹
  • mouse_x = 80 * cos(t), mouse_y = 60 * sin(2*t)
  • 产生优美的曲线运动,更好地演示算法效果

动画优化

  • 使用set_visible()控制连线的显示/隐藏
  • blit=True 只更新变化的图形元素,提高渲染效率

主程序执行

# 运行所有绘图函数
if __name__ == "__main__":
    print("生成椭圆眼睛跟随鼠标交互算法配图...")
    
    print("1. 生成椭圆到圆的映射图...")
    plot_ellipse_to_circle_mapping()
    
    print("2. 生成仿射变换过程图...")
    plot_affine_transformation_process()
    
    print("3. 生成眼珠位置边界条件图...")
    plot_pupil_position_boundary()
    
    print("4. 生成完整算法演示动画...")
    anim = plot_complete_algorithm_demonstration()
    
    print("所有配图生成完成!")
    print(f"图像保存在: {os.path.abspath(image_folder)}")

不常用知识点详解

1. 颜色映射(Color Map)

colors = plt.cm.tab10(np.linspace(0, 1, num_points))
  • plt.cm 包含多种颜色映射方案
  • tab10 提供10种区分度良好的颜色
  • np.linspace(0, 1, num_points) 在0-1范围内生成等分数值
  • 结果是一个RGBA颜色数组,每个元素代表一个颜色

2. 网格生成(Meshgrid)

x = np.linspace(-a, a, 15)
y = np.linspace(-b, b, 15)
x_grid, y_grid = np.meshgrid(x, y)
  • 从一维坐标数组生成二维网格坐标
  • 常用于3D曲面绘图和2D网格显示
  • 返回的x_grid和y_grid都是二维数组

3. 动画技术中的blit优化

anim = FuncAnimation(..., blit=True)
  • blit=True 只重绘变化的图形元素,大幅提高性能
  • 需要更新函数返回所有需要重绘的图形对象
  • 对于复杂动画,性能提升非常明显

4. LaTeX数学公式渲染

ax.text(0.1, 0.6, r'$k = \frac{a}{b}$', fontsize=16)
  • 使用r''原始字符串避免转义字符问题
  • $...$ 包围LaTeX数学公式
  • 支持分数、上下标、希腊字母等复杂数学符号
Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐