Diffusers
Safetensors
File size: 8,156 Bytes
f0fc238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import io
import os
import tarfile
import pickle
import zstandard
import matplotlib.pyplot as plt
import numpy as np
import torch
import trimesh

# 引入刚才写好的 VAE 包装类
from models.vae import TripoSGVaeWrapper
# 引入 TripoSG 官方提供的层次化几何提取工具
from triposg.inference_utils import hierarchical_extract_geometry

# 配置路径
archive_path = "/home/dataset-assist-0/usr/lh/ysh/dw/RL/AR/data/DyMesh_50000v_16f_0001_part_00"
triposg_weights = "/home/dataset-assist-0/usr/lh/ysh/dw/RL/AR/TripoSG/pretrained_weights/TripoSG"
output_image_path = "vae_reconstruction_test.png"

def get_first_mesh_frame(archive_file):
    """从数据集中提取第一个有效物体的第0帧"""
    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 member.isfile():
                        f = tar.extractfile(member)
                        if f is not None:
                            data = pickle.load(io.BytesIO(f.read()))
                            if isinstance(data, dict) and 'vertices' in data and 'faces' in data:
                                if data['vertices'].shape[0] == 16:
                                    # 提取第0帧 (形状为 [V, 3]) 和 共享的面片
                                    v_f0 = data['vertices'][0]
                                    faces = data['faces']
                                    short_name = os.path.basename(member.name).split('.')[0]
                                    return v_f0, faces, short_name
    raise RuntimeError("未在压缩包中找到合法的 16帧 动态 Mesh 数据。")

if __name__ == "__main__":
    device = "cuda" if torch.cuda.is_available() else "cpu"
    dtype = torch.float16

    # 1. 读取原数据并抽取静态帧
    print("📦 正在从数据包中读取测试 Mesh...")
    raw_vertices, raw_faces, obj_name = get_first_mesh_frame(archive_path)
    print(f"成功提取物体 [{obj_name}] 的第一帧. 顶点数: {raw_vertices.shape[0]}, 面数: {raw_faces.shape[0]}")

    # 2. 对原网格在内存中做归一化,以便后续做对齐可视化
    mesh_raw = trimesh.Trimesh(vertices=raw_vertices, faces=raw_faces, process=False)
    max_edge = np.max(mesh_raw.bounding_box.extents)
    mesh_raw.vertices = (mesh_raw.vertices - mesh_raw.center_mass) * (2.0 / max_edge)

    # 3. 初始化 VAE 包装模型
    print("🔮 正在初始化冻结的 TripoSG VAE 模型...")
    vae_wrapper = TripoSGVaeWrapper(weights_dir=triposg_weights, device=device, dtype=dtype)

    # 4. 编码 (Mesh -> Latent Code)
    # 修改 3. 编码部分的调用逻辑:
    print("🚀 正在将 Mesh 表面密集点云编码至隐空间...")
    # 将 numpy 数组显式转换为 torch.Tensor
    v_tensor = torch.from_numpy(raw_vertices)
    f_tensor = torch.from_numpy(raw_faces)
    
    latent_code = vae_wrapper.encode_mesh(v_tensor, f_tensor, num_samples=204800)
    print(f"编码成功!隐特征张量形状: {latent_code.shape}")

    # 5. 解码重建 (Latent Code -> SDF -> Marching Cubes Mesh)
    print("📐 正在通过层次化八叉树提取器(Hierarchical Octree)解码并重建 Mesh...")
    geometric_func = lambda x: vae_wrapper.decode_latent(latent_code, sampled_points=x)
    
    output_geometry = hierarchical_extract_geometry(
        geometric_func,
        device=device,
        bounds=(-1.005, -1.005, -1.005, 1.005, 1.005, 1.005),
        dense_octree_depth=8,         # 密度深度
        hierarchical_octree_depth=9,  # 恢复细节的八叉树总深度
    )
    
    # 将提取出的解算数据转换为 trimesh 对象
    recon_v, recon_f = output_geometry[0]
    mesh_recon = trimesh.Trimesh(vertices=recon_v.astype(np.float32), faces=recon_f, process=False)
    print(f"重建成功!重建后 Mesh 顶点数: {mesh_recon.vertices.shape[0]}, 面数: {mesh_recon.faces.shape[0]}")

    # 🌟 5.5 进行几何指标定量评估 (Evaluation Metric) - 使用 Scipy KDTree 修复版本
    print("📊 正在执行 KDTree 算法,评测重建精度指标...")
    from scipy.spatial import KDTree
    num_eval_samples = 50000
    
    # 在两个几何表面分别均匀采样 5万 个测试点
    pts_raw, _ = trimesh.sample.sample_surface(mesh_raw, num_eval_samples)
    pts_recon, _ = trimesh.sample.sample_surface(mesh_recon, num_eval_samples)
    
    # 建立空间平衡二叉树,用于秒级检索最近邻点
    tree_raw = KDTree(pts_raw)
    tree_recon = KDTree(pts_recon)
    
    # 1. 计算重建点到真值点云的最近距离 (Recon -> Raw) -> 代表重建偏离误差
    dists_recon_to_raw, _ = tree_raw.query(pts_recon, k=1)
    
    # 2. 计算真值点到重建点云的最近距离 (Raw -> Recon) -> 代表细节遗漏误差
    dists_raw_to_recon, _ = tree_recon.query(pts_raw, k=1)
    
    # 计算均值
    mae_recon_to_raw = np.mean(dists_recon_to_raw)
    mae_raw_to_recon = np.mean(dists_raw_to_recon)
    
    # 模拟双向倒角距离 (Chamfer Distance)
    chamfer_dist_approx = mae_recon_to_raw + mae_raw_to_recon
    
    # 计算在 1% 空间尺度阈值下的 F-Score
    obj_scale = np.linalg.norm(mesh_raw.bounding_box.extents)
    tau = 0.01 * obj_scale  # 阈值设置为物体对角线长度的 1%
    
    precision = np.mean(dists_recon_to_raw < tau)
    recall = np.mean(dists_raw_to_recon < tau)
    f_score = (2 * precision * recall) / (precision + recall) if (precision + recall) > 0 else 0.0

    # 打印评估报告
    print("\n" + "="*50)
    print("📈 VAE Mesh 自编码重建评测报告 (KDTree 加速版):")
    print("="*50)
    print(f"1. 重建偏离误差 (Recon -> Raw MAE): {mae_recon_to_raw:.6f} (越小越好)")
    print(f"2. 细节遗漏误差 (Raw -> Recon MAE): {mae_raw_to_recon:.6f} (越小越好)")
    print(f"3. 倒角距离指标 (Chamfer Distance): {chamfer_dist_approx:.6f} (越小越好)")
    print(f"4. 表面对齐分数 (F-Score @ 1%):     {f_score * 100:.2f}% (越大越好)")
    print("="*50 + "\n")

    # 6. 使用 Matplotlib 绘制同视角对比图
    print("🎨 正在绘制原网格与 VAE 重建网格的对比效果图...")
    fig = plt.figure(figsize=(14, 6))
    
    # 共同视角参数控制,防止发生视差偏离
    all_points = np.concatenate([mesh_raw.vertices, mesh_recon.vertices], axis=0)
    mid_x, mid_y, mid_z = (all_points.max(axis=0) + all_points.min(axis=0)) / 2.0
    max_range = (all_points.max(axis=0) - all_points.min(axis=0)).max() / 2.0

    # 子图 1:原始 Mesh(归一化后)
    ax1 = fig.add_subplot(1, 2, 1, projection='3d')
    ax1.plot_trisurf(mesh_raw.vertices[:, 0], mesh_raw.vertices[:, 1], mesh_raw.vertices[:, 2], 
                     triangles=mesh_raw.faces, cmap='coolwarm', edgecolor='none', alpha=0.7)
    ax1.set_title(f"Original Mesh (Normalized)\n({obj_name})", fontsize=12, weight='bold')
    ax1.set_xlim(mid_x - max_range, mid_x + max_range)
    ax1.set_ylim(mid_y - max_range, mid_y + max_range)
    ax1.set_zlim(mid_z - max_range, mid_z + max_range)
    ax1.axis('off')
    ax1.view_init(elev=20, azim=45) # 设定固定视角

    # 子图 2:VAE 解码重建的 Mesh
    ax2 = fig.add_subplot(1, 2, 2, projection='3d')
    ax2.plot_trisurf(mesh_recon.vertices[:, 0], mesh_recon.vertices[:, 1], mesh_recon.vertices[:, 2], 
                     triangles=mesh_recon.faces, cmap='viridis', edgecolor='none', alpha=0.7)
    # 在标题上动态附带上 F-Score 信息,方便肉眼和指标联动对照
    ax2.set_title(f"Reconstructed Mesh\n(F-Score: {f_score * 100:.2f}%)", fontsize=12, weight='bold')
    ax2.set_xlim(mid_x - max_range, mid_x + max_range)
    ax2.set_ylim(mid_y - max_range, mid_y + max_range)
    ax2.set_zlim(mid_z - max_range, mid_z + max_range)
    ax2.axis('off')
    ax2.view_init(elev=20, azim=45) # 设定相同视角

    plt.tight_layout()
    plt.savefig(output_image_path, dpi=150, bbox_inches='tight')
    plt.close()
    print(f"💾 对比可视化结果已成功保存至: {output_image_path}")