diff --git a/fill2sc/generate_fill_mesh_hsc.py b/fill2sc/generate_fill_mesh_hsc.py index 8ff6066..56c792b 100644 --- a/fill2sc/generate_fill_mesh_hsc.py +++ b/fill2sc/generate_fill_mesh_hsc.py @@ -85,9 +85,9 @@ class FillMeshGenerator: # ====================================================================== # 参数配置 # ====================================================================== - IN_OBJ = "632723.obj" - IN_IDX = "index.txt" - OUT_FILL = "fill_mesh.obj" + OBJ = "632723.obj" + INDEX = "index.txt" + OUT = "fill_mesh.obj" OUT_MERGED = "632723_filled.obj" OUT_PREVIEW = "fill_preview.png" @@ -109,15 +109,15 @@ class FillMeshGenerator: BLAM = 0.6 # 边界平滑系数 TOL = 1e-4 # Jacobi 收敛阈值 - def __init__(self, in_obj=None, in_idx=None, out_fill=None): - if in_obj: - self.IN_OBJ = in_obj - if in_idx: - self.IN_IDX = in_idx - if out_fill: - self.OUT_FILL = out_fill + def __init__(self, obj=None, index=None, out=None): + if obj: + self.OBJ = obj + if index: + self.INDEX = index + if out: + self.OUT = out # 根据 OUT_FILL 自动推导 OUT_MERGED 和 OUT_PREVIEW - base, ext = os.path.splitext(self.OUT_FILL) + base, ext = os.path.splitext(self.OUT) self.OUT_MERGED = base + "_filled" + ext self.OUT_PREVIEW = base.replace("mesh", "preview") + ".png" if self.OUT_PREVIEW == base + ".png": @@ -353,8 +353,8 @@ class FillMeshGenerator: def load_and_classify(self): """加载模型, 通过边连通主分量提取真眼镜物体""" self.log("== 1. 读入模型 ==") - V, F = self.parse_obj(self.IN_OBJ) - idx = np.loadtxt(self.IN_IDX, dtype=np.int64) - 1 + V, F = self.parse_obj(self.OBJ) + idx = np.loadtxt(self.INDEX, dtype=np.int64) - 1 ism = np.zeros(len(V), bool) ism[idx] = True face_g = ism[F].any(1) @@ -1597,7 +1597,7 @@ class FillMeshGenerator: self.log(f" {self.OUT_FILL}: {len(Pall)} 顶点, {len(Ff)} 面") # 输出合并模型 - with open(self.IN_OBJ) as f: + with open(self.OBJ) as f: orig = f.read() with open(self.OUT_MERGED, "w") as f: f.write(orig.rstrip("\n") + "\n") @@ -1754,11 +1754,11 @@ def diagnose_indices(obj_path, idx_path): # ====================================================================== if __name__ == "__main__": parser = argparse.ArgumentParser(description="眼镜-脸部填充网格生成器") - parser.add_argument("--in_obj", type=str, default="632723.obj", + parser.add_argument("--obj", type=str, default="632723.obj", help="输入 OBJ 模型文件路径 (默认: 632723.obj)") - parser.add_argument("--in_idx", type=str, default="index.txt", + parser.add_argument("--index", type=str, default="index.txt", help="眼镜顶点索引文件路径 (默认: index.txt)") - parser.add_argument("--out_fill", type=str, default="fill_mesh.obj", + parser.add_argument("--out", type=str, default="fill_mesh.obj", help="输出填充网格文件路径 (默认: fill_mesh.obj)") parser.add_argument("--diagnose", action="store_true", help="仅运行诊断,不生成填充网格") @@ -1766,13 +1766,13 @@ if __name__ == "__main__": args = parser.parse_args() if args.diagnose: - diagnose_indices(args.in_obj, args.in_idx) + diagnose_indices(args.obj, args.index) else: gen = FillMeshGenerator( - in_obj=args.in_obj, - in_idx=args.in_idx, - out_fill=args.out_fill + obj=args.obj, + index=args.index, + out=args.out ) gen.run() - # python generate_fill_mesh_hsc.py --in_obj 632723.obj --in_idx index.txt --out_fill fill_mesh.obj \ No newline at end of file + # python generate_fill_mesh_hsc.py --obj 632723.obj --index index.txt --out fill_mesh.obj \ No newline at end of file diff --git a/fill2sc/sdf_fill.py b/fill2sc/sdf_fill.py new file mode 100644 index 0000000..6cc1420 --- /dev/null +++ b/fill2sc/sdf_fill.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +fill_gap_v3.py — 基于射线投射的眼镜-脸空隙填充 +""" + +import numpy as np +from scipy.spatial import cKDTree +from scipy.ndimage import binary_dilation, distance_transform_edt +from skimage import measure +import trimesh +from trimesh.smoothing import filter_taubin +import argparse +import time + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--obj", required=True) + parser.add_argument("--index", required=True) + parser.add_argument("--out", default="fill_mesh.obj") + parser.add_argument("--res", type=int, default=60, help="分辨率(先跑低分辨率验证)") + parser.add_argument("--gap", type=float, default=1.5, help="填充间隙宽度 mm") + args = parser.parse_args() + + t0 = time.time() + print("=== 眼镜-脸部空隙填充 v3 ===\n") + + # 1. 加载 + mesh = trimesh.load(args.obj, force="mesh") + V = mesh.vertices + F = mesh.faces + idx = np.loadtxt(args.index, dtype=np.int64) - 1 + print(f"[load] 顶点 {len(V)}, 面 {len(F)}, 眼镜顶点 {len(idx)}") + + # 2. 分类 + is_glass = np.zeros(len(V), dtype=bool) + is_glass[idx] = True + glass_verts = V[idx] + face_verts = V[~is_glass] + print(f"[classify] 眼镜顶点 {len(glass_verts)}, 脸部顶点 {len(face_verts)}") + + # 3. 对每个眼镜顶点,找最近的脸部顶点 + face_tree = cKDTree(face_verts) + dists, nearest_idx = face_tree.query(glass_verts, k=1) + nearest_face_verts = face_verts[nearest_idx] + + # 方向向量:从脸部指向眼镜 + directions = glass_verts - nearest_face_verts + dir_lengths = np.linalg.norm(directions, axis=1) + directions = directions / dir_lengths[:, None] + + # 统计平均方向 + avg_dir = directions.mean(axis=0) + avg_dir = avg_dir / np.linalg.norm(avg_dir) + print(f"[dir] 平均方向(脸部→眼镜): {avg_dir.round(3)}") + + # 4. 确定填充范围 + # 眼镜中心 + glass_center = glass_verts.mean(axis=0) + # 沿方向投影 + proj = (glass_verts - glass_center) @ avg_dir + front_pos = proj.max() # 眼镜前表面(远离脸部) + back_pos = proj.min() # 眼镜后表面(靠近脸部) + print(f"[surf] 前 {front_pos:.2f}, 后 {back_pos:.2f}") + + # 5. 包围盒(只用眼镜区域) + margin = 4.0 + lo = glass_verts.min(axis=0) - margin + hi = glass_verts.max(axis=0) + margin + print(f"[bbox] {lo.round(2)} -> {hi.round(2)}") + + # 6. 体素网格(低分辨率测试) + shape = np.array([args.res] * 3) + pitch = (hi - lo).max() / args.res + xs = np.linspace(lo[0], hi[0], shape[0]) + ys = np.linspace(lo[1], hi[1], shape[1]) + zs = np.linspace(lo[2], hi[2], shape[2]) + X, Y, Z = np.meshgrid(xs, ys, zs, indexing="ij") + points = np.stack([X.ravel(), Y.ravel(), Z.ravel()], axis=1) + print(f"[grid] {shape} = {points.shape[0]} 体素, 大小 {pitch:.3f}mm") + + # 7. KD-Tree + glass_tree = cKDTree(glass_verts) + face_tree = cKDTree(face_verts) + print("[tree] KD-Tree 构建完成") + + # 8. 计算距离 + print("[dist] 计算距离场...") + dist_to_glass, _ = glass_tree.query(points, k=1) + dist_to_face, _ = face_tree.query(points, k=1) + + # 9. 计算每个体素在"方向"上的投影 + proj_val = (points - glass_center) @ avg_dir + + # 10. 填充规则 + # 在眼镜后表面附近(靠近脸部的那侧) + near_glass = dist_to_glass < args.gap * 2 + + # 不在脸内部 + outside_face = dist_to_face > 0.3 + + # 在后表面附近(back_pos 到 back_pos + gap 之间) + in_gap = (proj_val > back_pos - 0.5) & (proj_val < back_pos + args.gap) + + # 组合 + fill_grid = near_glass & outside_face & in_gap + fill_grid = fill_grid.reshape(shape) + + n_fill = fill_grid.sum() + print(f"[fill] 填充体素: {n_fill}") + + if n_fill < 30: + print("[warn] 太少,尝试仅用近眼镜条件...") + fill_grid = near_glass.reshape(shape) + n_fill = fill_grid.sum() + print(f"[fill] 仅近眼镜: {n_fill}") + + if n_fill < 10: + print("[error] 填充为空") + return + + # 11. 膨胀 + fill_grid = binary_dilation(fill_grid, iterations=3) + + # 12. MC + print("[mc] Marching Cubes...") + sdf = distance_transform_edt(~fill_grid).astype(np.float32) + sdf[fill_grid] = -distance_transform_edt(fill_grid)[fill_grid] + + verts, faces, _, _ = measure.marching_cubes( + sdf, level=0.0, + spacing=(pitch, pitch, pitch) + ) + verts += lo + + result = trimesh.Trimesh(vertices=verts, faces=faces, process=True) + + # 取最大分量 + if len(result.split()) > 1: + comps = result.split(only_watertight=False) + result = max(comps, key=lambda m: m.volume) + print(f"[clean] 取最大分量") + + filter_taubin(result, lamb=0.5, nu=0.5, iterations=5) + + result.export(args.out) + print(f"\n[output] {args.out}") + print(f" 顶点: {len(result.vertices)}, 面: {len(result.faces)}") + print(f" 体积: {result.volume:.3f}, 水密: {result.is_watertight}") + print(f" 耗时: {time.time()-t0:.1f}s") + + +if __name__ == "__main__": + main() \ No newline at end of file