Diffusers
Safetensors
File size: 11,052 Bytes
394918c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import os
import torch
import torch.nn as nn
import trimesh
import numpy as np
import io
import tarfile
import pickle
import zstandard
from scipy.spatial import KDTree
from tqdm import tqdm
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# 引入你的模型组件
from models.auto import TemporalTrajectoryPredictor, SpatialGraphRefiner
from models.vae import TripoSGVaeWrapper
from models.clip import FrozenCLIPTextEncoder
from triposg.inference_utils import hierarchical_extract_geometry

# 辅助类
class TextProjectionLayer(nn.Module):
    def __init__(self, in_dim=512, out_dim=64):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(in_dim, in_dim), nn.ReLU(), nn.Linear(in_dim, out_dim))
        self.residual = nn.Linear(in_dim, out_dim) if in_dim != out_dim else nn.Identity()
    def forward(self, x): return self.net(x) + self.residual(x)

def get_full_trajectories(archive_file, limit=20):
    """提取完整的轨迹数据及caption"""
    data_list = []
    with open(archive_file, 'rb') as fh:
        dctx = zstandard.ZstdDecompressor()
        with dctx.stream_reader(fh) as reader:
            with tarfile.open(fileobj=reader, mode='r|') as tar:
                for member in tar:
                    if len(data_list) >= limit: break
                    if member.isfile():
                        f = tar.extractfile(member)
                        if f:
                            data = pickle.load(io.BytesIO(f.read()))
                            # 确保数据中包含 caption
                            if 'vertices' in data and data['vertices'].shape[0] >= 16:
                                data_list.append(data)
    return data_list

def save_mesh_as_png(mesh, save_path):
    """使用matplotlib将mesh渲染并保存为png图片,兼容无显示器的服务器环境"""
    fig = plt.figure(figsize=(6, 6))
    ax = fig.add_subplot(111, projection='3d')
    
    # 提取顶点和面数据
    vertices = mesh.vertices
    faces = mesh.faces
    
    # 绘制3D三角网格
    ax.plot_trisurf(vertices[:, 0], vertices[:, 1], vertices[:, 2], 
                    triangles=faces, cmap='viridis', edgecolor='none', alpha=0.8)
    
    # 保持各轴比例统一并隐藏坐标轴以使视觉效果更干净
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    
    # 设置一个较好的初始观察视角
    ax.view_init(elev=20, azim=45)
    
    # 强制使轴范围一致
    max_range = np.array([vertices[:, 0].max()-vertices[:, 0].min(), 
                          vertices[:, 1].max()-vertices[:, 1].min(), 
                          vertices[:, 2].max()-vertices[:, 2].min()]).max() / 2.0
    mid_x = (vertices[:, 0].max()+vertices[:, 0].min()) * 0.5
    mid_y = (vertices[:, 1].max()+vertices[:, 1].min()) * 0.5
    mid_z = (vertices[:, 2].max()+vertices[:, 2].min()) * 0.5
    ax.set_xlim(mid_x - max_range, mid_x + max_range)
    ax.set_ylim(mid_y - max_range, mid_y + max_range)
    ax.set_zlim(mid_z - max_range, mid_z + max_range)
    
    plt.axis('off')
    plt.savefig(save_path, bbox_inches='tight', pad_inches=0, dpi=150)
    plt.close(fig)

def evaluate():
    # 检测可用GPU数量并进行针对性非对称分配
    num_gpus = torch.cuda.device_count()
    
    # 主卡分配给显存最多的 cuda:0
    dev_main = "cuda:0" 
    
    if num_gpus >= 3:
        # 显存大户(VAE和自回归Predictor)全部塞给大显存的 cuda:0
        dev_vae = dev_main
        dev_temporal = dev_main
        # 轻量级的文本端和空间端移出主卡,分流到剩余卡上
        dev_clip = "cuda:1"
        dev_spatial = "cuda:2"
        print(f"检测到 {num_gpus} 张卡。策略:VAE与Temporal驻留大显存卡({dev_main}),CLIP分流至(cuda:1),Spatial分流至(cuda:2)。")
    elif num_gpus == 2:
        dev_vae = dev_main
        dev_temporal = dev_main
        dev_clip = "cuda:1"
        dev_spatial = "cuda:1"
        print(f"检测到 2 张卡。策略:VAE与Temporal驻留大显存卡({dev_main}),CLIP与Spatial分流至(cuda:1)。")
    else:
        dev_vae = dev_clip = dev_temporal = dev_spatial = dev_main
        print(f"仅有单卡或策略回退,所有模型将挤在 {dev_main} 上运行。")

    checkpoint_path = "/home/dataset-assist-0/usr/lh/ysh/dw/RL/AR/checkpoints/20260608_011428/model_step_1500.pth"
    archive_path = "/home/dataset-assist-0/usr/lh/ysh/dw/RL/AR/data/shards/shard_0000.tar.zst"
    clip_path = "/home/dataset-assist-0/usr/lh/ysh/dw/RL/AR/pretrain/clip-vit-base-patch32"
    vis_base_dir = "/home/dataset-assist-0/usr/lh/ysh/dw/RL/AR/mesh_vis_results"
    
    # 1. 初始化模型并发送到指定卡上
    vae = TripoSGVaeWrapper(device=dev_vae)
    vae.eval()
    
    text_encoder = FrozenCLIPTextEncoder(clip_path, device=dev_clip)
    temporal_model = TemporalTrajectoryPredictor(num_tokens=512, token_dim=64, max_seq_len=16).to(dev_temporal)
    spatial_model = SpatialGraphRefiner(num_tokens=512, token_dim=64, text_dim=64).to(dev_spatial)
    text_proj = TextProjectionLayer(in_dim=512, out_dim=64).to(dev_clip)
    
    # 加载权重 (先加载至CPU,避免加载时在主卡产生双倍显存突刺)
    ckpt = torch.load(checkpoint_path, map_location="cpu")
    temporal_model.load_state_dict(ckpt['temporal'])
    spatial_model.load_state_dict(ckpt['spatial'])
    text_proj.load_state_dict(ckpt['proj'])
    
    temporal_model.eval()
    spatial_model.eval()
    text_proj.eval()

    # 准备数据
    dataset = get_full_trajectories(archive_path, limit=400)
    metrics = {"mae_r2r": [], "mae_raw2r": [], "cd": [], "fscore": []}

    print(f"开始测试,共 {len(dataset)} 条轨迹...")

    for traj_idx, traj_data in enumerate(tqdm(dataset)):
        if traj_idx % 25 == 0 and traj_idx >= 1:
            # 创建输出文件夹
            traj_vis_dir = os.path.join(vis_base_dir, f"{traj_idx:04d}")
            os.makedirs(traj_vis_dir, exist_ok=True)

            vertices_seq = traj_data['vertices'][:16]
            faces = traj_data['faces']
            caption = traj_data.get('caption')
            
            # 1. 编码所有GT帧到 Latent 以及处理文本特征
            with torch.no_grad():
                gt_latents = []
                for t in range(16):
                    v_t = torch.from_numpy(vertices_seq[t]).to(device=dev_vae, dtype=torch.float32)
                    f_t = torch.from_numpy(faces).to(device=dev_vae, dtype=torch.float32)
                    l = vae.encode_mesh(v_t, f_t)
                    gt_latents.append(l.squeeze(0))
                gt_latents = torch.stack(gt_latents, dim=0) # [16, 512, 64] (当前在 dev_vae 即 cuda:0)
                
                # 映射文本特征 (在指定的 dev_clip 卡上完成)
                text_embed = text_proj(text_encoder([caption])).unsqueeze(1) # [1, 1, 64]

            # 2. 自回归预测 (进行精细的跨卡张量传递)
            current_context = gt_latents[0:1].unsqueeze(0).to(dev_temporal) # [1, 1, 512, 64]
            
            # 保存用作后面解码的预测隐变量列表 (统一放在负责解码的 dev_vae 也就是 cuda:0 上)
            pred_latents = [gt_latents[0:1].unsqueeze(0).to(dev_vae)]
            
            for t in range(15):
                # 步骤 A: 时间序列模型自回归预测 (在大显存卡 dev_temporal 执行)
                text_embed_to_spatial = text_embed.to(dev_temporal)
                next_t = temporal_model(current_context, text_embed_to_spatial) # [1, 1, 512, 64]
                next_t = next_t[:, -1:, :, :]
                
                # 步骤 B: 传递数据到分流卡进行空间细化 (转移至 dev_spatial)
                text_embed_to_spatial = text_embed.to(dev_spatial)
                next_t_to_spatial = next_t.to(dev_spatial)
                
                spatial_mask = torch.zeros((1, 512, 512), device=dev_spatial)
                
                next_t_refined = spatial_model(
                    next_t_to_spatial, 
                    text_embed_to_spatial, 
                    spatial_mask
                )
                next_t_refined = next_t_refined[:, -1:, :, :]
                
                # 步骤 C: 将精细化后的隐变量送回负责解码的卡上备份
                pred_latents.append(next_t_refined.to(dev_vae))
                
                # 同时更新自回归上下文 (送回 dev_temporal 卡)
                next_t_to_temporal = next_t_refined.to(dev_temporal)
                current_context = torch.cat([current_context, next_t_to_temporal], dim=1)

            # 3. 处理第 0 帧 (GT 帧) 的可视化
            with torch.no_grad():
                mesh_gt_0 = trimesh.Trimesh(vertices=vertices_seq[0], faces=faces, process=False)
                save_mesh_as_png(mesh_gt_0, os.path.join(traj_vis_dir, f"{0:03d}.png"))

            # 4. 解码并计算指标 (在 dev_vae 即大显存卡 cuda:0 上运行)
            for t in range(1, 16):
                latent_t = pred_latents[t].squeeze(1).to(dev_vae)
                geometric_func = lambda x: vae.decode_latent(latent_t, sampled_points=x)
                output_geometry = hierarchical_extract_geometry(
                    geometric_func, device=dev_vae, 
                    bounds=(-1.005, -1.005, -1.005, 1.005, 1.005, 1.005)
                )
                
                recon_v, recon_f = output_geometry[0]
                mesh_recon = trimesh.Trimesh(vertices=recon_v, faces=recon_f, process=False)
                
                # 可视化并保存当前生成的 mesh 帧图片
                save_mesh_as_png(mesh_recon, os.path.join(traj_vis_dir, f"{t:03d}.png"))

                mesh_gt = trimesh.Trimesh(vertices=vertices_seq[t], faces=faces, process=False)
                
                pts_gt, _ = trimesh.sample.sample_surface(mesh_gt, 2048)
                pts_recon, _ = trimesh.sample.sample_surface(mesh_recon, 2048)
                
                tree_gt = KDTree(pts_gt)
                tree_recon = KDTree(pts_recon)
                
                d_r2r = np.mean(tree_gt.query(pts_recon, k=1)[0])
                d_raw2r = np.mean(tree_recon.query(pts_gt, k=1)[0])
                cd = d_r2r + d_raw2r
                
                tau = 0.05
                precision = np.mean(tree_gt.query(pts_recon, k=1)[0] < tau)
                recall = np.mean(tree_recon.query(pts_gt, k=1)[0] < tau)
                f_score = (2 * precision * recall) / (precision + recall) if (precision + recall) > 0 else 0.0
                
                metrics["mae_r2r"].append(d_r2r)
                metrics["mae_raw2r"].append(d_raw2r)
                metrics["cd"].append(cd)
                metrics["fscore"].append(f_score)
                for k, v in metrics.items():
                    print(f"{k}: {np.mean(v):.6f}")

    print("\n--- 测试结果 ---")
    for k, v in metrics.items():
        print(f"{k}: {np.mean(v):.6f}")

if __name__ == "__main__":
    evaluate()