Skip to content

Commit 7650d96

Browse files
committed
[port] further debug m2v, mesh2vol, mesh2mask, edge/size are wrong
1 parent 1ced258 commit 7650d96

File tree

1 file changed

+163
-95
lines changed

1 file changed

+163
-95
lines changed

iso2mesh/raster.py

Lines changed: 163 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import numpy as np
1414
import matplotlib.pyplot as plt
15+
from iso2mesh.modify import qmeshcut
1516

1617
##====================================================================================
1718
## implementations
@@ -33,70 +34,100 @@ def m2v(*args):
3334

3435
def mesh2vol(node, elem, xi, yi=None, zi=None):
3536
"""
36-
Fast rasterization of a 3D mesh to a volume with tetrahedron index labels.
37+
mesh2vol(node, elem, xi, yi=None, zi=None)
38+
39+
Fast rasterization of a 3D tetrahedral mesh into a volumetric label image.
3740
3841
Parameters:
39-
node: Node coordinates (Nx3 array)
40-
elem: Tetrahedron element list (Nx4 array)
41-
xi: Grid or number of divisions along x-axis
42-
yi: (Optional) Grid along y-axis
43-
zi: (Optional) Grid along z-axis
42+
node : ndarray
43+
Node coordinates (N x 3) or (N x 4, with values in 4th column)
44+
elem : ndarray
45+
Tetrahedral elements (M x 4 or M x >4)
46+
xi, yi, zi : array-like or scalar
47+
Grid definitions. Supports:
48+
- scalar: voxel resolution
49+
- [Nx, Ny, Nz]: volume size
50+
- xi, yi, zi: actual grid vectors
4451
4552
Returns:
46-
mask: 3D volume where voxel values correspond to the tetrahedron index
47-
weight: (Optional) Barycentric weights for each voxel
53+
mask : 3D ndarray
54+
Voxelized volume with element labels
55+
weight : 4 x Nx x Ny x Nz array (if requested or values present)
56+
57+
Author:
58+
Qianqian Fang <q.fang at neu.edu>
4859
"""
4960

50-
# Check if xi is scalar or list of grid divisions
51-
if isinstance(xi, (int, float)) and yi is None and zi is None:
52-
mn = np.min(node, axis=0)
53-
mx = np.max(node, axis=0)
54-
df = (mx[:3] - mn[:3]) / xi
55-
elif len(xi) == 3 and yi is None and zi is None:
56-
mn = np.min(node, axis=0)
57-
mx = np.max(node, axis=0)
58-
df = (mx[:3] - mn[:3]) / xi
59-
elif yi is not None and zi is not None:
60-
mx = [np.max(xi), np.max(yi), np.max(zi)]
61-
mn = [np.min(xi), np.min(yi), np.min(zi)]
62-
df = [np.min(np.diff(xi)), np.min(np.diff(yi)), np.min(np.diff(zi))]
61+
node = np.array(node, dtype=np.float64)
62+
elem = np.array(elem, dtype=np.int32)
63+
64+
nodeval = None
65+
if node.shape[1] == 4:
66+
nodeval = node[:, 3].copy()
67+
node = node[:, :3]
68+
69+
if yi is None and zi is None:
70+
if isinstance(xi, (int, float)):
71+
mn = np.min(node, axis=0)
72+
mx = np.max(node, axis=0)
73+
df = (mx - mn) / xi
74+
elif isinstance(xi, (list, tuple, np.ndarray)) and len(xi) == 3:
75+
mn = np.min(node, axis=0)
76+
mx = np.max(node, axis=0)
77+
df = (mx - mn) / np.array(xi)
78+
else:
79+
raise ValueError(
80+
"xi must be scalar or 3-element vector if yi and zi are not provided"
81+
)
82+
xi = np.arange(mn[0], mx[0] + df[0], df[0])
83+
yi = np.arange(mn[1], mx[1] + df[1], df[1])
84+
zi = np.arange(mn[2], mx[2] + df[2], df[2])
6385
else:
64-
raise ValueError("At least xi input is required")
65-
66-
xi = np.arange(mn[0], mx[0], df[0])
67-
yi = np.arange(mn[1], mx[1], df[1])
68-
zi = np.arange(mn[2], mx[2], df[2])
69-
70-
if node.shape[1] != 3 or elem.shape[1] <= 3:
71-
raise ValueError("node must have 3 columns; elem must have 4 columns")
86+
xi = np.array(xi)
87+
yi = np.array(yi)
88+
zi = np.array(zi)
89+
df = [np.min(np.diff(xi)), np.min(np.diff(yi)), np.min(np.diff(zi))]
7290

73-
mask = np.zeros((len(xi) - 1, len(yi) - 1, len(zi) - 1), dtype=int)
74-
weight = None
91+
if node.shape[1] != 3 or elem.shape[1] < 4:
92+
raise ValueError("node must have 3 columns; elem must have 4 or more columns")
7593

76-
if len(elem.shape) > 1:
77-
weight = np.zeros((4, len(xi) - 1, len(yi) - 1, len(zi) - 1))
94+
nx, ny, nz = len(xi), len(yi), len(zi)
95+
mask = np.zeros((nx, ny, nz))
96+
weight = np.zeros((4, nx, ny, nz)) if nodeval is not None else None
7897

79-
fig = plt.figure()
80-
for i in range(len(zi)):
81-
cutpos, cutvalue, facedata, elemid = qmeshcut(elem, node, zi[i])
82-
if cutpos is None:
98+
for i, zval in enumerate(zi[:-1]):
99+
if nodeval is not None:
100+
cutpos, cutvalue, facedata, elemid, _ = qmeshcut(
101+
elem, node, nodeval, f"z={zval}"
102+
)
103+
else:
104+
cutpos, cutvalue, facedata, elemid, _ = qmeshcut(
105+
elem, node, node[:, 0], f"z={zval}"
106+
)
107+
if cutpos is None or len(cutpos) == 0:
83108
continue
84109

85-
maskz, weightz = mesh2mask(cutpos, facedata, xi, yi, fig)
86-
idx = np.where(~np.isnan(maskz))
87-
mask[:, :, i] = maskz
88-
89110
if weight is not None:
90-
eid = facedata[maskz[idx]]
91-
maskz[idx] = (
111+
maskz, weightz = mesh2mask(cutpos, facedata, xi, yi)
112+
weight[:, :, :, i] = weightz
113+
else:
114+
maskz = mesh2mask(cutpos, facedata, xi, yi)[0]
115+
116+
idx = ~np.isnan(maskz)
117+
if nodeval is not None:
118+
eid = facedata[maskz[idx].astype(int) - 1] # 1-based to 0-based
119+
maskz_flat = (
92120
cutvalue[eid[:, 0]] * weightz[0, idx]
93121
+ cutvalue[eid[:, 1]] * weightz[1, idx]
94122
+ cutvalue[eid[:, 2]] * weightz[2, idx]
95123
+ cutvalue[eid[:, 3]] * weightz[3, idx]
96124
)
97-
weight[:, :, :, i] = weightz
125+
maskz[idx] = maskz_flat
126+
else:
127+
maskz[idx] = elemid[(maskz[idx] - 1).astype(int)] # adjust 1-based index
128+
129+
mask[:, :, i] = maskz
98130

99-
plt.close(fig)
100131
return mask, weight
101132

102133

@@ -115,6 +146,9 @@ def mesh2mask(node, face, xi, yi=None, hf=None):
115146
mask: 2D image where pixel values correspond to the triangle index
116147
weight: (Optional) Barycentric weights for each triangle
117148
"""
149+
from matplotlib.collections import PatchCollection
150+
from matplotlib.patches import Polygon
151+
import matplotlib.cm as cm
118152

119153
# Determine grid size from inputs
120154
if isinstance(xi, (int, float)) and yi is None:
@@ -138,78 +172,112 @@ def mesh2mask(node, face, xi, yi=None, hf=None):
138172
"node must have 2 or 3 columns; face must have at least 3 columns"
139173
)
140174

141-
# If no figure handle is provided, create one
142-
if hf is None:
143-
fig = plt.figure()
144-
else:
145-
plt.clf()
175+
fig = (
176+
plt.figure(
177+
figsize=(xi.size * 0.01, yi.size * 0.01), dpi=100, layout="compressed"
178+
)
179+
if hf is None
180+
else hf
181+
)
182+
ax = fig.add_subplot(111)
183+
ax.set_position([0, 0, 1, 1])
184+
ax.set_xlim(mn[0], mx[0])
185+
ax.set_ylim(mn[1], mx[1])
186+
ax.set_axis_off()
187+
188+
colors = cm.gray(np.linspace(0, 1, len(face)))
189+
190+
patches = []
191+
for i, f in enumerate(face[:, :3]):
192+
polygon = Polygon(
193+
node[f - 1, :2],
194+
closed=True,
195+
edgecolor="none",
196+
linewidth=0,
197+
linestyle="none",
198+
)
199+
patches.append(polygon)
200+
201+
collection = PatchCollection(
202+
patches, facecolors=colors, linewidths=0.01, edgecolors="none", edgecolor="face"
203+
)
204+
ax.add_collection(collection)
146205

147-
# Rasterize the mesh to an image
148-
plt.gca().patch.set_visible(False)
149-
plt.gca().set_position([0, 0, 1, 1])
206+
plt.draw()
207+
fig.canvas.draw()
208+
img = np.array(fig.canvas.renderer.buffer_rgba())
209+
mask_raw = img[:, :, 0]
210+
mask = np.zeros(mask_raw.shape, dtype=np.int32)
211+
color_vals = (colors[:, :3] * 255).astype(np.uint8)
150212

151-
cmap = plt.get_cmap("jet", len(face))
152-
plt.pcolormesh(node[:, 0], node[:, 1], np.arange(len(face)), cmap=cmap)
213+
for idx, cval in enumerate(color_vals):
214+
match = np.all(img[:, :, :3] == cval, axis=-1)
215+
mask[match] = idx + 1
153216

154-
# Set axis limits
155-
plt.xlim([mn[0], mx[0]])
156-
plt.ylim([mn[1], mx[1]])
157-
plt.clim([1, len(face)])
158-
output_size = np.round((mx[:2] - mn[:2]) / df).astype(int)
217+
mask = mask[: len(yi), : len(xi)].T
218+
weight = barycentricgrid(node, face, xi, yi, mask)
159219

160-
# Rendering or saving to image
161-
mask = np.zeros(output_size, dtype=np.int32)
162220
if hf is None:
163221
plt.close(fig)
164-
165-
# Optional weight calculation (if requested)
166-
weight = None
167-
if yi is not None:
168-
weight = barycentricgrid(node, face, xi, yi, mask)
169-
170222
return mask, weight
171223

172224

173225
def barycentricgrid(node, face, xi, yi, mask):
174226
"""
175-
Compute barycentric weights for a grid.
227+
Compute barycentric weights for a 2D triangle mesh over a pixel grid.
176228
177229
Parameters:
178-
node: Node coordinates
179-
face: Triangle surface
180-
xi: x-axis grid
181-
yi: y-axis grid
182-
mask: Rasterized triangle mask
230+
node : ndarray (N, 2 or 3)
231+
Node coordinates.
232+
face : ndarray (M, 3)
233+
Triangle face indices (1-based).
234+
xi, yi : 1D arrays
235+
Grid coordinate vectors.
236+
mask : 2D ndarray
237+
Label image where each pixel contains the triangle index (1-based), NaN if outside.
183238
184239
Returns:
185-
weight: Barycentric weights for each triangle
240+
weight : ndarray (3, H, W)
241+
Barycentric coordinate weights for each pixel inside a triangle.
186242
"""
187-
xx, yy = np.meshgrid(xi, yi)
188-
idx = ~np.isnan(mask)
189-
eid = mask[idx]
243+
xx, yy = np.meshgrid(xi, yi, indexing="ij") # shape: (H, W)
244+
mask = mask.astype(float)
245+
valid_idx = ~np.isnan(mask)
190246

191-
t1 = node[face[:, 0], :]
192-
t2 = node[face[:, 1], :]
193-
t3 = node[face[:, 2], :]
247+
# 1-based to 0-based index
248+
eid = mask[valid_idx].astype(int) - 1
194249

195-
# Calculate barycentric coordinates
250+
# triangle vertices (all triangles)
251+
t1 = node[face[:, 0] - 1]
252+
t2 = node[face[:, 1] - 1]
253+
t3 = node[face[:, 2] - 1]
254+
255+
# denominator (twice the area of each triangle)
196256
tt = (t2[:, 1] - t3[:, 1]) * (t1[:, 0] - t3[:, 0]) + (t3[:, 0] - t2[:, 0]) * (
197257
t1[:, 1] - t3[:, 1]
198258
)
199-
w = np.zeros((len(idx), 3))
200-
w[:, 0] = (t2[eid, 1] - t3[eid, 1]) * (xx[idx] - t3[eid, 0]) + (
259+
260+
# numerator for w1 and w2 (barycentric weights)
261+
w1 = (t2[eid, 1] - t3[eid, 1]) * (xx[valid_idx] - t3[eid, 0]) + (
201262
t3[eid, 0] - t2[eid, 0]
202-
) * (yy[idx] - t3[eid, 1])
203-
w[:, 1] = (t3[eid, 1] - t1[eid, 1]) * (xx[idx] - t3[eid, 0]) + (
263+
) * (yy[valid_idx] - t3[eid, 1])
264+
w2 = (t3[eid, 1] - t1[eid, 1]) * (xx[valid_idx] - t3[eid, 0]) + (
204265
t1[eid, 0] - t3[eid, 0]
205-
) * (yy[idx] - t3[eid, 1])
206-
w[:, 0] /= tt[eid]
207-
w[:, 1] /= tt[eid]
208-
w[:, 2] = 1 - w[:, 0] - w[:, 1]
209-
210-
weight = np.zeros((3, mask.shape[0], mask.shape[1]))
211-
weight[0, idx] = w[:, 0]
212-
weight[1, idx] = w[:, 1]
213-
weight[2, idx] = w[:, 2]
266+
) * (yy[valid_idx] - t3[eid, 1])
267+
268+
w1 = w1 / tt[eid]
269+
w2 = w2 / tt[eid]
270+
w3 = 1 - w1 - w2
271+
272+
# Assemble the weight volume
273+
weight = np.zeros((3, *mask.shape), dtype=np.float32)
274+
ww = np.zeros_like(mask, dtype=np.float32)
275+
276+
ww[valid_idx] = w1
277+
weight[0, :, :] = ww
278+
ww[valid_idx] = w2
279+
weight[1, :, :] = ww
280+
ww[valid_idx] = w3
281+
weight[2, :, :] = ww
214282

215283
return weight

0 commit comments

Comments
 (0)