File size: 5,879 Bytes
cec303b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""World-anchored checkerboard with a camera that translates with the root."""
from pathlib import Path
import shutil

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.animation import FFMpegWriter
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import numpy as np

CHAINS = [
    [0, 2, 5, 8, 11], [0, 1, 4, 7, 10],
    [0, 3, 6, 9, 12, 15], [9, 14, 17, 19, 21], [9, 13, 16, 18, 20],
]
COLORS = ["#43d5ca", "#3899ed", "#f3c46c", "#e586ad", "#b7a0f5"]
FLOOR_COLORS = ["#e4d8bd", "#627988"]


def world_to_camera(points, root):
    """Apply the identical horizontal translation to body, floor and landmarks."""
    result = np.array(points, dtype=np.float64, copy=True)
    result[..., 0] -= root[0]
    result[..., 2] -= root[2]
    return result


def checkerboard(root, radius, tile_size=0.5):
    """Tiles retain world coordinates and parity when the camera crosses a cell."""
    x0, z0 = np.floor(np.asarray(root)[[0, 2]] / tile_size).astype(int)
    extent = int(np.ceil(3 * radius / tile_size)) + 2
    tiles, colors = [], []
    for ix in range(x0 - extent, x0 + extent + 1):
        for iz in range(z0 - extent, z0 + extent + 1):
            x, z = ix * tile_size, iz * tile_size
            world = [[x, 0, z], [x + tile_size, 0, z],
                     [x + tile_size, 0, z + tile_size], [x, 0, z + tile_size]]
            camera = world_to_camera(world, root)
            tiles.append(camera[:, [0, 2, 1]])
            colors.append(FLOOR_COLORS[(ix + iz) % 2])
    return tiles, colors


def render_mp4(joints, destination, fps=20):
    joints = np.asarray(joints, dtype=np.float64)
    if joints.ndim != 3 or joints.shape[1:] != (22, 3) or len(joints) < 2:
        raise ValueError("Rendering expects positions with shape (T,22,3).")
    if not np.isfinite(joints).all():
        raise ValueError("Invalid joint positions for rendering.")
    if not np.isfinite(fps) or not 1 <= float(fps) <= 120:
        raise ValueError("Rendering requires a frame rate between 1 and 120 fps.")
    ffmpeg = shutil.which("ffmpeg")
    if not ffmpeg:
        try:
            import imageio_ffmpeg
            ffmpeg = imageio_ffmpeg.get_ffmpeg_exe()
        except (ImportError, RuntimeError) as error:
            raise RuntimeError("FFmpeg was not found. Install it before rendering MP4 files.") from error
    matplotlib.rcParams["animation.ffmpeg_path"] = ffmpeg
    destination = Path(destination)
    destination.parent.mkdir(parents=True, exist_ok=True)
    positions = joints.copy()
    positions[:, :, 1] -= positions[:, :, 1].min()
    local = positions.copy()
    local[:, :, 0] -= positions[:, :1, 0]
    local[:, :, 2] -= positions[:, :1, 2]
    height = max(float(positions[:, :, 1].max()), 1.0)
    radius = max(float(np.abs(local[:, :, [0, 2]]).max()) * 1.3, height * 0.85)
    fig = plt.figure(figsize=(6, 6), dpi=96, facecolor="#111c2d")
    ax = fig.add_subplot(111, projection="3d", facecolor="#111c2d", computed_zorder=False)
    ax.set_xlim(-radius, radius)
    ax.set_ylim(-radius, radius)
    ax.set_zlim(-0.02, height * 1.15)
    ax.set_box_aspect((2 * radius, 2 * radius, height * 1.15))
    ax.view_init(elev=20, azim=-65)
    ax.set_axis_off()
    fig.subplots_adjust(0, 0.05, 1, 1)
    floor = Poly3DCollection([], edgecolors="#344353", linewidths=0.3, zorder=0)
    ax.add_collection3d(floor)
    trail, = ax.plot([], [], [], color="#eeae56", lw=1.5, alpha=0.8, zorder=2)
    start, = ax.plot([], [], [], marker="o", markersize=6, color="#f07d55", zorder=2)
    lines = [ax.plot([], [], [], color=color, lw=3.2, solid_capstyle="round", zorder=3)[0]
             for color in COLORS]
    fig.text(0.5, 0.035, "World-fixed floor 路 0.5 m tiles 路 camera follows the person",
             ha="center", color="#bdc9d8", fontsize=8,
             bbox={"facecolor": "#111c2d", "edgecolor": "none", "pad": 4})
    distance = fig.text(0.5, 0.085, "", ha="center", color="#e4d8bd", fontsize=9,
                        bbox={"facecolor": "#111c2d", "edgecolor": "none", "pad": 4})
    roots = positions[:, 0].copy()
    roots[:, 1] = 0.006
    writer = FFMpegWriter(fps=fps, codec="libx264", bitrate=1800,
                          extra_args=["-pix_fmt", "yuv420p", "-movflags", "+faststart", "-threads", "2"])
    try:
        with writer.saving(fig, str(destination), dpi=96):
            for index, frame in enumerate(positions):
                root = frame[0]
                tiles, colors = checkerboard(root, radius)
                floor.set_verts(tiles)
                floor.set_facecolor(colors)
                for line, chain in zip(lines, CHAINS):
                    points = local[index, chain]
                    line.set_data(points[:, 0], points[:, 2])
                    line.set_3d_properties(points[:, 1])
                path = world_to_camera(roots[:index + 1], root)
                outside = (np.abs(path[:, 0]) > radius) | (np.abs(path[:, 2]) > radius)
                path[outside] = np.nan
                trail.set_data(path[:, 0], path[:, 2])
                trail.set_3d_properties(path[:, 1])
                origin = world_to_camera(roots[0], root)
                visible_origin = abs(origin[0]) <= radius and abs(origin[2]) <= radius
                start.set_data([origin[0]] if visible_origin else [], [origin[2]] if visible_origin else [])
                start.set_3d_properties([origin[1]] if visible_origin else [])
                traveled = np.linalg.norm(frame[0, [0, 2]] - positions[0, 0, [0, 2]])
                distance.set_text(f"{index / fps:.2f} s   路   displacement from start: {traveled:.2f} m")
                writer.grab_frame(facecolor=fig.get_facecolor())
    finally:
        plt.close(fig)
    if not destination.is_file() or destination.stat().st_size == 0:
        raise RuntimeError("FFmpeg did not produce the video.")
    return destination