Skip to content

Commit 5cb273d

Browse files
committed
Add script to rasterize meshes.
1 parent 0cfd54a commit 5cb273d

1 file changed

Lines changed: 145 additions & 0 deletions

File tree

  • components/image_simulation/wmtk/components/image_simulation/scripts
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import igl
2+
import numpy as np
3+
import argparse
4+
import nrrd
5+
6+
"""
7+
Rasterize a mesh into a voxel grid using libigl's winding number function.
8+
Saves the resulting voxel grid as an ASCII RAW file with padding.
9+
10+
Usage:
11+
python rasterize_mesh.py --i input_mesh.obj --o output_volume.raw --nx 50 --ny 50 --nz 50
12+
13+
Arguments:
14+
--i: Path to the input mesh file (OBJ format). Multiple files can be provided.
15+
--o: Path to the output volume file (RAW format). If the filename ends with .nrrd, the output will be saved in NRRD format.
16+
--nx: Dimension of the voxel grid in the X direction (optional, default = 50).
17+
--ny: Dimension of the voxel grid in the Y direction (optional, default = 50).
18+
--nz: Dimension of the voxel grid in the Z direction (optional, default = 50).
19+
"""
20+
21+
22+
def add_padding(array):
23+
dim1, dim2, dim3 = array.shape
24+
padded_array = np.zeros((dim1 + 2, dim2 + 2, dim3 + 2), dtype=array.dtype)
25+
padded_array[1:-1, 1:-1, 1:-1] = array
26+
return padded_array
27+
28+
29+
def write_array_data_ascii(filename, data):
30+
dim1, dim2, dim3 = data.shape
31+
32+
with open(filename, "w") as f:
33+
f.write(str(dim1) + " ")
34+
f.write(str(dim2) + " ")
35+
f.write(str(dim3))
36+
f.write("\n\n")
37+
for i in range(data.shape[0]):
38+
for j in range(data.shape[1]):
39+
for k in range(data.shape[2]):
40+
v = int(data[i][j][k])
41+
f.write(str(v) + " ")
42+
f.write("\n")
43+
f.write("\n")
44+
45+
46+
if __name__ == "__main__":
47+
# parse arguments
48+
49+
parser = argparse.ArgumentParser(description="Rasterize a mesh into a voxel grid.")
50+
parser.add_argument(
51+
"--i",
52+
type=str,
53+
nargs="+",
54+
default=[],
55+
help="Path to the input mesh file (OBJ format).",
56+
)
57+
parser.add_argument(
58+
"--o", type=str, help="Path to the output volume file (RAW format)."
59+
)
60+
parser.add_argument(
61+
"--nx",
62+
type=int,
63+
default=50,
64+
help="Dimension of the voxel grid in the X direction.",
65+
)
66+
parser.add_argument(
67+
"--ny",
68+
type=int,
69+
default=50,
70+
help="Dimension of the voxel grid in the Y direction.",
71+
)
72+
parser.add_argument(
73+
"--nz",
74+
type=int,
75+
default=50,
76+
help="Dimension of the voxel grid in the Z direction.",
77+
)
78+
79+
args = parser.parse_args()
80+
n = np.array([args.nx, args.ny, args.nz])
81+
82+
if len(args.i) == 0:
83+
print("Please provide one or multiple input meshes file using --i")
84+
exit(1)
85+
86+
# get bbox from all meshes
87+
all_V = []
88+
all_F = []
89+
for mesh_file in args.i:
90+
V, F = igl.read_triangle_mesh(mesh_file)
91+
# Ensure arrays are the expected dtype and memory layout (row-major / C-contiguous).
92+
# pybind11 / libigl bindings often require matching memory layout between inputs.
93+
V = np.ascontiguousarray(V, dtype=np.float64)
94+
F = np.ascontiguousarray(F, dtype=np.int32)
95+
96+
all_V.append(V)
97+
all_F.append(F)
98+
99+
all_V_stacked = np.vstack(all_V)
100+
101+
# get bbox from vertices
102+
bbox_min = np.min(all_V_stacked, axis=0)
103+
bbox_max = np.max(all_V_stacked, axis=0)
104+
diag = bbox_max - bbox_min
105+
# increase bbox by 5% in each direction
106+
bbox_min -= 0.05 * diag
107+
bbox_max += 0.05 * diag
108+
109+
dim = bbox_max - bbox_min
110+
spacing = dim / n
111+
112+
grid_points = (
113+
np.mgrid[
114+
bbox_min[0] : bbox_max[0] : spacing[0],
115+
bbox_min[1] : bbox_max[1] : spacing[1],
116+
bbox_min[2] : bbox_max[2] : spacing[2],
117+
]
118+
.reshape(3, -1)
119+
.T
120+
)
121+
# Make sure the query points are C-contiguous and float64 (row-major)
122+
grid_points = np.ascontiguousarray(grid_points, dtype=np.float64)
123+
124+
volume = np.zeros(n, dtype=np.uint8)
125+
volume = add_padding(volume)
126+
127+
for i in range(0, len(all_V)):
128+
V = all_V[i]
129+
F = all_F[i]
130+
131+
inside = igl.winding_number(V, F, grid_points) > 0.5
132+
133+
vol = inside.reshape(n).astype(np.uint8)
134+
vol = add_padding(vol)
135+
assert vol.shape == volume.shape
136+
vol = (i + 1) * vol # label the volume with mesh index + 1
137+
138+
volume = np.maximum(volume, vol) # combine volumes
139+
140+
if args.o.endswith(".nrrd"):
141+
nrrd.write(args.o, volume)
142+
else:
143+
write_array_data_ascii(args.o, volume)
144+
145+
print(f"=== Finished rasterization and wrote {args.o} ===")

0 commit comments

Comments
 (0)