t2m-gpt-vsl-code / render_trivis_strip.py
Tri1's picture
T2M-GPT VSL adaptation: Python sources only (82 files, no checkpoints or data)
8e5456b verified
Raw
History Blame Contribute Delete
6.15 kB
#!/usr/bin/env python3
"""Render one TriVis sentence across every system, straight from the scored dumps.
Reads the same .npz dumps `eval_trivis_metrics.py` scores, so the picture is
guaranteed to show exactly what the numbers describe -- no second generation pass,
no risk of a render that disagrees with the table.
All dumps are 50 joints in TriVis frame coordinates: body 0:8 (OpenPose 0-7),
left hand 8:29, right hand 29:50. Drawn in pixels so proportions are correct.
"""
import argparse
import glob
import os
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.animation import FFMpegWriter, FuncAnimation
# OpenPose 0-7: 0 nose, 1 neck, 2 Rsho, 3 Relb, 4 Rwri, 5 Lsho, 6 Lelb, 7 Lwri
BODY_EDGES = [(1, 0), (1, 2), (2, 3), (3, 4), (1, 5), (5, 6), (6, 7)]
HAND_EDGES = [(0, 1), (1, 2), (2, 3), (3, 4), (0, 5), (5, 6), (6, 7), (7, 8), (0, 9),
(9, 10), (10, 11), (11, 12), (0, 13), (13, 14), (14, 15), (15, 16),
(0, 17), (17, 18), (18, 19), (19, 20), (5, 9), (9, 13), (13, 17)]
LH, RH = 8, 29
WRIST_L, WRIST_R = 7, 4 # body wrists, to attach each hand root
def draw(ax, xy, color, hand_color, W, H, lo, hi, lw=1.6):
ax.clear()
px, py = xy[:, 0] * W, xy[:, 1] * H
for a, b in BODY_EDGES:
ax.plot([px[a], px[b]], [py[a], py[b]], color=color, lw=lw, solid_capstyle='round')
for off, wr in ((LH, WRIST_L), (RH, WRIST_R)):
ax.plot([px[wr], px[off]], [py[wr], py[off]], color=color, lw=lw * 0.8)
for a, b in HAND_EDGES:
ax.plot([px[off + a], px[off + b]], [py[off + a], py[off + b]],
color=hand_color, lw=lw * 1.1, solid_capstyle='round')
ax.set_xlim(lo[0], hi[0]); ax.set_ylim(hi[1], lo[1])
ax.set_aspect('equal'); ax.axis('off')
def load(p):
d = np.load(p, allow_pickle=True)
return {str(n): q for n, q in zip(d['names'], d['poses'])}
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--panels', nargs='+', required=True,
help='label=path/to.npz, in display order')
ap.add_argument('--clip', default=None, help='clip name (default: first shared)')
ap.add_argument('--frames', type=int, default=6)
ap.add_argument('--fps', type=int, default=30)
ap.add_argument('--frame-w', type=float, default=1176.0)
ap.add_argument('--frame-h', type=float, default=1288.0)
ap.add_argument('--gloss', default=None, help='caption text')
ap.add_argument('--out', default='qual_trivis/strip')
ap.add_argument('--mp4', action='store_true')
ap.add_argument('--gif', action='store_true',
help='also write a gif (ffmpeg palettegen: far smaller and cleaner '
'than a direct matplotlib gif)')
ap.add_argument('--gif-fps', type=int, default=15)
ap.add_argument('--gif-width', type=int, default=900)
args = ap.parse_args()
panels = []
for spec in args.panels:
lab, path = spec.split('=', 1)
panels.append((lab, load(path)))
shared = set(panels[0][1])
for _, m in panels[1:]:
shared &= set(m)
if not shared:
raise SystemExit('no clip shared by all panels')
clip = args.clip or sorted(shared)[0]
if clip not in shared:
raise SystemExit(f'{clip} not in all panels')
print(f'clip {clip} ({len(shared)} shared)')
seqs = [(lab, np.asarray(m[clip], np.float32)) for lab, m in panels]
W, H = args.frame_w, args.frame_h
ref = seqs[0][1]
lo = (ref.reshape(-1, 2).min(0) * np.array([W, H])) - 60
hi = (ref.reshape(-1, 2).max(0) * np.array([W, H])) + 60
cols = [('#111111', '#c0392b'), ('#1f6f3f', '#27ae60'), ('#1a4f8a', '#2980b9'),
('#8e44ad', '#c39bd3'), ('#b9770e', '#e59866'), ('#117a65', '#45b39d')]
os.makedirs(os.path.dirname(args.out) or '.', exist_ok=True)
title = f"{args.gloss or clip}"
# ---- keyframe grid: one row per system ----
nf = args.frames
fig, axes = plt.subplots(len(seqs), nf, figsize=(1.55 * nf, 1.9 * len(seqs)))
if len(seqs) == 1:
axes = axes[None, :]
fig.suptitle(title, fontsize=9)
for r, (lab, seq) in enumerate(seqs):
ts = np.linspace(0, len(seq) - 1, nf).astype(int)
c, hc = cols[r % len(cols)]
for ci, t in enumerate(ts):
draw(axes[r, ci], seq[t], c, hc, W, H, lo, hi, lw=1.2)
if ci == 0:
axes[r, ci].set_ylabel(lab, fontsize=7)
axes[r, ci].set_title(f'{t+1}', fontsize=6)
plt.tight_layout(rect=[0, 0, 1, 0.94])
png = args.out + '.png'
fig.savefig(png, dpi=140)
plt.close(fig)
print(f'wrote {png}')
if args.mp4 or args.gif:
nT = max(len(s) for _, s in seqs)
fig, axes = plt.subplots(1, len(seqs), figsize=(2.1 * len(seqs), 3.4))
fig.suptitle(title, fontsize=9)
def frame(t):
for k, (lab, seq) in enumerate(seqs):
tt = min(t, len(seq) - 1)
c, hc = cols[k % len(cols)]
draw(axes[k], seq[tt], c, hc, W, H, lo, hi)
axes[k].set_title(f'{lab}\n{tt+1}/{len(seq)}', fontsize=7)
return []
anim = FuncAnimation(fig, frame, frames=nT, interval=1000 / args.fps)
mp4 = args.out + '.mp4'
anim.save(mp4, writer=FFMpegWriter(fps=args.fps, bitrate=2400))
plt.close(fig)
print(f'wrote {mp4}')
if args.gif:
import subprocess
gif = args.out + '.gif'
pal = args.out + '.pal.png'
vf = f'fps={args.gif_fps},scale={args.gif_width}:-1:flags=lanczos'
subprocess.run(['ffmpeg', '-y', '-loglevel', 'error', '-i', mp4,
'-vf', vf + ',palettegen=stats_mode=diff', pal], check=True)
subprocess.run(['ffmpeg', '-y', '-loglevel', 'error', '-i', mp4, '-i', pal,
'-lavfi', vf + '[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=3',
gif], check=True)
os.remove(pal)
print(f'wrote {gif} ({os.path.getsize(gif)/1e6:.2f} MB)')
if __name__ == '__main__':
main()