【Python】绘制椭圆眼睛跟随鼠标交互算法配图详解
摘要 本文通过Python演示椭圆眼睛跟随鼠标交互的算法实现,重点讲解了仿射变换原理和边界条件处理。使用matplotlib和numpy库,文章展示了从椭圆到圆的坐标映射、仿射变换过程可视化,以及完整的交互式眼睛动画。内容包括:1) 椭圆与圆的参数方程转换;2) 网格变换过程图解;3) 边界条件处理机制;4) 最终交互实现代码。算法通过缩放变换将椭圆坐标系转为圆形坐标系,使瞳孔能自然跟随鼠标移动。
·
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数学公式- 支持分数、上下标、希腊字母等复杂数学符号
更多推荐



所有评论(0)