【Python】绘制椭圆眼睛跟随鼠标交互算法配图详解
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') # 标注半长轴aax1.text(0, b/2, 'b', fontsize=12, ha='left', va='center') # 标注半短轴bax1.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 = 60b = 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 = 60b = 40pupil_scale = 0.3 # 眼珠相对于眼睛大小的比例rp = a * pupil_scale # 眼珠半径xe, ye = 0, 0# 生成测试点:在不同角度和距离上测试num_points = 8angles = 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, Ymmarker = 'o' # 圆形标记else:# 安全区域外:眼珠停留在边界上u = np.array([Xm/d, Ym/d]) # 单位方向向量Xp, Yp = u * (a - rp)marker = 's' # 方形标记# 绘制鼠标位置label = Noneif 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) + Xppupil_circle_y = rp * np.sin(theta) + Ypax1.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, Ymmarker = 'o'else:u = np.array([Xm/d, Ym/d])Xp, Yp = u * (a - rp)marker = 's'# 逆变换到椭圆坐标系xm_ellipse = Xm + xeym_ellipse = (b/a) * Ym + yexp_ellipse = Xp + xeyp_ellipse = (b/a) * Yp + ye# 绘制鼠标位置label = Noneif 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 = 60b = 40pupil_scale = 0.3rp = a * pupil_scalexe, 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.1mouse_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 + xeyp = Yp / k + ye# 更新变换后坐标系图形pupil_circle_x = rp * np.cos(theta) + Xppupil_circle_y = rp * np.sin(theta) + Ypmouse_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数学公式- 支持分数、上下标、希腊字母等复杂数学符号