#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ fill_all_empty_faces_v2_harmonic_atlas.py 目标:替换原来的“一个缺色孤岛 -> 一个均值色块”的逻辑。 核心逻辑: 1. 正常 face 保持原有 UV,只因为 texture 高度变大而 remap v 坐标。 2. missing face 不再按 region 写一个颜色点,也不再整组 face 指向同一个 vt。 3. 在 3D mesh 拓扑上,对 missing region 的“顶点颜色”做 harmonic/Laplace 扩散: - 边界顶点颜色来自相邻正常 face 的原始 texture 采样; - 内部顶点颜色由图拉普拉斯扩散得到; - 所以颜色从边缘向内部连续变化。 4. 每个 missing face 在底部新增 atlas 中分配一个小三角 tile。 - tile 内不是纯色,而是按三个顶点颜色做 barycentric 插值; - 相邻 missing face 即使在 atlas 里不相邻,只要共享 3D 顶点,边上的插值颜色也连续; - tile 周围做 padding,避免纹理过滤时采到白底/其他 tile。 输入输出参数保持原脚本一致: --input_obj --input_texture --missing_faces --output_obj --output_texture 额外参数都是可选。 """ import argparse import os import time from collections import defaultdict, deque from typing import Dict, List, Tuple, Set import cv2 import numpy as np import tqdm # ============================================================ # OBJ / texture IO # ============================================================ def _parse_obj_index(s: str, current_len: int) -> int: """OBJ index: positive is 1-based, negative is relative to current list end.""" idx = int(s) if idx > 0: return idx - 1 if idx < 0: return current_len + idx raise ValueError("OBJ index 不能为 0") def read_obj_basic(obj_path: str): vertices: List[List[float]] = [] uvs: List[List[float]] = [] vertex_indices: List[List[int]] = [] uv_indices: List[List[int]] = [] mtllib_lines: List[str] = [] usemtl_name = "material_0" with open(obj_path, "r", encoding="utf-8", errors="ignore") as f: for line in f: if line.startswith("mtllib "): mtllib_lines.append(line.strip()) elif line.startswith("usemtl "): parts = line.strip().split(maxsplit=1) if len(parts) == 2: usemtl_name = parts[1] elif line.startswith("v "): parts = line.strip().split() if len(parts) < 4: continue vertices.append([float(parts[1]), float(parts[2]), float(parts[3])]) elif line.startswith("vt "): parts = line.strip().split() if len(parts) < 3: continue uvs.append([float(parts[1]), float(parts[2])]) elif line.startswith("f "): parts = line.strip().split()[1:] if len(parts) != 3: raise ValueError(f"当前脚本只支持三角面,发现非三角 face: {line.strip()}") v_row = [] vt_row = [] for token in parts: sub = token.split("/") if len(sub) < 2 or sub[1] == "": raise ValueError(f"face 缺少 vt,无法处理: {line.strip()}") v_row.append(_parse_obj_index(sub[0], len(vertices))) vt_row.append(_parse_obj_index(sub[1], len(uvs))) vertex_indices.append(v_row) uv_indices.append(vt_row) vertices_np = np.asarray(vertices, dtype=np.float32) uvs_np = np.asarray(uvs, dtype=np.float32) vertex_indices_np = np.asarray(vertex_indices, dtype=np.int64) uv_indices_np = np.asarray(uv_indices, dtype=np.int64) if len(vertices_np) == 0 or len(uvs_np) == 0 or len(vertex_indices_np) == 0: raise ValueError("OBJ 内没有读到完整的 v / vt / f 数据。") return vertices_np, uvs_np, vertex_indices_np, uv_indices_np, mtllib_lines, usemtl_name def read_missing_faces(path: str, index_base: int = 0) -> np.ndarray: arr = [] with open(path, "r", encoding="utf-8", errors="ignore") as f: for line in f: s = line.strip() if not s: continue arr.append(int(s) - index_base) return np.asarray(arr, dtype=np.int64) def read_texture_rgb(path: str) -> np.ndarray: bgr = cv2.imread(path, cv2.IMREAD_COLOR) if bgr is None: raise FileNotFoundError(f"无法读取贴图: {path}") return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) def write_texture_rgb(path: str, rgb: np.ndarray): os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR) ok = cv2.imwrite(path, bgr, [cv2.IMWRITE_PNG_COMPRESSION, 3]) if not ok: raise IOError(f"写入贴图失败: {path}") def write_obj_with_uv_coordinates( filename: str, vertices: np.ndarray, uvs: np.ndarray, vertex_indices: np.ndarray, uv_indices: np.ndarray, mtllib_lines: List[str] = None, usemtl_name: str = "material_0", ): os.makedirs(os.path.dirname(os.path.abspath(filename)), exist_ok=True) if not mtllib_lines: mtllib_lines = ["mtllib mesh.mtl"] estimated_size = len(vertices) * 48 + len(uvs) * 32 + len(vertex_indices) * 64 buffer_size = min(max(int(estimated_size * 1.2), 64 * 1024 * 1024), 1024 * 1024 * 1024) lines: List[str] = [] lines.extend(mtllib_lines) for v in vertices: lines.append("v %.6f %.6f %.6f" % (v[0], v[1], v[2])) lines.append("") for uv in uvs: lines.append("vt %.8f %.8f" % (uv[0], uv[1])) lines.append("") lines.append(f"usemtl {usemtl_name}") for v_idx, vt_idx in zip(vertex_indices, uv_indices): lines.append( "f %d/%d %d/%d %d/%d" % ( int(v_idx[0]) + 1, int(vt_idx[0]) + 1, int(v_idx[1]) + 1, int(vt_idx[1]) + 1, int(v_idx[2]) + 1, int(vt_idx[2]) + 1, ) ) with open(filename, "w", buffering=buffer_size, encoding="utf-8") as f: f.write("\n".join(lines)) # ============================================================ # Mesh topology # ============================================================ def build_face_adjacency_and_vertex_faces(faces: np.ndarray, num_vertices: int): """ 基于共享边构建 face adjacency,同时构建 vertex -> faces。 返回:face_adjacency, vertex_faces """ faces = np.asarray(faces, dtype=np.int64) num_faces = len(faces) # vertex -> faces vertex_faces: List[List[int]] = [[] for _ in range(num_vertices)] for fi, row in enumerate(faces): vertex_faces[int(row[0])].append(fi) vertex_faces[int(row[1])].append(fi) vertex_faces[int(row[2])].append(fi) # edge -> faces,向量化排序 edges = np.stack([ np.column_stack((faces[:, 0], faces[:, 1])), np.column_stack((faces[:, 1], faces[:, 2])), np.column_stack((faces[:, 2], faces[:, 0])), ], axis=1).reshape(-1, 2) edges.sort(axis=1) edge_faces = np.repeat(np.arange(num_faces, dtype=np.int64), 3) key = edges[:, 0] * num_vertices + edges[:, 1] order = np.argsort(key) key = key[order] edge_faces = edge_faces[order] adj = defaultdict(list) same = key[1:] == key[:-1] pos = np.where(same)[0] for p in pos: f0 = int(edge_faces[p]) f1 = int(edge_faces[p + 1]) if f0 != f1: adj[f0].append(f1) adj[f1].append(f0) return dict(adj), vertex_faces def find_missing_regions(face_adjacency: Dict[int, List[int]], missing_faces: np.ndarray, total_faces: int): missing_all = set(int(x) for x in missing_faces if 0 <= int(x) < total_faces) unused = set(missing_all) regions: List[Tuple[Set[int], Set[int]]] = [] pbar = tqdm.tqdm(total=len(unused), desc="Finding missing islands") done = 0 while unused: start = unused.pop() group = {start} boundary = set() stack = [start] while stack: f = stack.pop() for nb in face_adjacency.get(f, []): if nb in unused: unused.remove(nb) group.add(nb) stack.append(nb) elif nb not in missing_all: boundary.add(nb) regions.append((group, boundary)) new_done = len(missing_all) - len(unused) pbar.update(new_done - done) done = new_done pbar.close() if regions: sizes = [len(x[0]) for x in regions] print(f"Total missing islands: {len(regions)}") print(f"Island size: min={min(sizes)}, max={max(sizes)}, avg={sum(sizes)/len(sizes):.2f}") else: print("No missing islands found.") return regions # ============================================================ # Texture sampling # ============================================================ def sample_texture_bilinear_rgb(texture_rgb: np.ndarray, uv: np.ndarray) -> np.ndarray: """uv: [2], v is OBJ convention, image y is flipped.""" H, W = texture_rgb.shape[:2] u = float(np.clip(uv[0], 0.0, 1.0)) v = float(np.clip(uv[1], 0.0, 1.0)) x = u * (W - 1) y = (1.0 - v) * (H - 1) x0 = int(np.floor(x)) y0 = int(np.floor(y)) x1 = min(x0 + 1, W - 1) y1 = min(y0 + 1, H - 1) dx = x - x0 dy = y - y0 c00 = texture_rgb[y0, x0].astype(np.float32) c10 = texture_rgb[y0, x1].astype(np.float32) c01 = texture_rgb[y1, x0].astype(np.float32) c11 = texture_rgb[y1, x1].astype(np.float32) c0 = c00 * (1.0 - dx) + c10 * dx c1 = c01 * (1.0 - dx) + c11 * dx return c0 * (1.0 - dy) + c1 * dy def sample_face_corner_color( face_id: int, corner: int, texture_rgb: np.ndarray, uvs: np.ndarray, face_uv_indices: np.ndarray, corner_inset: float = 0.90, ) -> np.ndarray: """ 在正常 face 的某个顶点附近采样。 不直接采顶点本身,而是往三角形内部缩一点,避免 seam 边界采样不稳。 corner_inset 越接近 1,越贴近该顶点。 """ tri_uv = uvs[face_uv_indices[face_id]] # [3,2] w = np.full(3, (1.0 - corner_inset) / 2.0, dtype=np.float32) w[corner] = corner_inset uv = w @ tri_uv return sample_texture_bilinear_rgb(texture_rgb, uv) def median_color(colors: List[np.ndarray]) -> np.ndarray: if len(colors) == 0: return np.asarray([128, 128, 128], dtype=np.float32) return np.median(np.stack(colors, axis=0).astype(np.float32), axis=0) # ============================================================ # Harmonic vertex color diffusion # ============================================================ def build_region_vertex_graph(region_faces: Set[int], faces: np.ndarray): """ 对一个 missing island 构建 local vertex graph。 返回:unique_vertices, vertex_to_local, unique_edges, local_face_ids """ face_ids = np.asarray(sorted(region_faces), dtype=np.int64) tri_v = faces[face_ids] # [F,3] unique_vertices = np.unique(tri_v.reshape(-1)) v2local = {int(v): i for i, v in enumerate(unique_vertices)} local_faces = np.vectorize(lambda x: v2local[int(x)], otypes=[np.int64])(tri_v) edges = np.concatenate([ local_faces[:, [0, 1]], local_faces[:, [1, 2]], local_faces[:, [2, 0]], ], axis=0) edges.sort(axis=1) edges = np.unique(edges, axis=0) return face_ids, unique_vertices, v2local, edges, local_faces def compute_boundary_vertex_constraints( unique_vertices: np.ndarray, vertex_faces: List[List[int]], faces: np.ndarray, face_uv_indices: np.ndarray, uvs: np.ndarray, texture_rgb: np.ndarray, missing_mask: np.ndarray, corner_inset: float = 0.90, ): """ 对 region 内的每个顶点,如果它邻接任何非 missing face,就从这些正常 face 的对应 corner 采颜色。 这比“对边界 face 求一个均值”更合理,因为边界不同位置的颜色会保留下来。 """ n = len(unique_vertices) constrained = np.zeros(n, dtype=bool) constraint_colors = np.zeros((n, 3), dtype=np.float32) for local_i, gv in enumerate(unique_vertices): samples = [] for f in vertex_faces[int(gv)]: if missing_mask[f]: continue row = faces[f] corners = np.where(row == gv)[0] if len(corners) == 0: continue for c in corners: samples.append(sample_face_corner_color( f, int(c), texture_rgb, uvs, face_uv_indices, corner_inset=corner_inset, )) if samples: constrained[local_i] = True constraint_colors[local_i] = median_color(samples) return constrained, constraint_colors def initialize_vertex_colors_by_bfs(edges: np.ndarray, constrained: np.ndarray, constraint_colors: np.ndarray) -> np.ndarray: n = len(constrained) colors = np.zeros((n, 3), dtype=np.float32) if constrained.any(): colors[constrained] = constraint_colors[constrained] fallback = np.mean(constraint_colors[constrained], axis=0) else: fallback = np.asarray([128, 128, 128], dtype=np.float32) colors[:] = fallback return colors adj = [[] for _ in range(n)] for a, b in edges: adj[int(a)].append(int(b)) adj[int(b)].append(int(a)) q = deque(np.where(constrained)[0].tolist()) visited = np.zeros(n, dtype=bool) visited[constrained] = True while q: cur = q.popleft() for nb in adj[cur]: if visited[nb]: continue colors[nb] = colors[cur] visited[nb] = True q.append(nb) colors[~visited] = fallback return colors def solve_harmonic_vertex_colors( edges: np.ndarray, constrained: np.ndarray, constraint_colors: np.ndarray, smooth_iters: int = 80, self_weight: float = 0.0, ) -> np.ndarray: """ 对 region 顶点颜色做 Dirichlet boundary harmonic extension。 constrained 顶点保持边界颜色;非 constrained 顶点反复取邻居平均。 """ n = len(constrained) colors = initialize_vertex_colors_by_bfs(edges, constrained, constraint_colors) if n == 0 or len(edges) == 0: return colors e0 = edges[:, 0].astype(np.int64) e1 = edges[:, 1].astype(np.int64) deg = np.zeros(n, dtype=np.float32) np.add.at(deg, e0, 1.0) np.add.at(deg, e1, 1.0) denom = deg[:, None] + float(self_weight) denom = np.maximum(denom, 1e-8) for _ in range(max(0, int(smooth_iters))): acc = np.zeros_like(colors) np.add.at(acc, e0, colors[e1]) np.add.at(acc, e1, colors[e0]) if self_weight > 0: acc += colors * float(self_weight) new_colors = acc / denom # Dirichlet boundary:边界顶点锁住,不允许被内部均化掉。 new_colors[constrained] = constraint_colors[constrained] colors = new_colors return np.clip(colors, 0, 255).astype(np.float32) # ============================================================ # Per-face triangular atlas baking # ============================================================ def precompute_triangle_tile(tile_size: int, pad: int): """ 预计算一个标准三角 tile 的 barycentric weights 和 padding 最近邻映射。 后续每个 missing face 只需要 weights @ 三个顶点颜色。 """ tile_size = int(tile_size) pad = int(pad) if tile_size < 4: raise ValueError("tile_size 至少要 >= 4") if pad < 1: pad = 1 if pad * 2 + 2 >= tile_size: pad = max(1, tile_size // 4) # 标准右三角,三个 UV 角点都留 padding,避免双线性采样采到 tile 外。 p0 = np.asarray([pad, pad], dtype=np.float32) p1 = np.asarray([tile_size - pad - 1, pad], dtype=np.float32) p2 = np.asarray([pad, tile_size - pad - 1], dtype=np.float32) yy, xx = np.meshgrid(np.arange(tile_size, dtype=np.float32), np.arange(tile_size, dtype=np.float32), indexing="ij") pts = np.stack([xx + 0.5, yy + 0.5], axis=-1) # [T,T,2], pixel centers # barycentric v0 = p1 - p0 v1 = p2 - p0 v2 = pts - p0 d00 = float(np.dot(v0, v0)) d01 = float(np.dot(v0, v1)) d11 = float(np.dot(v1, v1)) denom = d00 * d11 - d01 * d01 if abs(denom) < 1e-8: raise ValueError("退化 tile triangle") d20 = v2[..., 0] * v0[0] + v2[..., 1] * v0[1] d21 = v2[..., 0] * v1[0] + v2[..., 1] * v1[1] w1 = (d11 * d20 - d01 * d21) / denom w2 = (d00 * d21 - d01 * d20) / denom w0 = 1.0 - w1 - w2 weights = np.stack([w0, w1, w2], axis=-1).astype(np.float32) inside = (weights[..., 0] >= -1e-4) & (weights[..., 1] >= -1e-4) & (weights[..., 2] >= -1e-4) # padding:tile 外部/三角外部像素复制最近的三角内部像素颜色。 inside_coords = np.argwhere(inside) nearest_y = np.zeros((tile_size, tile_size), dtype=np.int32) nearest_x = np.zeros((tile_size, tile_size), dtype=np.int32) for y in range(tile_size): for x in range(tile_size): if inside[y, x]: nearest_y[y, x] = y nearest_x[y, x] = x else: d2 = (inside_coords[:, 0] - y) ** 2 + (inside_coords[:, 1] - x) ** 2 k = int(np.argmin(d2)) nearest_y[y, x] = int(inside_coords[k, 0]) nearest_x[y, x] = int(inside_coords[k, 1]) padded_weights = weights[nearest_y, nearest_x] # UV 角点使用像素中心,更稳定。 uv_points = np.stack([p0, p1, p2], axis=0).astype(np.float32) return padded_weights, uv_points def build_harmonic_atlas_texture( texture_rgb: np.ndarray, original_uvs: np.ndarray, faces: np.ndarray, face_uv_indices: np.ndarray, missing_faces: np.ndarray, face_corner_colors: Dict[int, np.ndarray], tile_size: int = 12, tile_pad: int = 2, ): """ 给每个 missing face 分配一个小三角 tile。 tile 内按三个顶点颜色插值,不是纯色块。 """ H, W = texture_rgb.shape[:2] missing_faces_sorted = np.asarray(sorted(set(int(x) for x in missing_faces)), dtype=np.int64) n_missing = len(missing_faces_sorted) cols = max(1, W // tile_size) rows = int(np.ceil(n_missing / cols)) atlas_h = rows * tile_size new_H = H + atlas_h print(f"atlas tiles: {n_missing}, cols={cols}, rows={rows}, atlas_h={atlas_h}") new_texture = np.full((new_H, W, 3), 255, dtype=np.uint8) new_texture[:H, :, :] = texture_rgb # 原始 UV remap:保持原图像素位置不变,只是贴图高度变大。 remapped_uvs = original_uvs.copy().astype(np.float32) old_y = (1.0 - remapped_uvs[:, 1]) * (H - 1) remapped_uvs[:, 1] = 1.0 - old_y / max(new_H - 1, 1) new_uvs: List[List[float]] = remapped_uvs.tolist() new_face_uv_indices = face_uv_indices.copy() weights_map, uv_points = precompute_triangle_tile(tile_size, tile_pad) weights_flat = weights_map.reshape(-1, 3) # [T*T,3] for i, f in enumerate(tqdm.tqdm(missing_faces_sorted, desc="Baking missing face tiles")): col = i % cols row = i // cols x0 = col * tile_size y0 = H + row * tile_size corner_colors = face_corner_colors.get(int(f)) if corner_colors is None: corner_colors = np.full((3, 3), 128, dtype=np.float32) corner_colors = np.asarray(corner_colors, dtype=np.float32) tile = weights_flat @ corner_colors # [T*T,3] tile = np.clip(np.rint(tile), 0, 255).astype(np.uint8).reshape(tile_size, tile_size, 3) new_texture[y0:y0 + tile_size, x0:x0 + tile_size, :] = tile new_vt = [] for p in uv_points: px = x0 + float(p[0]) py = y0 + float(p[1]) u = px / max(W - 1, 1) v = 1.0 - py / max(new_H - 1, 1) new_uvs.append([u, v]) new_vt.append(len(new_uvs) - 1) new_face_uv_indices[int(f)] = np.asarray(new_vt, dtype=new_face_uv_indices.dtype) return new_texture, np.asarray(new_uvs, dtype=np.float32), new_face_uv_indices # ============================================================ # Main process # ============================================================ def process( input_obj_path: str, input_texture_path: str, missing_faces_path: str, output_obj_path: str, output_texture_path: str, missing_index_base: int = 0, tile_size: int = 12, tile_pad: int = 2, smooth_iters: int = 80, self_weight: float = 0.0, corner_inset: float = 0.90, ): start = time.time() print("Reading input files...") vertices, uvs, faces, face_uv_indices, mtllib_lines, usemtl_name = read_obj_basic(input_obj_path) texture_rgb = read_texture_rgb(input_texture_path) missing_faces = read_missing_faces(missing_faces_path, index_base=missing_index_base) total_faces = len(faces) missing_faces = missing_faces[(missing_faces >= 0) & (missing_faces < total_faces)] missing_faces = np.unique(missing_faces) missing_mask = np.zeros(total_faces, dtype=bool) missing_mask[missing_faces] = True print(f"vertices: {len(vertices)}") print(f"uvs: {len(uvs)}") print(f"faces: {len(faces)}") print(f"missing faces: {len(missing_faces)}") print(f"texture: {texture_rgb.shape[1]} x {texture_rgb.shape[0]}") if len(missing_faces) == 0: write_obj_with_uv_coordinates(output_obj_path, vertices, uvs, faces, face_uv_indices, mtllib_lines, usemtl_name) write_texture_rgb(output_texture_path, texture_rgb) return t0 = time.time() print("Building adjacency...") face_adjacency, vertex_faces = build_face_adjacency_and_vertex_faces(faces, len(vertices)) print(f"adjacency using: {time.time() - t0:.2f}s") t0 = time.time() regions = find_missing_regions(face_adjacency, missing_faces, total_faces) print(f"regions using: {time.time() - t0:.2f}s") # face -> three corner colors face_corner_colors: Dict[int, np.ndarray] = {} for region_idx, (region_faces, _boundary_faces) in enumerate(tqdm.tqdm(regions, desc="Solving region colors")): face_ids, unique_vertices, v2local, edges, local_faces = build_region_vertex_graph(region_faces, faces) constrained, constraint_colors = compute_boundary_vertex_constraints( unique_vertices=unique_vertices, vertex_faces=vertex_faces, faces=faces, face_uv_indices=face_uv_indices, uvs=uvs, texture_rgb=texture_rgb, missing_mask=missing_mask, corner_inset=corner_inset, ) if not constrained.any(): # 极少数情况:region 没有任何正常面顶点约束。 # 这种输入本身没有颜色来源,只能给灰色,避免崩。 print(f"Warning: region {region_idx} has no boundary vertex color constraints, using gray.") vertex_colors = np.full((len(unique_vertices), 3), 128, dtype=np.float32) else: vertex_colors = solve_harmonic_vertex_colors( edges=edges, constrained=constrained, constraint_colors=constraint_colors, smooth_iters=smooth_iters, self_weight=self_weight, ) # 写成每个 missing face 的三个角颜色。 for local_fi, f in enumerate(face_ids): lf = local_faces[local_fi] face_corner_colors[int(f)] = vertex_colors[lf] print("Building harmonic atlas...") new_texture, new_uvs, new_face_uv_indices = build_harmonic_atlas_texture( texture_rgb=texture_rgb, original_uvs=uvs, faces=faces, face_uv_indices=face_uv_indices, missing_faces=missing_faces, face_corner_colors=face_corner_colors, tile_size=tile_size, tile_pad=tile_pad, ) print("Writing outputs...") write_obj_with_uv_coordinates( output_obj_path, vertices, new_uvs, faces, new_face_uv_indices, mtllib_lines=mtllib_lines, usemtl_name=usemtl_name, ) write_texture_rgb(output_texture_path, new_texture) print(f"output texture: {new_texture.shape[1]} x {new_texture.shape[0]}") print(f"new uv count: {len(new_uvs)}") print(f"Total using: {time.time() - start:.2f}s") def main(): parser = argparse.ArgumentParser(description="Fill missing color faces with harmonic vertex-color atlas baking.") # 原始接口保持一致 parser.add_argument("--input_obj", type=str, required=True, help="Path to the input OBJ file") parser.add_argument("--input_texture", type=str, required=True, help="Path to the input texture file") parser.add_argument("--missing_faces", type=str, required=True, help="Path to missing face index file") parser.add_argument("--output_obj", type=str, required=True, help="Path to the output OBJ file") parser.add_argument("--output_texture", type=str, required=True, help="Path to the output texture file") # 可选参数 parser.add_argument("--missing_index_base", type=int, default=0, help="missing_faces.txt 的索引基准。原脚本默认 0-based;如果文件是 1-based,传 1。") parser.add_argument("--tile_size", type=int, default=12, help="每个 missing face 在新增 atlas 中的 tile 尺寸。越大越细,贴图越大。推荐 8~14。") parser.add_argument("--tile_pad", type=int, default=2, help="每个 tile 内三角形到边界的 padding。用于减少纹理过滤串色。推荐 2。") parser.add_argument("--smooth_iters", type=int, default=80, help="顶点颜色 harmonic 扩散迭代次数。越大越平滑,稍慢。推荐 50~120。") parser.add_argument("--self_weight", type=float, default=0.0, help="扩散时保留当前颜色的权重。一般用 0。") parser.add_argument("--corner_inset", type=float, default=0.90, help="边界顶点采样时往正常 face 内部缩进的比例。0.85~0.95 比较稳。") args = parser.parse_args() process( input_obj_path=args.input_obj, input_texture_path=args.input_texture, missing_faces_path=args.missing_faces, output_obj_path=args.output_obj, output_texture_path=args.output_texture, missing_index_base=args.missing_index_base, tile_size=args.tile_size, tile_pad=args.tile_pad, smooth_iters=args.smooth_iters, self_weight=args.self_weight, corner_inset=args.corner_inset, ) if __name__ == "__main__": main()