extract_mesh_watertight.py 3.82 KB
Newer Older
ashawkey's avatar
ashawkey committed
1
2
import os
import zlib
3
4
import glob
import tqdm
ashawkey's avatar
ashawkey committed
5
6
7
8
import time
import mcubes
import trimesh
import argparse
9
10
11
12
13
import numpy as np
from skimage import morphology

import torch
import cubvh
ashawkey's avatar
ashawkey committed
14
15
16

import kiui

17
18
19
"""
Extract watertight mesh from a arbitrary mesh by UDF expansion and floodfill.
"""
ashawkey's avatar
ashawkey committed
20
parser = argparse.ArgumentParser()
21
parser.add_argument('test_path', type=str)
ashawkey's avatar
ashawkey committed
22
parser.add_argument('--res', type=int, default=512)
23
parser.add_argument('--workspace', type=str, default='output')
ashawkey's avatar
ashawkey committed
24
25
26
27
28
29
30
31
32
33
34
35
36
opt = parser.parse_args()

device = torch.device('cuda')

points = torch.stack(
    torch.meshgrid(
        torch.linspace(-1, 1, opt.res, device=device),
        torch.linspace(-1, 1, opt.res, device=device),
        torch.linspace(-1, 1, opt.res, device=device),
        indexing="ij",
    ), dim=-1,
) # [N, N, N, 3]

ashawkey's avatar
ashawkey committed
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

def sphere_normalize(vertices):
    bmin = vertices.min(axis=0)
    bmax = vertices.max(axis=0)
    bcenter = (bmax + bmin) / 2
    radius = np.linalg.norm(vertices - bcenter, axis=-1).max()
    vertices = (vertices - bcenter) / (radius)  # to [-1, 1]
    return vertices


def box_normalize(vertices, bound=0.95):
    bmin = vertices.min(axis=0)
    bmax = vertices.max(axis=0)
    bcenter = (bmax + bmin) / 2
    vertices = bound * (vertices - bcenter) / (bmax - bmin).max()
    return vertices


ashawkey's avatar
ashawkey committed
55
def run(path):
ashawkey's avatar
ashawkey committed
56
57
58
59
60

    mesh = trimesh.load(path, process=False, force='mesh')
    mesh.vertices = sphere_normalize(mesh.vertices)
    vertices = torch.from_numpy(mesh.vertices).float().to(device)
    triangles = torch.from_numpy(mesh.faces).long().to(device)
ashawkey's avatar
ashawkey committed
61
62

    t0 = time.time()
ashawkey's avatar
ashawkey committed
63
64
    BVH = cubvh.cuBVH(vertices, triangles)
    print(f'BVH build time: {time.time() - t0:.4f}s')
65
    eps = 2 / opt.res
ashawkey's avatar
ashawkey committed
66
67
68
69
70
71
72
73
74

    # naive sdf
    # sdf, _, _ = BVH.signed_distance(points.view(-1, 3), return_uvw=False, mode='raystab') # some mesh may not be watertight...
    # sdf = sdf.cpu().numpy()
    # occ = (sdf < 0)

    # udf floodfill
    t0 = time.time()
    udf, _, _ = BVH.unsigned_distance(points.view(-1, 3), return_uvw=False)
ashawkey's avatar
ashawkey committed
75
    print(f'UDF time: {time.time() - t0:.4f}s')
ashawkey's avatar
ashawkey committed
76
    udf = udf.cpu().numpy().reshape(opt.res, opt.res, opt.res)
77
    occ = udf < eps # tolerance 2 voxels
ashawkey's avatar
ashawkey committed
78
79
80

    t0 = time.time()
    empty_mask = morphology.flood(occ, (0, 0, 0), connectivity=1) # flood from the corner, which is for sure empty
ashawkey's avatar
ashawkey committed
81
    print(f'Floodfill time: {time.time() - t0:.4f}s')
82
83

    # binary occupancy
ashawkey's avatar
ashawkey committed
84
85
    occ = ~empty_mask

86
87
88
89
90
    # truncated SDF
    sdf = udf - eps  # inner is negative
    inner_mask = occ & (sdf > 0)
    sdf[inner_mask] *= -1

ashawkey's avatar
ashawkey committed
91
92
    print(f'SDF occupancy ratio: {np.sum(sdf < 0) / sdf.size:.4f}')

ashawkey's avatar
ashawkey committed
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
    # # packbits and compress
    # occ = occ.astype(np.uint8).reshape(-1)
    # occ = np.packbits(occ)
    # occ = zlib.compress(occ.tobytes())

    # # save to disk
    # with open(os.path.basename(path).split('.')[0] + '.bin', 'wb') as f:
    #     f.write(occ)

    # # uncompress and unpack
    # occ = zlib.decompress(occ)
    # occ = np.frombuffer(occ, dtype=np.uint8)
    # occ = np.unpackbits(occ, count=opt.res**3).reshape(opt.res, opt.res, opt.res)

    # marching cubes
    t0 = time.time()
109
110
111
112
113
    vertices, triangles = mcubes.marching_cubes(sdf, 0)
    vertices = vertices / (sdf.shape[-1] - 1.0) * 2 - 1
    vertices = vertices.astype(np.float32)
    triangles = triangles.astype(np.int32)
    watertight_mesh = trimesh.Trimesh(vertices, triangles)
ashawkey's avatar
ashawkey committed
114
    print(f'MC time: {time.time() - t0:.4f}s, vertices: {len(watertight_mesh.vertices)}, triangles: {len(watertight_mesh.faces)}')
ashawkey's avatar
ashawkey committed
115

116
117
118
119
    name = os.path.splitext(os.path.basename(path))[0]
    watertight_mesh.export(f'{opt.workspace}/{name}.obj')

os.makedirs(opt.workspace, exist_ok=True)
ashawkey's avatar
ashawkey committed
120

121
122
123
if os.path.isdir(opt.test_path):
    file_paths = glob.glob(os.path.join(opt.test_path, "*"))
    for path in tqdm.tqdm(file_paths):
ashawkey's avatar
update  
ashawkey committed
124
125
126
127
        try:
            run(path)
        except Exception as e:
            print(f'[WARN] {path} failed: {e}')
128
129
else:
    run(opt.test_path)