Instructions to use zhaxie/tooth with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Trellis
How to use zhaxie/tooth with Trellis:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Download app/surface_postprocess.py from zhaxie/tooth: direct link, hf CLI and curl.
- Browser
- Download file 5.28 kB
-
https://huggingface.co/zhaxie/tooth/resolve/main/app/surface_postprocess.py
- Command line
-
hf download hf://zhaxie/tooth/app/surface_postprocess.py
-
curl -L -o surface_postprocess.py https://huggingface.co/zhaxie/tooth/resolve/main/app/surface_postprocess.py
5.28 kB
| """2026-10-07:交付版局部补洞、Taubin平滑及几何变化保护,无需GT。""" | |
| import numpy as np | |
| import trimesh | |
| from scipy import ndimage | |
| from skimage.measure import marching_cubes | |
| from mesh_repair import repair_small_holes | |
| def topology(mesh): | |
| counts = np.bincount(mesh.edges_unique_inverse) | |
| bodies = int(mesh.body_count) | |
| return {'boundary_edges': int((counts == 1).sum()), | |
| 'components': bodies, 'watertight': bool(mesh.is_watertight), | |
| 'handles': int(round((2*bodies-mesh.euler_number)/2)) if mesh.is_watertight else None} | |
| def scene_for(mesh): | |
| import open3d as o3d | |
| scene = o3d.t.geometry.RaycastingScene(nthreads=2) | |
| scene.add_triangles(o3d.t.geometry.TriangleMesh( | |
| o3d.core.Tensor(np.asarray(mesh.vertices, np.float32)), | |
| o3d.core.Tensor(np.asarray(mesh.faces, np.int32)))) | |
| return scene | |
| def geometry_change(raw, result, seed): | |
| import open3d as o3d | |
| distances = [] | |
| for target, source in [(raw, result), (result, raw)]: | |
| points = trimesh.sample.sample_surface(source, 100000, seed=seed)[0] | |
| distances.append(scene_for(target).compute_distance( | |
| o3d.core.Tensor(np.asarray(points, np.float32)), nthreads=2).numpy()) | |
| distances = np.concatenate(distances) | |
| return {'extent_change_max': float(np.max(np.abs(result.extents / np.maximum(raw.extents, 1e-12)-1))), | |
| 'surface_shift_p95': float(np.quantile(distances, .95)), | |
| 'surface_shift_mean': float(distances.mean())} | |
| def within_limits(change): | |
| return change['extent_change_max'] <= .02 and change['surface_shift_p95'] <= .005 | |
| def close_small_tunnels(mesh): | |
| import open3d as o3d | |
| pitch, pad = 1/384, 4 | |
| voxels = mesh.voxelized(pitch=pitch) | |
| occupied = np.pad(voxels.matrix, pad) | |
| origin = voxels.transform[:3, 3] - pad*pitch | |
| x, y, z = np.meshgrid(*([np.arange(-2, 3)]*3), indexing='ij') | |
| ball = x*x+y*y+z*z <= 4 | |
| expanded = ndimage.binary_dilation(occupied, structure=ball) | |
| closed = ndimage.binary_erosion(ndimage.binary_fill_holes(expanded), structure=ball) | |
| closed |= occupied | |
| field = ndimage.gaussian_filter(closed.astype(np.float32), sigma=.55) | |
| vertices, faces, _, _ = marching_cubes(field, .5, spacing=(pitch,)*3, allow_degenerate=False) | |
| result = trimesh.Trimesh(vertices+origin, faces, process=False) | |
| result.fix_normals(multibody=True) | |
| nearest = scene_for(mesh).compute_closest_points( | |
| o3d.core.Tensor(np.asarray(result.vertices, np.float32)), nthreads=2) | |
| target = nearest['points'].numpy() | |
| ids = nearest['primitive_ids'].numpy().astype(np.int64) | |
| selected = ((np.linalg.norm(target-result.vertices, axis=1) < 2*pitch) & | |
| (np.einsum('ij,ij->i', result.vertex_normals, mesh.face_normals[ids]) > .5)) | |
| projected = np.asarray(result.vertices).copy() | |
| projected[selected] = .9*target[selected]+.1*projected[selected] | |
| result.vertices = projected | |
| return trimesh.smoothing.filter_taubin(result, lamb=.5, nu=.53, iterations=80) | |
| def postprocess(mesh, seed=1234): | |
| if not len(mesh.faces) or not np.isfinite(mesh.vertices).all(): | |
| raise ValueError('Invalid input crown mesh') | |
| seed = int(seed) % (2**32) | |
| before = topology(mesh) | |
| raw, boundary_meta = repair_small_holes(mesh, max_boundary_edges=32) | |
| repaired = False | |
| rejected = None | |
| if raw.is_watertight and (topology(raw)['handles'] or 0) > 0: | |
| try: | |
| candidate = close_small_tunnels(raw) | |
| change = geometry_change(mesh, candidate, seed) | |
| after = topology(candidate) | |
| repaired = bool(after['watertight'] and after['components'] <= before['components'] and | |
| (after['handles'] or 0) < (topology(raw)['handles'] or 0) and within_limits(change)) | |
| if not repaired: | |
| rejected = {'topology': after, **change, 'reason': 'topology or geometry gate'} | |
| except Exception as exc: | |
| rejected = {'reason': repr(exc)} | |
| result = candidate if repaired else trimesh.smoothing.filter_taubin(raw.copy(), lamb=.5, nu=.53, iterations=80) | |
| change = geometry_change(mesh, result, seed) | |
| passes = 80 | |
| # Guard the smoothing fallback as well as the remeshing candidate. | |
| if not within_limits(change): | |
| repaired = False | |
| result = trimesh.smoothing.filter_taubin(mesh.copy(), lamb=.5, nu=.53, iterations=20) | |
| passes = 20 | |
| change = geometry_change(mesh, result, seed) | |
| if not within_limits(change): | |
| result, passes = mesh.copy(), 0 | |
| change = geometry_change(mesh, result, seed) | |
| after = topology(result) | |
| if after['boundary_edges']: | |
| status = 'open_boundaries_remaining' | |
| elif after['handles']: | |
| status = 'major_topology_hole' | |
| elif repaired or before['boundary_edges'] > after['boundary_edges']: | |
| status = 'repaired' | |
| else: | |
| status = 'clean' | |
| return result, {'status': status, 'topology_before': before, 'topology_after': after, | |
| 'hole_repair_accepted': repaired, 'boundary_repair': boundary_meta, 'rejected_candidate': rejected, | |
| 'taubin': {'lambda': .5, 'nu': .53, 'passes': passes}, 'pitch': 1/384, | |
| 'closing_radius_voxels': 2, 'gaussian_sigma_voxels': .55, **change} | |