File size: 5,478 Bytes
c3d6fc9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# -*- 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()