"""fix3 tokenizer(已融合 numba 加速实现, 原 *_fast.py 并入本文件)。 encode_mesh / decode_tokens 走 numpy 向量化 + numba 枚举, 比纯 python 版快约 5x。 quantized_mesh 与 decode 输出和纯 python 版完全一致; token 的 face 块集合一致, 仅少数平局块顺序不同(本实现补了 (f2.y,f2.x) 决定性平局键, 比原版更可复现)。 原文件头: fix3: 连接矩阵改为「octant 绝对原位 8x8 + 因子分解(按行)」编码,保留 F。 相对 fix2 的核心变化 -------------------- 1. 不再按 F 排序、不再受 max_children 限制。每个连接矩阵(intra / inter)统一是 8x8 的「octant 绝对原位」矩阵:行/列 = octant 0..7 的固定语义。 → 天然 mc=8 无损,"超标 / 合并 / 退化分摊" 这套概念全部消失。 2. 8x8 矩阵不再编码成单个 token(那是 2^64 词表),而是按行拆成 8 个「行子码」, 每个行子码 = 该行 8 个 bit ∈ [0, 256)。intra / inter 各 8 个子码。 3. **逻辑序列长度不变**:每个 face 仍是 18 个「位置」(9 coord + 3 F + 3 intra + 3 inter), 只是 intra / inter 这 6 个位置每个携带 8 个子码(给因子分解头:同一位置 8 个并行 256-way 头)。 coord / F 仍是单子码位置。tokenizer.encode 返回 List[List[int]]:每个内层 list = 一个位置的子码。 4. 保留 F(标识 active octant)。重建时用 F mask 掉非 active 行列(M &= F⊗F),消除歧义。 词表(极小):pad(1) + coord(resolution) + F(256) + intra_row(256) + inter_row(256)。 """ import numpy as np import trimesh from tqdm import tqdm import os import sys import glob sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from pathlib import Path from utils.data_utils import undiscretize, discretize, normalize, clean_mesh from utils.mesh_metric import compute_mesh_metrics from modules.fast_octree_level import _triangles_csr import itertools from dataclasses import dataclass NUM_OCTANTS = 8 # 预计算 24 个节点(3 槽位 x 8 octant)中选 3 个的所有组合,用于向量化找三角形 _TRI_INDICES = np.array(list(itertools.combinations(range(24), 3))) _IDX_I = _TRI_INDICES[:, 0] _IDX_J = _TRI_INDICES[:, 1] _IDX_K = _TRI_INDICES[:, 2] # ========================================== # Tokenizer:octant 原位 + 按行子码 # ========================================== class DynamicMeshTokenizer: """ encode 输出格式(fine):List[List[int]],每个内层 list 是一个「逻辑位置」的子码。 每 face 18 个位置: [v0x][v0y][v0z][v1x][v1y][v1z][v2x][v2y][v2z] (9 个 coord, 各 1 子码) [F0][F1][F2] (3 个 F, 各 1 子码) [M00 的 8 行][M11..][M22..] (3 个 intra, 各 8 子码) [M01 的 8 行][M02..][M12..] (3 个 inter, 各 8 子码) coord / F 位置长度 1;intra / inter 位置长度 8(行子码)。逻辑序列长度 = 18/face,与 fix2 相同。 """ def __init__(self, resolution, max_children=NUM_OCTANTS): self.resolution = resolution self.max_children = NUM_OCTANTS # octant 原位下固定为 8;保留字段仅为兼容 config self.pad_id = 0 self.coord_offset = 1 self.f_offset = self.coord_offset + self.resolution self.row_vocab = 1 << NUM_OCTANTS # 2^8 = 256 self.f_vocab = 1 << NUM_OCTANTS # F 也是 8bit self.intra_row_offset = self.f_offset + self.f_vocab self.inter_row_offset = self.intra_row_offset + self.row_vocab self.vocab_size = self.inter_row_offset + self.row_vocab # ---- bit <-> int ---- def _row_to_int(self, bits): v = 0 for i, b in enumerate(bits): if b: v |= (1 << i) return v def _int_to_row(self, val): return [(val >> i) & 1 for i in range(NUM_OCTANTS)] def _mat_to_rows(self, M, offset): return [self._row_to_int(M[r]) + offset for r in range(NUM_OCTANTS)] def _rows_to_mat(self, rows, offset): return [self._int_to_row(rows[r] - offset) for r in range(NUM_OCTANTS)] # ------------------------------------ # 细分网格 (Fine Mesh) —— 扁平 1D int 布局,每 face 60 个 token: # 9 coord + 3 F + 6 矩阵 x 8 行子码 # 顺序: [coord(9)] [F0 F1 F2] [M00 M11 M22 各 8 行] [M01 M02 M12 各 8 行] # F: 每个 1 个 token;intra/inter: 每个矩阵按行拆成 8 个行子码(每行 8bit) # 所有真实 token 都带 offset(>=1),0 仅作 trailing pad,所以 F 不需要复制避坑。 # 与现有 dataset/collate(1D int, pad_id=0) 兼容;模型侧整理成 9 个 refine 位置/face # (F 位置 1 子码、M 位置 8 行子码 sum 成一个 embedding token),逻辑序列 18/face 不变。 # ------------------------------------ TOKENS_PER_FACE = 9 + 3 + 6 * NUM_OCTANTS # 60 REFINE_POS_PER_FACE = 9 # 模型逻辑: 3 F + 3 intra + 3 inter def encode(self, faces_data, use_hourglass=True): seq = [] for face in faces_data: v0, v1, v2, F0, F1, F2, M00, M11, M22, M01, M02, M12 = face for v in (v0, v1, v2): seq.extend([v[0] + self.coord_offset, v[1] + self.coord_offset, v[2] + self.coord_offset]) for F in (F0, F1, F2): seq.append(self._row_to_int(F) + self.f_offset) # F: 1 个 token for M in (M00, M11, M22): seq.extend(self._mat_to_rows(M, self.intra_row_offset)) # 各 8 行 for M in (M01, M02, M12): seq.extend(self._mat_to_rows(M, self.inter_row_offset)) # 各 8 行 return seq def decode(self, seq): faces_data = [] B = self.TOKENS_PER_FACE # 60 clean_seq = [t for t in seq if t != self.pad_id] co = self.coord_offset for i in range(0, len(clean_seq) - B + 1, B): ch = clean_seq[i:i + B] cv = [ch[k] - co for k in range(9)] v0 = (cv[0], cv[1], cv[2]); v1 = (cv[3], cv[4], cv[5]); v2 = (cv[6], cv[7], cv[8]) F0 = self._int_to_row(ch[9] - self.f_offset) F1 = self._int_to_row(ch[10] - self.f_offset) F2 = self._int_to_row(ch[11] - self.f_offset) M00 = self._rows_to_mat(ch[12:20], self.intra_row_offset) M11 = self._rows_to_mat(ch[20:28], self.intra_row_offset) M22 = self._rows_to_mat(ch[28:36], self.intra_row_offset) M01 = self._rows_to_mat(ch[36:44], self.inter_row_offset) M02 = self._rows_to_mat(ch[44:52], self.inter_row_offset) M12 = self._rows_to_mat(ch[52:60], self.inter_row_offset) faces_data.append((v0, v1, v2, F0, F1, F2, M00, M11, M22, M01, M02, M12)) return faces_data # ------------------------------------ # 粗糙网格 (Raw Mesh):仅顶点坐标,每 face 9 个单子码位置 # ------------------------------------ def encode_raw(self, raw_faces_data, use_hourglass=True): seq = [] for face in raw_faces_data: v0, v1, v2 = face for v in (v0, v1, v2): seq.append([v[0] + self.coord_offset]) seq.append([v[1] + self.coord_offset]) seq.append([v[2] + self.coord_offset]) return seq def decode_raw(self, seq): faces_data = [] clean_seq = [p for p in seq if not (len(p) == 1 and p[0] == self.pad_id)] for i in range(0, len(clean_seq) - 9 + 1, 9): chunk = clean_seq[i:i + 9] if len(chunk) < 9: break cv = [chunk[k][0] - self.coord_offset for k in range(9)] faces_data.append(((cv[0], cv[1], cv[2]), (cv[3], cv[4], cv[5]), (cv[6], cv[7], cv[8]))) return faces_data # ========================================== # Builder:直接构建 octant 原位 8x8 矩阵 # ========================================== class OctreeFaceTokenBuilder: def __init__(self, tree, max_children=NUM_OCTANTS): self.tree = tree self.max_children = NUM_OCTANTS # octant 原位固定 8 def get_level_info(self, depth): nodes_at_depth = {} def collect_nodes(current, target_depth): if current.depth == target_depth: nodes_at_depth[current.coord] = current return for child in current.children: if child: collect_nodes(child, target_depth) collect_nodes(self.tree.root, depth) coords = sorted(nodes_at_depth.keys(), key=lambda x: (x[2], x[1], x[0])) coord_to_idx = {c: i for i, c in enumerate(coords)} adj = {i: set() for i in range(len(coords))} for i, c in enumerate(coords): node = nodes_at_depth[c] all_neighbors = node.intra_neighbors | node.inter_neighbors for neighbor_coord in all_neighbors: if neighbor_coord in coord_to_idx: j = coord_to_idx[neighbor_coord] if i < j: adj[i].add(j) raw_faces_set = set() covered_edges = set() # 1. 3-clique (三角形) for u in range(len(coords)): for v in adj[u]: common_neighbors = adj[u] & adj[v] for w in common_neighbors: face_coords = tuple(sorted([coords[u], coords[v], coords[w]])) raw_faces_set.add(face_coords) covered_edges.add(tuple(sorted([face_coords[0], face_coords[1]]))) covered_edges.add(tuple(sorted([face_coords[1], face_coords[2]]))) covered_edges.add(tuple(sorted([face_coords[0], face_coords[2]]))) # 2. 未被三角形覆盖的独立边 -> 退化面片 (A, B, B) for u in range(len(coords)): for v in adj[u]: edge = tuple(sorted([coords[u], coords[v]])) if edge not in covered_edges: raw_faces_set.add((edge[0], edge[1], edge[1])) # 3. 孤立顶点 -> (A, A, A) for u in range(len(coords)): has_edges = len(adj[u]) > 0 or any(u in adj[i] for i in range(u)) if not has_edges: raw_faces_set.add((coords[u], coords[u], coords[u])) raw_faces_at_depth = list(raw_faces_set) raw_faces_at_depth.sort(key=lambda f: (f[0][2], f[0][1], f[0][0], f[1][2], f[1][1], f[1][0], f[2][2])) return raw_faces_at_depth, nodes_at_depth def _active_octants(self, coord, n, nodes_map): """该节点哪些 octant 子节点存在(occupancy)。 用 children 占据而非"有连接"标记 —— 因为「有连接 ⟹ 必有子节点」,occupancy 是 "有连接"的超集,且正好等于 raw 的 get_level_info 收集的 depth+1 节点集; 这样「有几何但无边」的孤立子节点也会被 F 标记,fine 重构能补回孤立点,与 raw 完全一致。 M(连接矩阵)不变,故三角形/边重构不受影响。""" return set(i for i in range(NUM_OCTANTS) if n.children[i] is not None) def encode_level_tokens(self, depth): raw_faces, nodes_map = self.get_level_info(depth) empty = np.zeros((NUM_OCTANTS, NUM_OCTANTS), dtype=np.int8) # F:每个节点的 active octant 掩码 (8bit) node_F = {} for coord, n in nodes_map.items(): active = self._active_octants(coord, n, nodes_map) node_F[coord] = [1 if i in active else 0 for i in range(NUM_OCTANTS)] intra_cache = {} def intra8(coord): if coord not in intra_cache: M = (nodes_map[coord].intra_matrix != 0).astype(np.int8) # 8x8 octant 原位、对称 intra_cache[coord] = M.tolist() return intra_cache[coord] def inter8(a, b): if a == b: return intra8(a) na = nodes_map[a] M = na.inter_matrices.get(b) if M is None: nb = nodes_map[b] Mt = nb.inter_matrices.get(a) M = Mt.T if Mt is not None else empty return (np.asarray(M) != 0).astype(np.int8).tolist() raw_tokens = [] output_tokens = [] for face in raw_faces: v0_c, v1_c, v2_c = face raw_tokens.append([v0_c, v1_c, v2_c]) F0, F1, F2 = node_F[v0_c], node_F[v1_c], node_F[v2_c] M00, M11, M22 = intra8(v0_c), intra8(v1_c), intra8(v2_c) M01 = inter8(v0_c, v1_c) M02 = inter8(v0_c, v2_c) M12 = inter8(v1_c, v2_c) output_tokens.append((v0_c, v1_c, v2_c, F0, F1, F2, M00, M11, M22, M01, M02, M12)) return raw_tokens, output_tokens class OctreeNode: def __init__(self, x, y, z, depth): self.coord = (x, y, z) self.depth = depth self.children = [None] * 8 self.intra_neighbors = set() self.inter_neighbors = set() self.intra_matrix = np.zeros((8, 8), dtype=np.int8) self.inter_matrices = {} class MatrixOctree: def __init__(self, max_depth=7): self.max_depth = max_depth self.root = None def _get_parent_coord(self, coord): return (coord[0] >> 1, coord[1] >> 1, coord[2] >> 1) def _get_local_index(self, coord): return ((coord[2] & 1) << 2) | ((coord[1] & 1) << 1) | (coord[0] & 1) def build(self, vertices, faces): resolution = 2 ** self.max_depth quantized = discretize(vertices, num_discrete=resolution) leaf_nodes_map = {} for i in range(len(vertices)): coord = tuple(quantized[i]) if coord not in leaf_nodes_map: leaf_nodes_map[coord] = OctreeNode(coord[0], coord[1], coord[2], self.max_depth) for face in faces: coords = [tuple(quantized[i]) for i in face] for i in range(3): u, v = coords[i], coords[(i + 1) % 3] if u == v: continue parent_u = self._get_parent_coord(u) parent_v = self._get_parent_coord(v) node_u = leaf_nodes_map[u] node_v = leaf_nodes_map[v] if parent_u == parent_v: node_u.intra_neighbors.add(v) node_v.intra_neighbors.add(u) else: node_u.inter_neighbors.add(v) node_v.inter_neighbors.add(u) current_layer_nodes = list(leaf_nodes_map.values()) for d in range(self.max_depth, 0, -1): parent_layer_map = {} for child in current_layer_nodes: parent_coord = self._get_parent_coord(child.coord) if parent_coord not in parent_layer_map: parent_layer_map[parent_coord] = OctreeNode(*parent_coord, d - 1) parent_node = parent_layer_map[parent_coord] idx = self._get_local_index(child.coord) parent_node.children[idx] = child for child in current_layer_nodes: parent_coord = self._get_parent_coord(child.coord) parent_node = parent_layer_map[parent_coord] child_idx = self._get_local_index(child.coord) for sibling_coord in child.intra_neighbors: sibling_idx = self._get_local_index(sibling_coord) parent_node.intra_matrix[child_idx, sibling_idx] = 1 for neighbor_coord in child.inter_neighbors: neighbor_parent_coord = self._get_parent_coord(neighbor_coord) if neighbor_parent_coord not in parent_node.inter_matrices: parent_node.inter_matrices[neighbor_parent_coord] = np.zeros((8, 8), dtype=np.int8) neighbor_child_idx = self._get_local_index(neighbor_coord) parent_node.inter_matrices[neighbor_parent_coord][child_idx, neighbor_child_idx] = 1 grandparent_p = self._get_parent_coord(parent_coord) grandparent_n = self._get_parent_coord(neighbor_parent_coord) if grandparent_p == grandparent_n: parent_node.intra_neighbors.add(neighbor_parent_coord) else: parent_node.inter_neighbors.add(neighbor_parent_coord) current_layer_nodes = list(parent_layer_map.values()) if current_layer_nodes: self.root = current_layer_nodes[0] def reconstruct_next_level_optimized(output_tokens, resolution, max_children=NUM_OCTANTS): """octant 绝对原位重建:M 已是 8x8、行列=octant;用 F mask 掉非 active 行列。""" indices = np.arange(8) offsets = np.stack([indices & 1, (indices >> 1) & 1, (indices >> 2) & 1], axis=1).astype(np.int32) tri_blocks = [] # 逐 face 向量化收集的三角形 (N,3,3),最后统一去重 edge_keys = set() # 全局所有边: canonical (key_a, key_b) tri_edge_keys = set() # 被三角形覆盖的边(全局) active_keys = set() # 全局 active 节点 key key_to_coord = {} # key(int 三元组) -> np.int32 坐标(3,) for f_token in output_tokens: v0_c, v1_c, v2_c, F0, F1, F2, M00, M11, M22, M01, M02, M12 = f_token fa = [np.asarray(F0, dtype=bool), np.asarray(F1, dtype=bool), np.asarray(F2, dtype=bool)] def intra(F, M): return np.asarray(M, dtype=bool) & np.outer(F, F) def inter(Fa, Fb, M): return np.asarray(M, dtype=bool) & np.outer(Fa, Fb) in0 = intra(fa[0], M00); in1 = intra(fa[1], M11); in2 = intra(fa[2], M22) m01 = inter(fa[0], fa[1], M01); m02 = inter(fa[0], fa[2], M02); m12 = inter(fa[1], fa[2], M12) A = np.zeros((24, 24), dtype=bool) A[0:8, 0:8] = in0 | in0.T A[8:16, 8:16] = in1 | in1.T A[16:24, 16:24] = in2 | in2.T A[0:8, 8:16] = m01; A[8:16, 0:8] = m01.T A[0:8, 16:24] = m02; A[16:24, 0:8] = m02.T A[8:16, 16:24] = m12; A[16:24, 8:16] = m12.T # 24 个节点(slot p, octant c)的世界坐标 = v_parents[p]*2 + offset[c] v_parents_arr = np.stack([np.array(v0_c[:3], dtype=np.int32), np.array(v1_c[:3], dtype=np.int32), np.array(v2_c[:3], dtype=np.int32)]) node_coords = v_parents_arr[np.arange(24) // 8] * 2 + offsets[np.arange(24) % 8] # (24,3) keys = [tuple(int(x) for x in node_coords[i]) for i in range(24)] for i in range(24): if keys[i] not in key_to_coord: key_to_coord[keys[i]] = node_coords[i] # 三角形(逐 face 向量化);同时把三角形的边记入全局 covered 集合 valid_mask = A[_IDX_I, _IDX_J] & A[_IDX_J, _IDX_K] & A[_IDX_I, _IDX_K] tris = _TRI_INDICES[valid_mask] if len(tris) > 0: p_idx = tris // 8 c_idx = tris % 8 tri_blocks.append(v_parents_arr[p_idx] * 2 + offsets[c_idx]) for i, j, k in tris: ka, kb, kc = sorted((keys[int(i)], keys[int(j)], keys[int(k)])) tri_edge_keys.add((ka, kb)); tri_edge_keys.add((kb, kc)); tri_edge_keys.add((ka, kc)) # 全局收集所有边 ei, ej = np.where(np.triu(A, 1)) for i, j in zip(ei.tolist(), ej.tolist()): ki, kj = keys[i], keys[j] edge_keys.add((ki, kj) if ki <= kj else (kj, ki)) # 全局收集 active 节点 for i in np.where(np.concatenate(fa))[0].tolist(): active_keys.add(keys[i]) all_blocks = list(tri_blocks) # 未被任何三角形覆盖的边 -> 退化面 (A, B, B) [全局判断,与 raw 的 get_level_info 一致] for a, b in (edge_keys - tri_edge_keys): all_blocks.append(np.stack([key_to_coord[a], key_to_coord[b], key_to_coord[b]])[None]) # 孤立的 active 节点(全局无任何边) -> 点 (A, A, A) edge_nodes = set() for a, b in edge_keys: edge_nodes.add(a); edge_nodes.add(b) for a in (active_keys - edge_nodes): c = key_to_coord[a] all_blocks.append(np.stack([c, c, c])[None]) if not all_blocks: return np.array([]), np.array([]) flat_v = np.concatenate(all_blocks, axis=0).reshape(-1, 3) V, inv = np.unique(flat_v, axis=0, return_inverse=True) F_raw = inv.reshape(-1, 3) F_sorted = np.sort(F_raw, axis=1) _, unique_face_indices = np.unique(F_sorted, axis=0, return_index=True) F = F_raw[unique_face_indices] V = undiscretize(V, num_discrete=resolution) return V, F def reconstruct_base_mesh(output_tokens, resolution): all_face_coords = [] for f_token in output_tokens: v0_c, v1_c, v2_c = f_token c0 = np.array(v0_c[:3], dtype=np.int32) c1 = np.array(v1_c[:3], dtype=np.int32) c2 = np.array(v2_c[:3], dtype=np.int32) all_face_coords.append(np.stack([c0, c1, c2])) if not all_face_coords: return np.array([]), np.array([]) flat_v = np.concatenate(all_face_coords, axis=0).reshape(-1, 3) V, inv = np.unique(flat_v, axis=0, return_inverse=True) F_raw = inv.reshape(-1, 3) F_sorted = np.sort(F_raw, axis=1) _, unique_face_indices = np.unique(F_sorted, axis=0, return_index=True) F = F_raw[unique_face_indices] V = undiscretize(V, num_discrete=resolution) return V, F class MeshCodec: def __init__(self, args): self.max_depth = args.max_depth self.max_resolution = args.max_resolution self.use_hourglass = args.use_hourglass self.max_children = NUM_OCTANTS # octant 原位固定 8(忽略 config 的 max_children) self.tokenizer = DynamicMeshTokenizer(resolution=self.max_resolution) def encode_mesh(self, mesh: trimesh.Trimesh, target_depth: int, corrupt=None) -> tuple: if isinstance(mesh, trimesh.Scene): mesh = mesh.dump(concatenate=True) V = np.asarray(mesh.vertices, dtype=np.float64) res = 2 ** (target_depth + 1) v_restored = undiscretize(discretize(V, num_discrete=res), num_discrete=res) quantized_mesh = trimesh.Trimesh(vertices=v_restored, faces=mesh.faces, process=False) # corrupt(仅训练时由 dataset 传入)= 2a 暴露偏差腐蚀; None 时与原版逐位一致(推理不受影响) raw_seq, seq = _fast_fix3_level_tokens(V, mesh.faces, target_depth, self.tokenizer, corrupt=corrupt) return raw_seq, seq, quantized_mesh def decode_tokens(self, token_sequence: list, target_depth: int): if os.environ.get("FIX3_FAST_DECODE", "1") != "1": return super().decode_tokens(token_sequence, target_depth) if token_sequence is None or len(token_sequence) == 0: return None return _fast_decode_tokens(token_sequence, target_depth, self.tokenizer) def decode_raw_tokens(self, raw_token_sequence: list, target_depth: int) -> trimesh.Trimesh: if not raw_token_sequence: return None resolution = 2 ** (target_depth) raw_t = self.tokenizer.decode_raw(raw_token_sequence) v, f = reconstruct_base_mesh(raw_t, resolution) if len(v) == 0 or len(f) == 0: return None return trimesh.Trimesh(vertices=v, faces=f, process=False) def get_model_config(self) -> dict: t = self.tokenizer return { "total_vocab_size": t.vocab_size, "coord_vocab_size": t.resolution, "f_vocab_size": t.f_vocab, "row_vocab_size": t.row_vocab, # intra/inter 行子码各 256 "coord_offset": t.coord_offset, "f_offset": t.f_offset, "intra_row_offset": t.intra_row_offset, "inter_row_offset": t.inter_row_offset, "pad_token_id": t.pad_id, "num_octants": NUM_OCTANTS, "subcodes_per_position": NUM_OCTANTS, # 每个 refine 位置 8 个子码槽(F 复制;intra/inter 为 8 行) "tokens_per_face": DynamicMeshTokenizer.TOKENS_PER_FACE, # 60 扁平 token/face (9+3+6*8) "coord_per_face": 9, "refine_positions_per_face": DynamicMeshTokenizer.REFINE_POS_PER_FACE, # 9 (3F+3intra+3inter) "input_num_categories": t.resolution + 1, # 输出端:F / intra_row / inter_row 三类,每类 256;coord 为输入端条件 "output_num_categories": 1 + t.f_vocab + 2 * t.row_vocab, } # ========================================== # 自测:encode -> decode -> reconstruct 往返 # ========================================== if __name__ == "__main__": # ========================================== # 编解码 + 重构导出流程 (参照 fix2 的 __main__) # 每个 mesh: 导出 GT;对每个 depth 做 # encode -> decode_tokens(细分/Fine) / decode_raw_tokens(粗糙/Coarse) / 量化网格 # 并打印计时、token 数(60/face)与 token 级往返一致性。 # ========================================== import time # 1. 输入/输出文件夹 (按需修改) INPUT_DIRECTORY = "/root/junkai_lin/mul_res_diffusion/demo/complex_test_data/files" OUTPUT_DIRECTORY = "./output/fix3_octant_factorized2" os.makedirs(OUTPUT_DIRECTORY, exist_ok=True) @dataclass class MeshCodecArgs: max_depth: int = 10 max_resolution: int = 1024 use_hourglass: bool = True max_children: int = 8 # fix3 octant 原位固定 8 (此字段实际被忽略) # 2. 初始化 Codec args = MeshCodecArgs() codec = MeshCodec(args=args) print(f"[*] Initialized fix3 Codec. Total Vocab Size: {codec.tokenizer.vocab_size} " f"(tokens/face={DynamicMeshTokenizer.TOKENS_PER_FACE})") # 3. 收集所有 .obj / .glb input_dir_path = Path(INPUT_DIRECTORY) mesh_files = sorted(input_dir_path.rglob("*.obj")) + sorted(input_dir_path.rglob("*.glb")) if not mesh_files: print(f"[!] No .obj or .glb files found in {INPUT_DIRECTORY}") DEPTHS = [0, 1, 2, 3, 4, 5, 6] # 4. 逐文件处理 for idx, file_path in enumerate(mesh_files, 1): obj_name = file_path.stem print(f"\n{'=' * 50}") print(f"[{idx}/{len(mesh_files)}] Processing: {file_path.name}") print(f"{'=' * 50}") file_start_time = time.perf_counter() try: raw_mesh = trimesh.load(str(file_path), force="mesh") normalize(raw_mesh) except Exception as e: print(f"[!] load/normalize failed: {e}") continue # 导出 GT gt_path = os.path.join(OUTPUT_DIRECTORY, f"{obj_name}_gt.obj") raw_mesh.export(gt_path) print(f"[*] Saved GT -> {gt_path}") # 不同深度的编解码 for depth in DEPTHS: print(f"\n--- Depth {depth} | res(fine)={2 ** (depth + 1)} | octant 原位 ---") # Encoding t0 = time.perf_counter() raw_tokens, tokens, quantized_mesh = codec.encode_mesh(raw_mesh, target_depth=depth) encode_time = time.perf_counter() - t0 nface = len(tokens) // DynamicMeshTokenizer.TOKENS_PER_FACE print(f"[Timer] Encoding: {encode_time:.4f}s | flat_tokens={len(tokens)} " f"(nface={nface}, 模型逻辑位置={18 * nface})") # token 级往返一致性 (encode->decode->re-encode) same = (tokens == codec.tokenizer.encode(codec.tokenizer.decode(tokens))) print(f"[Check] token 往返一致: {same}") # Decoding (Fine) if len(tokens) > 0: t1 = time.perf_counter() recon_mesh = codec.decode_tokens(tokens, target_depth=depth) decode_fine_time = time.perf_counter() - t1 if recon_mesh is not None: out_path = os.path.join( OUTPUT_DIRECTORY, f"{obj_name}_recon_d{depth}_res{2 ** (depth + 1)}_fix3.obj") recon_mesh.export(out_path) print(f"[Timer] Decoding (Fine): {decode_fine_time:.4f}s " f"-> V={len(recon_mesh.vertices)} F={len(recon_mesh.faces)}") else: print(f"[Timer] Decoding (Fine): {decode_fine_time:.4f}s -> empty") # Decoding (Coarse / Raw) if len(raw_tokens) > 0: t2 = time.perf_counter() raw_recon_mesh = codec.decode_raw_tokens(raw_tokens, target_depth=depth) decode_coarse_time = time.perf_counter() - t2 if raw_recon_mesh is not None: raw_out_path = os.path.join( OUTPUT_DIRECTORY, f"{obj_name}_raw_recon_d{depth}_res{2 ** depth}.obj") raw_recon_mesh.export(raw_out_path) print(f"[Timer] Decoding (Coarse): {decode_coarse_time:.4f}s " f"-> V={len(raw_recon_mesh.vertices)} F={len(raw_recon_mesh.faces)}") # 量化网格 (输入离散化后的 GT,用于对照重构质量上限) if quantized_mesh is not None: quant_out_path = os.path.join( OUTPUT_DIRECTORY, f"{obj_name}_dis_d{depth + 1}_res{2 ** (depth + 1)}.obj") quantized_mesh.export(quant_out_path) file_total_time = time.perf_counter() - file_start_time print(f"\n[*] Finished {file_path.name} in {file_total_time:.2f}s") # ========================================================================== # encode_mesh / decode_tokens 的 numba 加速实现细节(原 *_fast.py 并入) # ========================================================================== _OCT_W = np.array([1, 2, 4], dtype=np.int64) # octant 局部索引 = x + 2y + 4z def _corrupt_fid_nodes(fid, nodes, und, nN, target_depth, corrupt): """2a 暴露偏差训练腐蚀(仅训练): 模拟级联实测的两类粗输入误差。 (a) 伪面: 在已存在的 GT 节点间加不该有的面(过连接)。下游打包按 fid 查表 -> 非边对的 inter 行自动全 0(empty),教模型"看 PC 把伪面 refine 成空"。 (b) 节点坐标抖动 ±jitter_max(位移): 只改 coord_part 的输入坐标,目标 F/intra/inter 仍按真节点身份查表不变,教模型"输入坐标偏了也 refine 对"。 目标无需重新导出 —— 全部沿用 fid 索引,与干净路径同一套向量化打包。返回 (fid_aug, nodes_coord)。 """ sp_rate = float(corrupt.get("spurious_rate", 0.0)) jp = float(corrupt.get("jitter_prob", 0.0)) if sp_rate > 0.0 and nN >= 3 and und.shape[0] > 0: radius = int(corrupt.get("spatial_radius", 2)) n_add = int(sp_rate * fid.shape[0]) if n_add > 0: coord2idx = {(int(c[0]), int(c[1]), int(c[2])): i for i, c in enumerate(nodes)} existing = set(map(tuple, np.sort(fid, axis=1).tolist())) ne = und.shape[0] spurious, tries, maxtries = [], 0, n_add * 6 while len(spurious) < n_add and tries < maxtries: tries += 1 ei = int(np.random.randint(ne)) ai, bi = int(und[ei, 0]), int(und[ei, 1]) base = nodes[ai] if np.random.random() < 0.5 else nodes[bi] off = np.random.randint(-radius, radius + 1, size=3) if not off.any(): continue ck = (int(base[0] + off[0]), int(base[1] + off[1]), int(base[2] + off[2])) ci = coord2idx.get(ck) if ci is None or ci == ai or ci == bi: continue tri = tuple(sorted((ai, bi, ci))) if tri in existing: continue existing.add(tri) spurious.append(tri) if spurious: fid = np.concatenate([fid, np.asarray(spurious, dtype=fid.dtype)], axis=0) nodes_coord = nodes if jp > 0.0: jmax = int(corrupt.get("jitter_max", 1)) res_c = 2 ** target_depth nodes_coord = nodes.copy() mask = np.random.random(nN) < jp k = int(mask.sum()) if k > 0: noise = np.random.randint(-jmax, jmax + 1, size=(k, 3)) nodes_coord[mask] = np.clip(nodes_coord[mask] + noise, 0, res_c - 1) return fid, nodes_coord def _fast_fix3_level_tokens(vertices, faces, target_depth, tok, corrupt=None): """等价替换 MatrixOctree(max_depth=td+1).build + encode_level_tokens(td) + tokenizer.encode/encode_raw。 返回 (raw_token_sequence, token_sequence),与原版逐位一致。""" res = 2 ** (target_depth + 1) Q = discretize(np.asarray(vertices, dtype=np.float64), num_discrete=res).astype(np.int64) faces = np.asarray(faces, dtype=np.int64).reshape(-1, 3) # ---- 节点(td) = 所有顶点叶子坐标的 parent;占据 F = 所有顶点的 octant ---- P_all = Q >> 1 i_all = (Q & 1) @ _OCT_W # ---- 叶子边表(每面 3 条,跳过零长边 u==v,与原版一致) ---- if faces.size: e = np.concatenate([faces[:, [0, 1]], faces[:, [1, 2]], faces[:, [2, 0]]], axis=0) u, v = Q[e[:, 0]], Q[e[:, 1]] keep = np.any(u != v, axis=1) u, v = u[keep], v[keep] else: u = v = np.zeros((0, 3), dtype=np.int64) pu, pv = u >> 1, v >> 1 iu = (u & 1) @ _OCT_W iv = (v & 1) @ _OCT_W # 节点表(np.unique 行字典序 = 坐标 (x,y,z) 升序 = 原版 face 内顶点 sorted() 同序) nodes, inv = np.unique(np.concatenate([P_all, pu, pv], axis=0), axis=0, return_inverse=True) nV, M = P_all.shape[0], pu.shape[0] occ_idx = inv[:nV] a, b = inv[nV:nV + M], inv[nV + M:] nN = nodes.shape[0] intra = a == b # ---- F 行子码(8bit int):所有顶点的 octant 占据 ---- Fbits = np.zeros(nN, dtype=np.int64) np.bitwise_or.at(Fbits, occ_idx, np.int64(1) << i_all) # ---- intra 行子码 [nN,8]:绝对原位、对称双向、无对角填充(与原版一致) ---- intra_rows = np.zeros((nN, 8), dtype=np.int64) ai, iui, ivi = a[intra], iu[intra], iv[intra] np.bitwise_or.at(intra_rows, (ai, iui), np.int64(1) << ivi) np.bitwise_or.at(intra_rows, (ai, ivi), np.int64(1) << iui) # ---- inter 行子码:有向 (a,b) 分组(双向都存,与原版 build 一致) ---- im = ~intra ea, eb, eiu, eiv = a[im], b[im], iu[im], iv[im] dkey = np.concatenate([ea * nN + eb, eb * nN + ea]) drow = np.concatenate([eiu, eiv]) dcol = np.concatenate([eiv, eiu]) ukeys, kinv = np.unique(dkey, return_inverse=True) inter_rows = np.zeros((max(len(ukeys), 1), 8), dtype=np.int64) if dkey.size: np.bitwise_or.at(inter_rows, (kinv, drow), np.int64(1) << dcol) # ---- 无向节点邻接 -> CSR -> numba 三角形枚举 ---- if ea.size: und = np.unique(np.stack([np.minimum(ea, eb), np.maximum(ea, eb)], axis=1), axis=0) else: und = np.zeros((0, 2), dtype=np.int64) deg = np.zeros(nN, dtype=np.int64) if und.size: np.add.at(deg, und[:, 0], 1) np.add.at(deg, und[:, 1], 1) order = np.lexsort((und[:, 1], und[:, 0])) fu, fv = und[order, 0], und[order, 1] indptr = np.zeros(nN + 1, dtype=np.int64) np.add.at(indptr, fu + 1, 1) indptr = np.cumsum(indptr) tu, tv, tw = _triangles_csr(indptr, fv.astype(np.int64)) else: tu = tv = tw = np.zeros(0, dtype=np.int64) # ---- faces_ids:三角形 + 未覆盖边退化 (A,B,B) + 孤立点 (A,A,A),集合与原版一致 ---- tris = np.stack([tu, tv, tw], axis=1) if tu.size else np.zeros((0, 3), dtype=np.int64) covered = set() for x, y, z in tris: x, y, z = int(x), int(y), int(z) covered.add((x, y)); covered.add((y, z)); covered.add((x, z)) extra = [] for x, y in und: x, y = int(x), int(y) if (x, y) not in covered: extra.append((x, y, y)) for x in np.flatnonzero(deg == 0): x = int(x) extra.append((x, x, x)) fid = np.concatenate([tris, np.asarray(extra, dtype=np.int64).reshape(-1, 3)], axis=0) if fid.shape[0] == 0: return [], [] # ---- 原版排序键 + 决定性平局键 ---- c0, c1, c2 = nodes[fid[:, 0]], nodes[fid[:, 1]], nodes[fid[:, 2]] order = np.lexsort((c2[:, 0], c2[:, 1], c2[:, 2], c1[:, 0], c1[:, 1], c1[:, 2], c0[:, 0], c0[:, 1], c0[:, 2])) # 主键=c0.z(最后) ... 与原版 7 键一致+补齐平局 fid = fid[order] nodes_coord = nodes if corrupt is not None: fid, nodes_coord = _corrupt_fid_nodes(fid, nodes, und, nN, target_depth, corrupt) c0, c1, c2 = nodes_coord[fid[:, 0]], nodes_coord[fid[:, 1]], nodes_coord[fid[:, 2]] # ---- 60/face 扁平 token 向量化打包 ---- # 注: F_part/intra_part/inter_part 下面仍用 fid(节点身份)查表 -> 腐蚀的伪面自动得正确目标 # (非边对 inter 全 0),坐标抖动只影响上面 coord_part,目标不变。 co, fo = tok.coord_offset, tok.f_offset io, eo = tok.intra_row_offset, tok.inter_row_offset nF = fid.shape[0] coord_part = np.concatenate([c0, c1, c2], axis=1) + co # (nF,9) F_part = np.stack([Fbits[fid[:, 0]], Fbits[fid[:, 1]], Fbits[fid[:, 2]]], axis=1) + fo intra_part = np.concatenate([intra_rows[fid[:, 0]], intra_rows[fid[:, 1]], intra_rows[fid[:, 2]]], axis=1) + io # (nF,24) inter_blocks = [] for x, y in ((0, 1), (0, 2), (1, 2)): A_, B_ = fid[:, x], fid[:, y] rows = np.zeros((nF, 8), dtype=np.int64) same = A_ == B_ rows[same] = intra_rows[A_[same]] # inter8(a,a)=intra8(a),与原版一致 ns = np.flatnonzero(~same) if ns.size: dk = A_[ns] * nN + B_[ns] pos = np.searchsorted(ukeys, dk) pos_c = np.minimum(pos, max(len(ukeys) - 1, 0)) found = (ukeys[pos_c] == dk) if len(ukeys) else np.zeros(ns.size, dtype=bool) rows[ns[found]] = inter_rows[pos_c[found]] # 未找到 -> 保持全 0(原版 empty 矩阵) inter_blocks.append(rows) inter_part = np.concatenate(inter_blocks, axis=1) + eo # (nF,24) token = np.concatenate([coord_part, F_part, intra_part, inter_part], axis=1) # (nF,60) token_sequence = token.ravel().tolist() raw_token_sequence = coord_part.reshape(-1, 1).tolist() # 9 coord/face, 各 [t] return raw_token_sequence, token_sequence _BIT_SHIFTS = np.arange(NUM_OCTANTS, dtype=np.int64) # _int_to_row: bit i = (v>>i)&1 _NODE_PARENT = np.arange(24) // NUM_OCTANTS # 24 个节点 -> 哪个父顶点 slot _NODE_OCTANT = np.arange(24) % NUM_OCTANTS # 24 个节点 -> 哪个 octant _OCT_OFFSETS = np.stack([np.arange(8) & 1, (np.arange(8) >> 1) & 1, (np.arange(8) >> 2) & 1], axis=1).astype(np.int32) # octant -> (dx,dy,dz) _INTER_ROW_SLOT = np.array([0, 0, 1]) _INTER_COL_SLOT = np.array([1, 2, 2]) def _unpack_rows(values): """int 行子码 -> bool 位。values (...,) -> (..., 8), 与 _int_to_row 同序(bit i = (v>>i)&1)。""" return ((values[..., None].astype(np.int64) >> _BIT_SHIFTS) & 1).astype(bool) def _pack_keys(coords, resolution): """(...,3) int 坐标 -> (...) int64 唯一 key。坐标范围 [0, resolution)。""" c = coords.astype(np.int64) return (c[..., 0] * resolution + c[..., 1]) * resolution + c[..., 2] def _fast_reconstruct_next_level(token_array, resolution, tokenizer): """token_array: (nface, TOKENS_PER_FACE) int。返回 (V, F), 语义同 reconstruct_next_level_optimized。""" from modules.octree_codec import _TRI_INDICES nface = token_array.shape[0] parents = (token_array[:, :9] - tokenizer.coord_offset).reshape(nface, 3, 3).astype(np.int32) occupancy = _unpack_rows(token_array[:, 9:12] - tokenizer.f_offset) # (nface,3,8) bool def rows_to_mat(lo, hi, offset): return _unpack_rows(token_array[:, lo:hi] - offset) # (nface,8,8) bool, [行,列] intra = [rows_to_mat(12 + 8 * i, 20 + 8 * i, tokenizer.intra_row_offset) for i in range(3)] inter = [rows_to_mat(36 + 8 * i, 44 + 8 * i, tokenizer.inter_row_offset) for i in range(3)] # ---- 24x24 邻接: intra 块按 F&F 掩码后对称化, inter 块按 Fa&Fb 掩码 ---- adjacency = np.zeros((nface, 24, 24), dtype=bool) for slot in range(3): f = occupancy[:, slot] # (nface,8) block = intra[slot] & (f[:, :, None] & f[:, None, :]) block = block | np.swapaxes(block, 1, 2) adjacency[:, slot * 8:(slot + 1) * 8, slot * 8:(slot + 1) * 8] = block for b in range(3): ra, ca = _INTER_ROW_SLOT[b], _INTER_COL_SLOT[b] block = inter[b] & (occupancy[:, ra][:, :, None] & occupancy[:, ca][:, None, :]) adjacency[:, ra * 8:(ra + 1) * 8, ca * 8:(ca + 1) * 8] = block adjacency[:, ca * 8:(ca + 1) * 8, ra * 8:(ra + 1) * 8] = np.swapaxes(block, 1, 2) # ---- 24 个节点的世界坐标与 key ---- node_coords = parents[:, _NODE_PARENT] * 2 + _OCT_OFFSETS[_NODE_OCTANT] # (nface,24,3) node_keys = _pack_keys(node_coords, resolution) # (nface,24) # ---- 三角形: 三条边都在邻接里 ---- i_idx, j_idx, k_idx = _TRI_INDICES[:, 0], _TRI_INDICES[:, 1], _TRI_INDICES[:, 2] valid = (adjacency[:, i_idx, j_idx] & adjacency[:, j_idx, k_idx] & adjacency[:, i_idx, k_idx]) face_of_tri, tri_of_tri = np.nonzero(valid) # 各 (ntri,) tri_nodes = _TRI_INDICES[tri_of_tri] # (ntri,3) 节点编号 tri_coords = node_coords[face_of_tri[:, None], tri_nodes] # (ntri,3,3) tri_keys = node_keys[face_of_tri[:, None], tri_nodes] # (ntri,3) # ---- 全部边(上三角) ---- upper = np.triu(np.ones((24, 24), dtype=bool), 1) face_of_edge, node_i, node_j = np.nonzero(adjacency & upper) key_i = node_keys[face_of_edge, node_i] key_j = node_keys[face_of_edge, node_j] edge_pairs = np.unique(np.stack([np.minimum(key_i, key_j), np.maximum(key_i, key_j)], axis=1), axis=0) # ---- 被三角形覆盖的边 ---- if len(tri_keys): tri_sorted = np.sort(tri_keys, axis=1) covered = np.concatenate([tri_sorted[:, [0, 1]], tri_sorted[:, [1, 2]], tri_sorted[:, [0, 2]]], axis=0) covered = np.unique(covered, axis=0) else: covered = np.zeros((0, 2), dtype=np.int64) blocks = [tri_coords] if len(tri_coords) else [] # ---- 未被任何三角形覆盖的边 -> 退化面 (A,B,B) ---- if len(edge_pairs): edge_flat = edge_pairs[:, 0] * (resolution ** 3) + edge_pairs[:, 1] covered_flat = (covered[:, 0] * (resolution ** 3) + covered[:, 1]) if len(covered) \ else np.zeros(0, dtype=np.int64) open_edges = edge_pairs[~np.isin(edge_flat, covered_flat)] if len(open_edges): key_to_coord = _build_key_lookup(node_keys, node_coords) ca = key_to_coord[open_edges[:, 0]] cb = key_to_coord[open_edges[:, 1]] blocks.append(np.stack([ca, cb, cb], axis=1)) # ---- 孤立的 active 节点(无任何边) -> 点 (A,A,A) ---- active_keys = np.unique(node_keys[occupancy.reshape(nface, 24)]) if len(active_keys): edge_nodes = np.unique(edge_pairs.reshape(-1)) if len(edge_pairs) else np.zeros(0, dtype=np.int64) isolated = active_keys[~np.isin(active_keys, edge_nodes)] if len(isolated): key_to_coord = _build_key_lookup(node_keys, node_coords) c = key_to_coord[isolated] blocks.append(np.stack([c, c, c], axis=1)) if not blocks: return np.array([]), np.array([]) flat_v = np.concatenate(blocks, axis=0).reshape(-1, 3) V, inverse = np.unique(flat_v, axis=0, return_inverse=True) faces_raw = inverse.reshape(-1, 3) _, unique_face_indices = np.unique(np.sort(faces_raw, axis=1), axis=0, return_index=True) F = faces_raw[unique_face_indices] return undiscretize(V, num_discrete=resolution), F def _build_key_lookup(node_keys, node_coords): """key -> 坐标 的稠密查表(用 np.unique 的第一次出现), 供退化面/孤立点还原坐标。""" flat_keys = node_keys.reshape(-1) flat_coords = node_coords.reshape(-1, 3) uniq, first = np.unique(flat_keys, return_index=True) lookup = np.zeros((int(uniq.max()) + 1, 3), dtype=np.int32) lookup[uniq] = flat_coords[first] return lookup def _fast_decode_tokens(token_sequence, target_depth, tokenizer): """decode_tokens 的向量化实现。返回 trimesh.Trimesh 或 None。""" arr = np.asarray(token_sequence).reshape(-1) arr = arr[arr != tokenizer.pad_id] # 与原版 clean_seq 一致 per_face = tokenizer.TOKENS_PER_FACE nface = arr.shape[0] // per_face if nface == 0: return None token_array = arr[:nface * per_face].reshape(nface, per_face).astype(np.int64) V, F = _fast_reconstruct_next_level(token_array, 2 ** (target_depth + 1), tokenizer) if len(V) == 0 or len(F) == 0: return None return trimesh.Trimesh(vertices=V, faces=F, process=False) FastMeshCodec = MeshCodec # 兼容别名: 加速实现已并入 MeshCodec