You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优化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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 16:37:01