Download code/make_combined_video.py from lin267/value-based-qlearning: direct link, hf CLI and curl.
- Browser
- Download file 5.48 kB
-
https://huggingface.co/lin267/value-based-qlearning/resolve/main/code/make_combined_video.py
- Command line
-
hf download hf://lin267/value-based-qlearning/code/make_combined_video.py
-
curl -L -o make_combined_video.py https://huggingface.co/lin267/value-based-qlearning/resolve/main/code/make_combined_video.py
5.48 kB
| # -*- coding: utf-8 -*- | |
| """ | |
| 把三个智能体的回放视频拼成一支"总回放"视频(方便上传 Hub / 交作业)。 | |
| ============================================================== | |
| 输入:videos/replay_frozenlake4x4.mp4、replay_frozenlake8x8.mp4、replay_taxi.mp4 | |
| 输出:videos/replay_value_based_all.mp4(1280x720 / 30fps,开头有总览卡,结尾有分数汇总卡) | |
| 用法: | |
| D:\\study\\robot\\pyenv\\DRL\\python.exe make_combined_video.py | |
| """ | |
| import json | |
| import os | |
| import imageio.v2 as imageio | |
| import numpy as np | |
| import evaluate_and_record as ev # 复用录像脚本里的画布/字体/配色 | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| VIDEO_DIR = os.path.join(BASE_DIR, "videos") | |
| EVAL_DIR = os.path.join(BASE_DIR, "eval_results") | |
| FPS = ev.FPS | |
| RUNS = [ | |
| ("frozenlake4x4", "FrozenLake-v1 · 4x4 不滑", "PPT 第一部分(10,000 回合)"), | |
| ("frozenlake8x8", "FrozenLake-v1 · 8x8", "进阶:地图变大 + 探索调度"), | |
| ("taxi", "Taxi-v3", "PPT 第二部分(25,000 回合)"), | |
| ] | |
| def load_scores(): | |
| scores = {} | |
| for run, _, _ in RUNS: | |
| path = os.path.join(EVAL_DIR, f"eval_{run}.json") | |
| if os.path.exists(path): | |
| with open(path, encoding="utf-8") as f: | |
| scores[run] = json.load(f) | |
| return scores | |
| def intro_card(scores): | |
| canvas, draw = ev.base_canvas() | |
| ev.draw_text(draw, (ev.WIDTH // 2, 110), "value-based 实践 · Q-Learning 智能体回放合集", | |
| ev.FONT_TITLE, fill=ev.ACCENT, anchor="mm") | |
| ev.draw_text(draw, (ev.WIDTH // 2, 165), | |
| "从零手写 Q 表 + epsilon-greedy + Bellman 更新(PPT 第 27–40 页)", | |
| ev.FONT_BODY, fill=ev.TEXT, anchor="mm") | |
| draw.line([120, 205, ev.WIDTH - 120, 205], fill=ev.PANEL_EDGE, width=2) | |
| y = 265 | |
| for i, (run, name, note) in enumerate(RUNS): | |
| s = scores.get(run) | |
| ev.draw_text(draw, (110, y), f"{i + 1}. {name}", ev.FONT_H2, fill=ev.TEXT) | |
| ev.draw_text(draw, (110, y + 34), note, ev.FONT_SMALL, fill=ev.MUTED) | |
| if s: | |
| score_text = f"{s['mean_reward']:.2f} ± {s['std_reward']:.2f}" | |
| ev.draw_text(draw, (ev.WIDTH - 110, y + 16), score_text, ev.FONT_H1, | |
| fill=ev.GREEN if s["mean_reward"] > 0 else ev.RED, anchor="rm") | |
| ev.draw_text(draw, (ev.WIDTH - 110, y + 46), | |
| f"成功率 {s['success_rate'] * 100:.0f}% " | |
| f"平均步数 {s['mean_steps']:.1f}", | |
| ev.FONT_SMALL, fill=ev.MUTED, anchor="rm") | |
| y += 95 | |
| ev.draw_text(draw, (ev.WIDTH // 2, 620), | |
| "每段回放都标注了步数 / 动作 / Q 值条形图,片尾有该智能体的评估分数", | |
| ev.FONT_SMALL, fill=ev.MUTED, anchor="mm") | |
| return canvas | |
| def end_card(scores): | |
| canvas, draw = ev.base_canvas() | |
| ev.draw_text(draw, (ev.WIDTH // 2, 105), "三个智能体评估分数汇总", ev.FONT_TITLE, | |
| fill=ev.ACCENT, anchor="mm") | |
| ev.draw_text(draw, (ev.WIDTH // 2, 155), | |
| "独立环境 · 贪婪策略 argmax_a Q(s,a) · 100 局", ev.FONT_BODY, | |
| fill=ev.MUTED, anchor="mm") | |
| draw.line([120, 195, ev.WIDTH - 120, 195], fill=ev.PANEL_EDGE, width=2) | |
| y = 265 | |
| for run, name, _ in RUNS: | |
| s = scores.get(run) | |
| if not s: | |
| continue | |
| ev.draw_text(draw, (120, y), name, ev.FONT_H2, fill=ev.TEXT) | |
| ev.draw_text(draw, (560, y), f"回报 {s['mean_reward']:.2f} ± {s['std_reward']:.2f}", | |
| ev.FONT_H2, fill=ev.GREEN if s["mean_reward"] > 0 else ev.RED) | |
| ev.draw_text(draw, (880, y), f"成功率 {s['success_rate'] * 100:.0f}%", | |
| ev.FONT_H2, fill=ev.ACCENT) | |
| y += 90 | |
| ev.draw_text(draw, (ev.WIDTH // 2, 610), | |
| "模型文件(Q 表 pkl)+ 训练代码 + 日志 已打包上传 Hub", | |
| ev.FONT_BODY, fill=ev.TEXT, anchor="mm") | |
| return canvas | |
| def main(): | |
| scores = load_scores() | |
| clips = [] | |
| for run, _, _ in RUNS: | |
| path = os.path.join(VIDEO_DIR, f"replay_{run}.mp4") | |
| if not os.path.exists(path): | |
| print(f"[跳过] 找不到 {path},请先跑 evaluate_and_record.py --model models/qtable_{run}.pkl") | |
| continue | |
| clips.append(path) | |
| if not clips: | |
| raise SystemExit("没有可拼接的视频。") | |
| out_path = os.path.join(VIDEO_DIR, "replay_value_based_all.mp4") | |
| writer = imageio.get_writer(out_path, fps=FPS, codec="libx264", quality=8, | |
| macro_block_size=2) | |
| frames = 0 | |
| def push(img, repeat=1): | |
| nonlocal frames | |
| arr = np.asarray(img, dtype=np.uint8) | |
| for _ in range(repeat): | |
| writer.append_data(arr) | |
| frames += 1 | |
| push(intro_card(scores), int(3.0 * FPS)) | |
| for path in clips: | |
| reader = imageio.get_reader(path) | |
| n = 0 | |
| for frame in reader: | |
| writer.append_data(frame) | |
| frames += 1 | |
| n += 1 | |
| reader.close() | |
| print(f" [拼接] {os.path.basename(path)}:{n} 帧") | |
| push(end_card(scores), int(5.0 * FPS)) | |
| writer.close() | |
| size_mb = os.path.getsize(out_path) / (1024 * 1024) | |
| print(f"[输出] 总回放视频 = videos/{os.path.basename(out_path)} " | |
| f"({frames} 帧 / {frames / FPS:.1f} 秒 / {size_mb:.1f} MB)") | |
| if __name__ == "__main__": | |
| main() | |