extract_mesh_watertight.py 3.01 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
17

import kiui
from kiui.mesh import Mesh

18
19
20
"""
Extract watertight mesh from a arbitrary mesh by UDF expansion and floodfill.
"""
ashawkey's avatar
ashawkey committed
21
parser = argparse.ArgumentParser()
22
parser.add_argument('test_path', type=str)
ashawkey's avatar
ashawkey committed
23
parser.add_argument('--res', type=int, default=512)
24
parser.add_argument('--workspace', type=str, default='output')
ashawkey's avatar
ashawkey committed
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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]

def run(path):
    
    mesh = Mesh.load(path, wotex=True, bound=0.95, device=device)

    t0 = time.time()
    BVH = cubvh.cuBVH(mesh.v, mesh.f)
    print('BVH build time:', time.time() - t0)
45
    eps = 2 / opt.res
ashawkey's avatar
ashawkey committed
46
47
48
49
50
51
52
53
54
55
56

    # 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)
    print('UDF time:', time.time() - t0)
    udf = udf.cpu().numpy().reshape(opt.res, opt.res, opt.res)
57
    occ = udf < eps # tolerance 2 voxels
ashawkey's avatar
ashawkey committed
58
59
60
61

    t0 = time.time()
    empty_mask = morphology.flood(occ, (0, 0, 0), connectivity=1) # flood from the corner, which is for sure empty
    print('Floodfill time:', time.time() - t0)
62
63

    # binary occupancy
ashawkey's avatar
ashawkey committed
64
65
    occ = ~empty_mask

66
67
68
69
70
    # truncated SDF
    sdf = udf - eps  # inner is negative
    inner_mask = occ & (sdf > 0)
    sdf[inner_mask] *= -1

ashawkey's avatar
ashawkey committed
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
    # # 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()
87
88
89
90
91
    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
92
93
    print('MC time:', time.time() - t0)

94
95
96
97
    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
98

99
100
101
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
102
103
104
105
        try:
            run(path)
        except Exception as e:
            print(f'[WARN] {path} failed: {e}')
106
107
else:
    run(opt.test_path)