如何优化Matplotlib中3D肢体动画的保存速度?
优化Matplotlib 3D火柴人动画导出MP4的性能
问题背景
我有一个大型数据集,包含多名人员多帧肢体的X、Y、Z坐标,用Python结合Matplotlib制作3D火柴人动画展示运动状态。当前从多个CSV读取数据(每帧对应一个文件,含25人25个肢体坐标),通过定义关节连接关系绘制连接线,用FuncAnimation更新帧。动画生成正常,但保存为MP4时耗时极长,尝试过调整编码器、比特率、FPS都没明显效果。
当前代码如下:
# 读取数据并创建包含所有帧文件数据的DataFrame #... # 初始化绘图 fig = plt.figure(figsize=(10, 30), dpi = 100) ax = fig.add_subplot(projection='3d') # 设置坐标轴范围 ax.set_xlim(-50, 50) ax.set_ylim(-35, 35) ax.set_zlim(0, 2) ax.set_aspect('equal') # 定义关节间的连接关系 connections = [ ('neck', 'r_shoulder'), ('r_shoulder', 'r_elbow'), ('r_elbow', 'r_wrist'), ('neck', 'l_shoulder'), ('l_shoulder', 'l_elbow'), ('l_elbow', 'l_wrist'), ('neck', 'pelvis'), ('pelvis', 'r_hip'), ... ] # 为不同队伍创建独特色彩映射 team_ids = frame_data['team_id'].unique() team_colors = { "Home": 'red', "Away": 'blue', "Referees": 'green' } # 创建唯一帧号列表 unique_frames = frame_data['frame_number'].unique() ball_row = 0 ani = FuncAnimation(fig, update_plot, frames=unique_frames, interval=50) def update_plot(frame): ax.clear() # 过滤当前帧的数据 frame_data_for_frame = frame_data[frame_data['frame_number'] == frame] # 绘制关节间的连接线 for _, row in frame_data_for_frame.iterrows(): team_id = row['team_id'] # 根据team_id获取当前球员的颜色 color = team_colors.get(team_id, 'black') for connection in connections: start_joint = connection[0] end_joint = connection[1] start_x = row[start_joint+'_x'] start_y = row[start_joint+'_y'] start_z = row[start_joint+'_z'] end_x = row[end_joint+'_x'] end_y = row[end_joint+'_y'] end_z = row[end_joint+'_z'] ax.plot([start_x, end_x], [start_y, end_y], [start_z, end_z], marker='.', markersize=0.5, color=color, linewidth=0.5) ani.save('basic_animation.mp4', writer="ffmpeg", fps=50)
核心优化方案
1. 复用绘图对象,避免每帧清空重绘
你当前的update_plot每次调用ax.clear(),会销毁所有已绘制的线条再重新创建,这是最大的性能瓶颈。正确的做法是预先创建所有需要的线条对象,只在更新帧时修改它们的坐标数据,而不是重建整个绘图。
2. 预处理数据,减少帧内重复计算
提前按帧对数据分组,避免每次更新时执行frame_data[frame_data['frame_number'] == frame]这种耗时的过滤操作,直接通过分组获取当前帧数据。
3. 简化渲染元素,降低负载
- 移除不必要的
marker='.'(如果不需要显示关节点的话),或者进一步缩小标记尺寸 - 降低线条宽度(比如从0.5调到0.3)
- 关闭坐标轴的网格、刻度、标签等非必要元素,减少渲染计算量
4. 使用非交互式渲染后端
Matplotlib默认的GUI后端(比如TkAgg)会有额外的交互开销,改用Agg后端可以专注于批量渲染,提升速度。
5. 优化FFmpeg编码参数
使用更快的编码预设,或者硬件加速编码器(如果你的机器支持):
- 用
libx264编码器配合preset='ultrafast'(牺牲少量画质换速度,可根据需求调整为fast/medium) - 如果有NVIDIA显卡,用
h264_nvenc硬件编码器;AMD显卡用h264_amf
优化后的完整代码
import matplotlib matplotlib.use('Agg') # 启用非交互式后端,避免GUI渲染开销 import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation import pandas as pd # 读取数据并创建包含所有帧文件数据的DataFrame # ... 替换成你的数据读取逻辑 # 预处理:按帧分组,后续直接通过分组获取数据,避免重复过滤 frame_groups = frame_data.groupby('frame_number') # 初始化绘图 fig = plt.figure(figsize=(10, 30), dpi=100) ax = fig.add_subplot(projection='3d') # 设置坐标轴范围,关闭非必要元素 ax.set_xlim(-50, 50) ax.set_ylim(-35, 35) ax.set_zlim(0, 2) ax.set_aspect('equal') ax.grid(False) # 关闭网格 ax.set_xticks([]) # 隐藏刻度 ax.set_yticks([]) ax.set_zticks([]) ax.set_xlabel('') # 隐藏坐标轴标签 ax.set_ylabel('') ax.set_zlabel('') # 定义关节间的连接关系 connections = [ ('neck', 'r_shoulder'), ('r_shoulder', 'r_elbow'), ('r_elbow', 'r_wrist'), ('neck', 'l_shoulder'), ('l_shoulder', 'l_elbow'), ('l_elbow', 'l_wrist'), ('neck', 'pelvis'), ('pelvis', 'r_hip'), # ... 补充剩余的连接关系 ] # 队伍颜色映射 team_colors = { "Home": 'red', "Away": 'blue', "Referees": 'green' } # 预先创建所有线条对象:每个球员的每个关节连接对应一条线 lines = [] # 用第一帧数据初始化所有线条 first_frame_data = frame_groups.first() for _, player_row in first_frame_data.iterrows(): player_color = team_colors.get(player_row['team_id'], 'black') for joint_conn in connections: start_joint, end_joint = joint_conn # 初始化线条的初始坐标 x_coords = [player_row[f'{start_joint}_x'], player_row[f'{end_joint}_x']] y_coords = [player_row[f'{start_joint}_y'], player_row[f'{end_joint}_y']] z_coords = [player_row[f'{start_joint}_z'], player_row[f'{end_joint}_z']] line, = ax.plot(x_coords, y_coords, z_coords, color=player_color, linewidth=0.3) lines.append(line) # 每个球员对应的线条数量 lines_per_player = len(connections) def update_plot(frame): # 获取当前帧的所有球员数据 current_frame_data = frame_groups.get_group(frame) # 遍历每个球员,更新对应的线条坐标 for player_idx, (_, player_row) in enumerate(current_frame_data.iterrows()): # 计算当前球员对应的线条起始索引 line_start_idx = player_idx * lines_per_player for conn_idx, joint_conn in enumerate(connections): start_joint, end_joint = joint_conn # 获取关节坐标 x1, y1, z1 = player_row[f'{start_joint}_x'], player_row[f'{start_joint}_y'], player_row[f'{start_joint}_z'] x2, y2, z2 = player_row[f'{end_joint}_x'], player_row[f'{end_joint}_y'], player_row[f'{end_joint}_z'] # 更新线条数据 target_line = lines[line_start_idx + conn_idx] target_line.set_data([x1, x2], [y1, y2]) target_line.set_3d_properties([z1, z2]) return lines # 返回需要更新的元素,开启blit加速 # 创建动画,启用blit=True只更新变化的元素 ani = FuncAnimation(fig, update_plot, frames=frame_groups.groups.keys(), interval=50, blit=True) # 保存动画,使用高效编码参数 ani.save('optimized_animation.mp4', writer="ffmpeg", fps=50, codec='libx264', preset='ultrafast', # 最快编码预设,可根据画质调整为fast/medium bitrate='500k') # 可选:降低比特率进一步减少编码时间
内容的提问来源于stack exchange,提问作者Jmagb
相关产品推荐
相关产品推荐

