整理代码
This commit is contained in:
1 parent
17446d4053
commit
22bfc8adfb
1 file changed
+199
-199
+199
-199
@@ -21,6 +21,10 @@ import numpy as np
|
||||
from collections import deque
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
class FillMeshGenerator:
|
||||
"""眼镜-脸部填充网格生成器"""
|
||||
|
||||
# ======================================================================
|
||||
# 参数配置
|
||||
# ======================================================================
|
||||
@@ -48,16 +52,22 @@ BITER = 25 # 边界 Laplacian 平滑迭代
|
||||
BLAM = 0.6 # 边界平滑系数
|
||||
TOL = 1e-4 # Jacobi 收敛阈值
|
||||
|
||||
report = []
|
||||
def log(s):
|
||||
print(s)
|
||||
report.append(str(s))
|
||||
def __init__(self, in_obj=None, in_idx=None):
|
||||
if in_obj:
|
||||
self.IN_OBJ = in_obj
|
||||
if in_idx:
|
||||
self.IN_IDX = in_idx
|
||||
self.report = []
|
||||
|
||||
def log(self, s):
|
||||
"""日志记录"""
|
||||
print(s)
|
||||
self.report.append(str(s))
|
||||
|
||||
# ======================================================================
|
||||
# IO 工具
|
||||
# ======================================================================
|
||||
def parse_obj(path):
|
||||
def parse_obj(self, path):
|
||||
"""解析 OBJ 文件, 返回顶点数组和面数组"""
|
||||
vs, fs = [], []
|
||||
with open(path) as f:
|
||||
@@ -72,11 +82,10 @@ def parse_obj(path):
|
||||
int(p[3].split("/")[0]) - 1))
|
||||
return np.asarray(vs), np.asarray(fs, dtype=np.int64)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 形态学操作
|
||||
# ======================================================================
|
||||
def dilate_8(mask, iterations=1):
|
||||
def dilate_8(self, mask, iterations=1):
|
||||
"""二值图像膨胀 (8-邻域)"""
|
||||
result = mask.copy()
|
||||
for _ in range(iterations):
|
||||
@@ -88,8 +97,7 @@ def dilate_8(mask, iterations=1):
|
||||
result = nm
|
||||
return result
|
||||
|
||||
|
||||
def erode_4(mask, iterations=1):
|
||||
def erode_4(self, mask, iterations=1):
|
||||
"""二值图像腐蚀 (4-邻域)"""
|
||||
result = mask.copy()
|
||||
for _ in range(iterations):
|
||||
@@ -99,8 +107,7 @@ def erode_4(mask, iterations=1):
|
||||
result = ne
|
||||
return result
|
||||
|
||||
|
||||
def dilate_8_inf(field, iterations=1):
|
||||
def dilate_8_inf(self, field, iterations=1):
|
||||
"""浮点数场膨胀 (8-邻域取最大值)"""
|
||||
result = field.copy()
|
||||
for _ in range(iterations):
|
||||
@@ -119,8 +126,7 @@ def dilate_8_inf(field, iterations=1):
|
||||
result = gf
|
||||
return result
|
||||
|
||||
|
||||
def erode_4_inf(field, iterations=1):
|
||||
def erode_4_inf(self, field, iterations=1):
|
||||
"""浮点数场腐蚀 (4-邻域取最小值)"""
|
||||
result = field.copy()
|
||||
for _ in range(iterations):
|
||||
@@ -135,8 +141,7 @@ def erode_4_inf(field, iterations=1):
|
||||
result = mn
|
||||
return result
|
||||
|
||||
|
||||
def flood_outside(mask):
|
||||
def flood_outside(self, mask):
|
||||
"""从窗口边界洪泛, 标记外部; 未标记且非 mask 即为洞"""
|
||||
outside = np.zeros_like(mask, bool)
|
||||
dq = deque()
|
||||
@@ -158,8 +163,7 @@ def flood_outside(mask):
|
||||
outside[nj, ni] = True; dq.append((nj, ni))
|
||||
return ~outside & ~mask
|
||||
|
||||
|
||||
def largest_component_8(mask):
|
||||
def largest_component_8(self, mask):
|
||||
"""取 mask 的最大连通分量 (8-邻域)"""
|
||||
labels = np.zeros(mask.shape, np.int32)
|
||||
cur = 0
|
||||
@@ -184,37 +188,32 @@ def largest_component_8(mask):
|
||||
stray = int(mask.sum() - sizes[main_label])
|
||||
return labels == main_label, stray
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 数值工具
|
||||
# ======================================================================
|
||||
def pbasis(x, y):
|
||||
def pbasis(self, x, y):
|
||||
"""二次多项式基函数: [1, x, y, x^2, xy, y^2]"""
|
||||
return np.stack([np.ones_like(x), x, y, x * x, x * y, y * y], 1)
|
||||
|
||||
|
||||
def cross3(o, a, b):
|
||||
def cross3(self, o, a, b):
|
||||
"""2D 叉积 (o->a) x (o->b)"""
|
||||
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
|
||||
|
||||
|
||||
def signed_volume(verts, faces):
|
||||
def signed_volume(self, verts, faces):
|
||||
"""计算有符号体积"""
|
||||
a = verts[faces[:, 0]]; b = verts[faces[:, 1]]; c = verts[faces[:, 2]]
|
||||
return float(np.sum(np.einsum('ij,ij->i', a, np.cross(b, c))) / 6.0)
|
||||
|
||||
|
||||
def inside_mask(pts, msk):
|
||||
def inside_mask(self, pts, msk):
|
||||
"""判断点是否在 mask 内"""
|
||||
ii = np.clip(((pts[:, 0] - X0) / RES).astype(int), 0, msk.shape[1] - 1)
|
||||
jj = np.clip(((pts[:, 1] - Y0) / RES).astype(int), 0, msk.shape[0] - 1)
|
||||
ii = np.clip(((pts[:, 0] - self.X0) / self.RES).astype(int), 0, msk.shape[1] - 1)
|
||||
jj = np.clip(((pts[:, 1] - self.Y0) / self.RES).astype(int), 0, msk.shape[0] - 1)
|
||||
return msk[jj, ii]
|
||||
|
||||
|
||||
def bilinear(grid, pts):
|
||||
def bilinear(self, grid, pts):
|
||||
"""双线性插值"""
|
||||
fx = (pts[:, 0] - X0) / RES
|
||||
fy = (pts[:, 1] - Y0) / RES
|
||||
fx = (pts[:, 0] - self.X0) / self.RES
|
||||
fy = (pts[:, 1] - self.Y0) / self.RES
|
||||
i = np.clip(np.floor(fx).astype(int), 0, grid.shape[1] - 2)
|
||||
j = np.clip(np.floor(fy).astype(int), 0, grid.shape[0] - 2)
|
||||
tx = np.clip(fx - i, 0, 1); ty = np.clip(fy - j, 0, 1)
|
||||
@@ -223,12 +222,13 @@ def bilinear(grid, pts):
|
||||
grid[j + 1, i] * (1 - tx) * ty +
|
||||
grid[j + 1, i + 1] * tx * ty)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 平滑与插值
|
||||
# ======================================================================
|
||||
def smooth_valid(field, valid, sigma=SIGMA, it=1):
|
||||
def smooth_valid(self, field, valid, sigma=None, it=1):
|
||||
"""归一化高斯卷积(只在 valid 上)"""
|
||||
if sigma is None:
|
||||
sigma = self.SIGMA
|
||||
rad = int(3 * sigma)
|
||||
k = np.exp(-0.5 * (np.arange(-rad, rad + 1) / sigma) ** 2)
|
||||
field = field.copy()
|
||||
@@ -255,10 +255,8 @@ def smooth_valid(field, valid, sigma=SIGMA, it=1):
|
||||
field = np.where(val, out, field)
|
||||
return field
|
||||
|
||||
|
||||
def laplace_fill(field, known, iters=6000):
|
||||
"""Dirichlet: known 固定, 其余解调和方程
|
||||
缺失邻居(网格边缘)用反射(补 F 自身), 恒除以 4 保证系数和=1(收敛)"""
|
||||
def laplace_fill(self, field, known, iters=6000):
|
||||
"""Dirichlet: known 固定, 其余解调和方程"""
|
||||
F = field.copy()
|
||||
with np.errstate(all='ignore'):
|
||||
mean_known = np.nanmean(F[known]) if known.any() else 0.0
|
||||
@@ -279,23 +277,22 @@ def laplace_fill(field, known, iters=6000):
|
||||
df = np.abs(G - F)[~known]
|
||||
delta = np.nanmax(df) if df.size and np.isfinite(df).any() else 0.0
|
||||
F = G
|
||||
if delta < TOL * RES:
|
||||
if delta < self.TOL * self.RES:
|
||||
break
|
||||
return F
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 步骤 1: 加载模型并分类眼镜面
|
||||
# ======================================================================
|
||||
def load_and_classify():
|
||||
def load_and_classify(self):
|
||||
"""加载模型, 通过边连通主分量提取真眼镜物体"""
|
||||
log("== 1. 读入模型 ==")
|
||||
V, F = parse_obj(IN_OBJ)
|
||||
idx = np.loadtxt(IN_IDX, dtype=np.int64) - 1
|
||||
self.log("== 1. 读入模型 ==")
|
||||
V, F = self.parse_obj(self.IN_OBJ)
|
||||
idx = np.loadtxt(self.IN_IDX, dtype=np.int64) - 1
|
||||
ism = np.zeros(len(V), bool)
|
||||
ism[idx] = True
|
||||
face_g = ism[F].any(1)
|
||||
log(f" 顶点 {len(V)}, 面 {len(F)}, 眼镜标记顶点 {len(idx)}")
|
||||
self.log(f" 顶点 {len(V)}, 面 {len(F)}, 眼镜标记顶点 {len(idx)}")
|
||||
|
||||
# 边连通主分量提取
|
||||
emap = {}
|
||||
@@ -327,38 +324,38 @@ def load_and_classify():
|
||||
|
||||
face_g = np.zeros(len(F), bool)
|
||||
face_g[best] = True
|
||||
log(f" 眼镜物体 = 边连通主分量 {len(best)} 面, "
|
||||
self.log(f" 眼镜物体 = 边连通主分量 {len(best)} 面, "
|
||||
f"剥离误标皮肤 {int((ism[F].any(1)).sum() - len(best))} 面")
|
||||
return V, F, face_g
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 步骤 2: 栅格化
|
||||
# ======================================================================
|
||||
def rasterize(V, F, face_g):
|
||||
def rasterize(self, V, F, face_g):
|
||||
"""栅格化三角形到高度场"""
|
||||
W = int(round((X1 - X0) / RES)) + 1
|
||||
H = int(round((Y1 - Y0) / RES)) + 1
|
||||
log(f"== 2. 栅格化 (窗口 x[{X0},{X1}] y[{Y0},{Y1}], res={RES}, {W}x{H}) ==")
|
||||
W = int(round((self.X1 - self.X0) / self.RES)) + 1
|
||||
H = int(round((self.Y1 - self.Y0) / self.RES)) + 1
|
||||
self.log(f"== 2. 栅格化 (窗口 x[{self.X0},{self.X1}] y[{self.Y0},{self.Y1}], "
|
||||
f"res={self.RES}, {W}x{H}) ==")
|
||||
|
||||
gz1 = np.full((H, W), -np.inf) # 眼镜最前表面
|
||||
gz2 = np.full((H, W), -np.inf) # 眼镜第二表面(镜框管背面)
|
||||
fz = np.full((H, W), -np.inf) # 非眼镜最前表面(脸)
|
||||
bz = np.full((H, W), np.inf) # 全网格最后表面(后脑)
|
||||
|
||||
inwin = ((V[:, 0] > X0 - 0.5) & (V[:, 0] < X1 + 0.5) &
|
||||
(V[:, 1] > Y0 - 0.5) & (V[:, 1] < Y1 + 0.5))
|
||||
inwin = ((V[:, 0] > self.X0 - 0.5) & (V[:, 0] < self.X1 + 0.5) &
|
||||
(V[:, 1] > self.Y0 - 0.5) & (V[:, 1] < self.Y1 + 0.5))
|
||||
fwin = inwin[F].all(1)
|
||||
log(f" 窗口内三角形 {int(fwin.sum())} (其中眼镜 {(face_g & fwin).sum()})")
|
||||
self.log(f" 窗口内三角形 {int(fwin.sum())} (其中眼镜 {(face_g & fwin).sum()})")
|
||||
|
||||
A, B, C = V[F[:, 0]], V[F[:, 1]], V[F[:, 2]]
|
||||
for t in np.where(fwin)[0]:
|
||||
ax, ay, az = A[t]; bx, by, bz_ = B[t]; cx, cy, cz = C[t]
|
||||
g = face_g[t]
|
||||
i0 = max(int((min(ax, bx, cx) - X0) / RES), 0)
|
||||
i1 = min(int((max(ax, bx, cx) - X0) / RES), W - 1)
|
||||
j0 = max(int((min(ay, by, cy) - Y0) / RES), 0)
|
||||
j1 = min(int((max(ay, by, cy) - Y0) / RES), H - 1)
|
||||
i0 = max(int((min(ax, bx, cx) - self.X0) / self.RES), 0)
|
||||
i1 = min(int((max(ax, bx, cx) - self.X0) / self.RES), W - 1)
|
||||
j0 = max(int((min(ay, by, cy) - self.Y0) / self.RES), 0)
|
||||
j1 = min(int((max(ay, by, cy) - self.Y0) / self.RES), H - 1)
|
||||
if i1 < i0 or j1 < j0:
|
||||
continue
|
||||
v0x, v0y = cx - ax, cy - ay
|
||||
@@ -370,9 +367,9 @@ def rasterize(V, F, face_g):
|
||||
if abs(den) < 1e-14:
|
||||
continue
|
||||
for j in range(j0, j1 + 1):
|
||||
py = Y0 + j * RES
|
||||
py = self.Y0 + j * self.RES
|
||||
for i in range(i0, i1 + 1):
|
||||
px = X0 + i * RES
|
||||
px = self.X0 + i * self.RES
|
||||
v2x, v2y = px - ax, py - ay
|
||||
d02 = v0x * v2x + v0y * v2y
|
||||
d12 = v1x * v2x + v1y * v2y
|
||||
@@ -383,10 +380,10 @@ def rasterize(V, F, face_g):
|
||||
z = az + u * (cz - az) + v * (bz_ - az)
|
||||
if g:
|
||||
if z > gz1[j, i] + 1e-9:
|
||||
if z - gz1[j, i] > SEP and gz1[j, i] > -np.inf:
|
||||
if z - gz1[j, i] > self.SEP and gz1[j, i] > -np.inf:
|
||||
gz2[j, i] = max(gz2[j, i], gz1[j, i])
|
||||
gz1[j, i] = z
|
||||
elif z < gz1[j, i] - SEP and z > gz2[j, i]:
|
||||
elif z < gz1[j, i] - self.SEP and z > gz2[j, i]:
|
||||
gz2[j, i] = z
|
||||
else:
|
||||
if z > fz[j, i]:
|
||||
@@ -399,26 +396,25 @@ def rasterize(V, F, face_g):
|
||||
have_f = np.isfinite(fz) & (fz > -1e30)
|
||||
have_b = np.isfinite(bz) & (bz < 1e30)
|
||||
g1f = have_g1
|
||||
log(f" 有眼镜表面格 {have_g1.sum()}, 管状(双面) {have_g2.sum()}, "
|
||||
self.log(f" 有眼镜表面格 {have_g1.sum()}, 管状(双面) {have_g2.sum()}, "
|
||||
f"有脸面 {have_f.sum()}, 有背面 {have_b.sum()}")
|
||||
|
||||
return gz1, gz2, fz, bz, have_g1, have_g2, have_f, have_b, g1f, W, H
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 步骤 3: 构建足迹 mask 与填充区域 R
|
||||
# ======================================================================
|
||||
def build_footprint(g1f):
|
||||
def build_footprint(self, g1f):
|
||||
"""构建眼镜足迹 mask 和填充区域 R"""
|
||||
log("== 3. 眼镜足迹 / 填充区域 R ==")
|
||||
self.log("== 3. 眼镜足迹 / 填充区域 R ==")
|
||||
mask = g1f.copy()
|
||||
|
||||
# 取主分量
|
||||
mask, n_stray = largest_component_8(mask)
|
||||
log(f" 足迹取主分量: 剔除杂散块 {n_stray} 格 (误标到镜框外的皮肤/后脑)")
|
||||
mask, n_stray = self.largest_component_8(mask)
|
||||
self.log(f" 足迹取主分量: 剔除杂散块 {n_stray} 格 (误标到镜框外的皮肤/后脑)")
|
||||
|
||||
# 中央区剪除(鼻侧多余填充)
|
||||
holes = ~mask & ~flood_outside(mask)
|
||||
holes = ~mask & ~self.flood_outside(mask)
|
||||
hl = np.zeros(mask.shape, np.int32)
|
||||
big_holes = []
|
||||
for j in range(mask.shape[0]):
|
||||
@@ -453,27 +449,27 @@ def build_footprint(g1f):
|
||||
cut[:y_cut, x_in_l + 1:x_in_r] = True
|
||||
n_cut = int((mask & cut).sum())
|
||||
mask &= ~cut
|
||||
log(f" 中央区剪除(两镜片开口之间, 开口底缘 y>{Y0 + y_cut * RES:.1f} 以下): "
|
||||
self.log(f" 中央区剪除(两镜片开口之间, 开口底缘 y>{self.Y0 + y_cut * self.RES:.1f} 以下): "
|
||||
f"剔除 {n_cut} 格误标鼻面")
|
||||
else:
|
||||
log(" ! 未找到两个镜片开口, 跳过中央区剪除")
|
||||
self.log(" ! 未找到两个镜片开口, 跳过中央区剪除")
|
||||
|
||||
# 膨胀 3 格
|
||||
mask = dilate_8(mask, 3)
|
||||
mask = self.dilate_8(mask, 3)
|
||||
|
||||
# 补内部洞 (镜片开口)
|
||||
holes = flood_outside(mask)
|
||||
log(f" 足迹 {mask.sum()} 格, 补内部洞 {holes.sum()} 格 (镜片开口)")
|
||||
holes = self.flood_outside(mask)
|
||||
self.log(f" 足迹 {mask.sum()} 格, 补内部洞 {holes.sum()} 格 (镜片开口)")
|
||||
mask = mask | holes
|
||||
|
||||
# 腐蚀
|
||||
eroded = erode_4(mask, ERODE)
|
||||
eroded = self.erode_4(mask, self.ERODE)
|
||||
|
||||
# y 裁剪 + 最大连通块
|
||||
H, W = eroded.shape
|
||||
jj = np.arange(H)
|
||||
eroded = eroded & (jj[:, None] >= int((RY0 - Y0) / RES)) & \
|
||||
(jj[:, None] <= int((RY1 - Y0) / RES))
|
||||
eroded = eroded & (jj[:, None] >= int((self.RY0 - self.Y0) / self.RES)) & \
|
||||
(jj[:, None] <= int((self.RY1 - self.Y0) / self.RES))
|
||||
|
||||
R = np.zeros_like(eroded)
|
||||
seen = np.zeros_like(eroded, bool)
|
||||
@@ -503,26 +499,26 @@ def build_footprint(g1f):
|
||||
comp, touch = best
|
||||
for j, i in comp:
|
||||
R[j, i] = True
|
||||
log(f" R: {R.sum()} 格 ({R.sum()*RES*RES:.2f} 面积单位), "
|
||||
self.log(f" R: {R.sum()} 格 ({R.sum()*self.RES*self.RES:.2f} 面积单位), "
|
||||
f"触碰窗口边界={touch} (触碰=侧墙切在头内部, 正常)")
|
||||
if R.sum() < 200:
|
||||
raise SystemExit("R 过小")
|
||||
|
||||
return mask, R
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 步骤 4: 构造填充前/后高度场
|
||||
# ======================================================================
|
||||
def build_height_fields(gz1, gz2, fz, bz, have_g1, have_f, have_b, g1f, mask, R):
|
||||
def build_height_fields(self, gz1, gz2, fz, bz, have_g1, have_f, have_b, g1f, mask, R):
|
||||
"""构造填充前/后高度场"""
|
||||
log("== 4. 构造填充前/后高度场 ==")
|
||||
self.log("== 4. 构造填充前/后高度场 ==")
|
||||
H, W = gz1.shape
|
||||
gx, gy = np.meshgrid(X0 + np.arange(W) * RES, Y0 + np.arange(H) * RES)
|
||||
gx, gy = np.meshgrid(self.X0 + np.arange(W) * self.RES,
|
||||
self.Y0 + np.arange(H) * self.RES)
|
||||
|
||||
# ---- 修补眼镜表面 ----
|
||||
g1c = dilate_8(g1f, 2)
|
||||
g1c = erode_4(g1c, 2)
|
||||
g1c = self.dilate_8(g1f, 2)
|
||||
g1c = self.erode_4(g1c, 2)
|
||||
g1c |= g1f
|
||||
|
||||
# 修补针孔
|
||||
@@ -545,66 +541,66 @@ def build_height_fields(gz1, gz2, fz, bz, have_g1, have_f, have_b, g1f, mask, R)
|
||||
nbmed = np.nanmedian(np.where(valid_nb, nb, np.nan), axis=0)
|
||||
pin = g1f & np.isfinite(nbmed) & (gz1 < nbmed - 0.4)
|
||||
|
||||
gz1d = dilate_8_inf(gz1f, 3)
|
||||
gz1d = self.dilate_8_inf(gz1f, 3)
|
||||
gz1f = np.where(g1f & ~pin, gz1, gz1d)
|
||||
log(f" 顶面针孔修补: {int(pin.sum())} 格 (孤立低谷, 用邻域最大值补回顶面)")
|
||||
self.log(f" 顶面针孔修补: {int(pin.sum())} 格 (孤立低谷, 用邻域最大值补回顶面)")
|
||||
nfill = int((g1c & ~g1f & R & (gz1f >= 7.0)).sum())
|
||||
log(f" 镜框带补栅格化角落缺口: {nfill} 格")
|
||||
self.log(f" 镜框带补栅格化角落缺口: {nfill} 格")
|
||||
|
||||
# ---- 框前面拟合 ----
|
||||
mfit = g1c & R & (gz1f >= 7.0)
|
||||
|
||||
# 框带定义
|
||||
eR = erode_4(R, 3)
|
||||
eR = self.erode_4(R, 3)
|
||||
outer_band = R & ~eR
|
||||
frame_band = (g1c & R) | (outer_band & (gz1f > -1e29))
|
||||
|
||||
# 陡斜坡外缘钳制
|
||||
mdil = np.where(g1f, gz1, np.inf)
|
||||
mdil = erode_4_inf(mdil, 3)
|
||||
mdil = self.erode_4_inf(mdil, 3)
|
||||
fringe = frame_band & ~g1f
|
||||
gz1f = np.where(fringe & (mdil < 1e29), np.minimum(gz1f, mdil), gz1f)
|
||||
gz1f = np.where(fringe, smooth_valid(gz1f, fringe, sigma=2.0, it=1), gz1f)
|
||||
gz1f = np.where(fringe, self.smooth_valid(gz1f, fringe, sigma=2.0, it=1), gz1f)
|
||||
|
||||
# 稳健二次拟合
|
||||
xp, yp, zp = gx[mfit], gy[mfit], gz1f[mfit]
|
||||
wt = np.ones(len(zp))
|
||||
for _ in range(3):
|
||||
Aw = pbasis(xp, yp) * wt[:, None]
|
||||
Aw = self.pbasis(xp, yp) * wt[:, None]
|
||||
coef, *_ = np.linalg.lstsq(Aw, zp * wt, rcond=None)
|
||||
rfit = zp - pbasis(xp, yp) @ coef
|
||||
rfit = zp - self.pbasis(xp, yp) @ coef
|
||||
wt = 1.0 / (1.0 + (np.abs(rfit) / 0.5) ** 2)
|
||||
zfit = (pbasis(gx.ravel(), gy.ravel()) @ coef).reshape(H, W)
|
||||
zfit = (self.pbasis(gx.ravel(), gy.ravel()) @ coef).reshape(H, W)
|
||||
rf = np.abs((zfit - gz1f)[mfit])
|
||||
log(f" 框前面拟合(稳健): 残差 mean {rf.mean():.3f}, p90 {np.percentile(rf, 90):.3f}, "
|
||||
self.log(f" 框前面拟合(稳健): 残差 mean {rf.mean():.3f}, p90 {np.percentile(rf, 90):.3f}, "
|
||||
f"max {rf.max():.3f}")
|
||||
|
||||
# 残差场调和延拓
|
||||
mfit2 = mfit & (np.abs(gz1f - zfit) <= 1.0)
|
||||
resf = np.full((H, W), np.nan)
|
||||
resf[mfit2] = gz1f[mfit2] - zfit[mfit2]
|
||||
resf = smooth_valid(resf, mfit2, sigma=2.5)
|
||||
Rext = laplace_fill(resf, mfit2)
|
||||
resf = self.smooth_valid(resf, mfit2, sigma=2.5)
|
||||
Rext = self.laplace_fill(resf, mfit2)
|
||||
Rext = np.where(R, Rext, np.nan)
|
||||
|
||||
# ---- 前表面 ----
|
||||
hf = np.full((H, W), np.nan)
|
||||
hf[frame_band] = gz1f[frame_band] - FLUSH
|
||||
hf[frame_band] = gz1f[frame_band] - self.FLUSH
|
||||
openf = R & ~frame_band
|
||||
hfo = np.where(openf, zfit + Rext - FLUSH, np.nan)
|
||||
hfo = np.where(openf, zfit + Rext - self.FLUSH, np.nan)
|
||||
|
||||
# 开口区安全钳制
|
||||
gmax2 = np.where(mfit, gz1f, -np.inf)
|
||||
gmax2 = dilate_8_inf(gmax2, 3)
|
||||
hfo = np.where(openf & (gmax2 > -1e29), np.minimum(hfo, gmax2 - FLUSH), hfo)
|
||||
hfo = smooth_valid(hfo, openf, sigma=2.0, it=2)
|
||||
gmax2 = self.dilate_8_inf(gmax2, 3)
|
||||
hfo = np.where(openf & (gmax2 > -1e29), np.minimum(hfo, gmax2 - self.FLUSH), hfo)
|
||||
hfo = self.smooth_valid(hfo, openf, sigma=2.0, it=2)
|
||||
hf = np.where(openf & np.isfinite(hfo), hfo, hf)
|
||||
hf = smooth_valid(hf, R, sigma=1.5)
|
||||
hf = self.smooth_valid(hf, R, sigma=1.5)
|
||||
|
||||
# 墙区安全钳制
|
||||
edge_in = erode_4(R, 4)
|
||||
edge_in = self.erode_4(R, 4)
|
||||
wall_zone = R & ~edge_in
|
||||
hf = np.where(frame_band & wall_zone, np.minimum(hf, gz1f - FLUSH), hf)
|
||||
hf = np.where(frame_band & wall_zone, np.minimum(hf, gz1f - self.FLUSH), hf)
|
||||
hf = np.where(frame_band & ~wall_zone, np.minimum(hf, gz1f + 0.10), hf)
|
||||
|
||||
# 交界连续性检查
|
||||
@@ -618,28 +614,28 @@ def build_height_fields(gz1, gz2, fz, bz, have_g1, have_f, have_b, g1f, mask, R)
|
||||
k = int(np.nanargmax(dd))
|
||||
if dd[k] > jn:
|
||||
ja = np.argwhere(a)[k]
|
||||
jn, jn_at = float(dd[k]), (X0 + ja[1] * RES, Y0 + ja[0] * RES)
|
||||
log(f" 交界处高度跳变最大 {jn:.3f} @ {jn_at} (平面前框带接缝应<0.3; "
|
||||
jn, jn_at = float(dd[k]), (self.X0 + ja[1] * self.RES, self.Y0 + ja[0] * self.RES)
|
||||
self.log(f" 交界处高度跳变最大 {jn:.3f} @ {jn_at} (平面前框带接缝应<0.3; "
|
||||
"镜圈圆角处~0.4为镜框自身几何, 并集后不暴露)")
|
||||
|
||||
# ---- 后表面 ----
|
||||
hb = np.full((H, W), np.nan)
|
||||
real_f = have_f & (fz >= FACE_MIN)
|
||||
hb[real_f] = fz[real_f] - EMBED_BOT
|
||||
real_f = have_f & (fz >= self.FACE_MIN)
|
||||
hb[real_f] = fz[real_f] - self.EMBED_BOT
|
||||
knownb = R & real_f
|
||||
hb = laplace_fill(hb, knownb)
|
||||
hb = smooth_valid(hb, R)
|
||||
hb = self.laplace_fill(hb, knownb)
|
||||
hb = self.smooth_valid(hb, R)
|
||||
|
||||
cap = hf - SHELL_MAX
|
||||
cap = hf - self.SHELL_MAX
|
||||
ncap = int((R & ~real_f & np.isfinite(hb) & (hb < cap)).sum())
|
||||
hb = np.where(R & ~real_f, np.maximum(hb, cap), hb)
|
||||
log(f" 厚度上限(无真实脸面处, 距前面>{SHELL_MAX})生效格数: {ncap}")
|
||||
self.log(f" 厚度上限(无真实脸面处, 距前面>{self.SHELL_MAX})生效格数: {ncap}")
|
||||
|
||||
hb = np.where(R & real_f, np.minimum(hb, fz - 0.25), hb)
|
||||
bzs = smooth_valid(np.where(have_b, bz, np.nan), have_b)
|
||||
bzs = self.smooth_valid(np.where(have_b, bz, np.nan), have_b)
|
||||
hb = np.where(R, hb, np.nan)
|
||||
hb = np.where(have_b, np.maximum(hb, bzs + CLAMP_BACK), hb)
|
||||
hb = np.minimum(hb, hf - GAP_MIN)
|
||||
hb = np.where(have_b, np.maximum(hb, bzs + self.CLAMP_BACK), hb)
|
||||
hb = np.minimum(hb, hf - self.GAP_MIN)
|
||||
|
||||
# 外扩填充 NaN
|
||||
for _ in range(60):
|
||||
@@ -658,18 +654,17 @@ def build_height_fields(gz1, gz2, fz, bz, have_g1, have_f, have_b, g1f, mask, R)
|
||||
hb = np.where(nanm & ok, s, hb)
|
||||
|
||||
thickm = np.where(R, hf - hb, np.nan)
|
||||
log(f" 填充厚度: min {np.nanmin(thickm):.2f}, 中位 {np.nanmedian(thickm):.2f}, "
|
||||
self.log(f" 填充厚度: min {np.nanmin(thickm):.2f}, 中位 {np.nanmedian(thickm):.2f}, "
|
||||
f"mean {np.nanmean(thickm):.2f}, max {np.nanmax(thickm):.2f}")
|
||||
|
||||
return hf, hb, frame_band, wall_zone, gz1f, bzs, real_f, fz
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 步骤 5: 边界提取与平滑
|
||||
# ======================================================================
|
||||
def extract_and_smooth_boundary(R, mask, g1f):
|
||||
def extract_and_smooth_boundary(self, R, mask, g1f):
|
||||
"""提取并平滑边界"""
|
||||
log("== 5. 边界提取与平滑 ==")
|
||||
self.log("== 5. 边界提取与平滑 ==")
|
||||
H, W = R.shape
|
||||
|
||||
# 裂缝边链成回路
|
||||
@@ -703,11 +698,11 @@ def extract_and_smooth_boundary(R, mask, g1f):
|
||||
nxt = nxts[0]
|
||||
used.add((cur, nxt))
|
||||
cur = nxt
|
||||
P = np.array([[X0 + i * RES, Y0 + j * RES] for (i, j) in loop], float)
|
||||
P = np.array([[self.X0 + i * self.RES, self.Y0 + j * self.RES] for (i, j) in loop], float)
|
||||
area2 = np.sum(P[:, 0] * np.roll(P[:, 1], -1) - np.roll(P[:, 0], -1) * P[:, 1])
|
||||
if area2 < 0:
|
||||
P = P[::-1]
|
||||
log(f" 边界回路 {len(P)} 点, 周长 {np.linalg.norm(np.roll(P,-1,0)-P,axis=1).sum():.2f}")
|
||||
self.log(f" 边界回路 {len(P)} 点, 周长 {np.linalg.norm(np.roll(P,-1,0)-P,axis=1).sum():.2f}")
|
||||
|
||||
def resample(P, step):
|
||||
d = np.linalg.norm(np.roll(P, -1, 0) - P, axis=1)
|
||||
@@ -726,12 +721,12 @@ def extract_and_smooth_boundary(R, mask, g1f):
|
||||
return float(da.mean()), float(da.std())
|
||||
|
||||
r0 = roughness(P)
|
||||
B = resample(P, BSTEP)
|
||||
for _ in range(BITER):
|
||||
B = (1 - BLAM) * B + BLAM * 0.5 * (np.roll(B, 1, 0) + np.roll(B, -1, 0))
|
||||
B = resample(P, self.BSTEP)
|
||||
for _ in range(self.BITER):
|
||||
B = (1 - self.BLAM) * B + self.BLAM * 0.5 * (np.roll(B, 1, 0) + np.roll(B, -1, 0))
|
||||
|
||||
# 面积恢复
|
||||
areaR = R.sum() * RES * RES
|
||||
areaR = R.sum() * self.RES * self.RES
|
||||
for _ in range(30):
|
||||
aB = 0.5 * abs(np.sum(B[:, 0] * np.roll(B[:, 1], -1) - np.roll(B[:, 0], -1) * B[:, 1]))
|
||||
if aB >= 0.97 * areaR:
|
||||
@@ -740,43 +735,42 @@ def extract_and_smooth_boundary(R, mask, g1f):
|
||||
s = np.sqrt(0.97 * areaR / max(aB, 1e-9))
|
||||
s = min(s, 1.05)
|
||||
B = c + (B - c) * s
|
||||
|
||||
B = resample(B, BSTEP)
|
||||
B = resample(B, self.BSTEP)
|
||||
r1 = roughness(B)
|
||||
log(f" 边界粗糙度(平均转折角/标准差): 平滑前 {np.degrees(r0[0]):.2f}°/{np.degrees(r0[1]):.2f}° "
|
||||
self.log(f" 边界粗糙度(平均转折角/标准差): 平滑前 {np.degrees(r0[0]):.2f}°/{np.degrees(r0[1]):.2f}° "
|
||||
f"-> 平滑后 {np.degrees(r1[0]):.2f}°/{np.degrees(r1[1]):.2f}°")
|
||||
|
||||
# 边界须压在镜框带上
|
||||
bad = ~inside_mask(B, mask)
|
||||
bad = ~self.inside_mask(B, mask)
|
||||
if bad.any():
|
||||
c = B.mean(0)
|
||||
for k in np.where(bad)[0]:
|
||||
p = B[k]
|
||||
for s in np.linspace(0.1, 0.9, 30):
|
||||
q = c + s * (p - c)
|
||||
if inside_mask(q[None, :], mask)[0]:
|
||||
if self.inside_mask(q[None, :], mask)[0]:
|
||||
B[k] = q
|
||||
break
|
||||
for _ in range(10):
|
||||
B = (1 - BLAM) * B + BLAM * 0.5 * (np.roll(B, 1, 0) + np.roll(B, -1, 0))
|
||||
B = resample(B, BSTEP)
|
||||
B = (1 - self.BLAM) * B + self.BLAM * 0.5 * (
|
||||
np.roll(B, 1, 0) + np.roll(B, -1, 0))
|
||||
B = resample(B, self.BSTEP)
|
||||
|
||||
bii = np.clip(((B[:, 0] - X0) / RES).astype(int), 0, W - 1)
|
||||
bjj = np.clip(((B[:, 1] - Y0) / RES).astype(int), 0, H - 1)
|
||||
bii = np.clip(((B[:, 0] - self.X0) / self.RES).astype(int), 0, W - 1)
|
||||
bjj = np.clip(((B[:, 1] - self.Y0) / self.RES).astype(int), 0, H - 1)
|
||||
nb = np.zeros(len(B), bool)
|
||||
for dj, di in ((0,0),(1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)):
|
||||
jj2 = np.clip(bjj + dj, 0, H - 1); ii2 = np.clip(bii + di, 0, W - 1)
|
||||
nb |= g1f[jj2, ii2]
|
||||
log(f" 平滑边界 {len(B)} 点, 超出足迹 {int((~inside_mask(B, mask)).sum())}, "
|
||||
self.log(f" 平滑边界 {len(B)} 点, 超出足迹 {int((~self.inside_mask(B, mask)).sum())}, "
|
||||
f"压在镜框带上 {nb.mean()*100:.1f}%")
|
||||
|
||||
return B, loop
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 步骤 6: 三角剖分 (耳切法 + 内部点插入)
|
||||
# ======================================================================
|
||||
def ear_clip(P):
|
||||
def ear_clip(self, P):
|
||||
"""P 为 CCW 简单多边形, 返回三角形索引列表"""
|
||||
idx = list(range(len(P)))
|
||||
tris = []
|
||||
@@ -788,7 +782,7 @@ def ear_clip(P):
|
||||
for k in range(m):
|
||||
i0, i1, i2 = idx[(k - 1) % m], idx[k], idx[(k + 1) % m]
|
||||
a, b, c = P[i0], P[i1], P[i2]
|
||||
cr = cross3(a, b, c)
|
||||
cr = self.cross3(a, b, c)
|
||||
if cr <= 1e-10:
|
||||
continue
|
||||
inside_any = False
|
||||
@@ -796,8 +790,8 @@ def ear_clip(P):
|
||||
if j in (i0, i1, i2):
|
||||
continue
|
||||
p = P[j]
|
||||
if (cross3(a, b, p) >= -1e-12 and cross3(b, c, p) >= -1e-12
|
||||
and cross3(c, a, p) >= -1e-12):
|
||||
if (self.cross3(a, b, p) >= -1e-12 and self.cross3(b, c, p) >= -1e-12
|
||||
and self.cross3(c, a, p) >= -1e-12):
|
||||
inside_any = True
|
||||
break
|
||||
if not inside_any:
|
||||
@@ -808,11 +802,11 @@ def ear_clip(P):
|
||||
best_k, best_score = k, score
|
||||
if best_k is None:
|
||||
m = len(idx)
|
||||
areas = [(cross3(P[idx[(k-1)%m]], P[idx[k]], P[idx[(k+1)%m]]), k)
|
||||
areas = [(self.cross3(P[idx[(k-1)%m]], P[idx[k]], P[idx[(k+1)%m]]), k)
|
||||
for k in range(m)]
|
||||
areas.sort(reverse=True)
|
||||
best_k = areas[0][1]
|
||||
log(" ! 耳切兜底触发")
|
||||
self.log(" ! 耳切兜底触发")
|
||||
i0, i1, i2 = (idx[(best_k - 1) % len(idx)], idx[best_k],
|
||||
idx[(best_k + 1) % len(idx)])
|
||||
tris.append((i0, i1, i2))
|
||||
@@ -820,16 +814,15 @@ def ear_clip(P):
|
||||
tris.append((idx[0], idx[1], idx[2]))
|
||||
return tris
|
||||
|
||||
|
||||
def triangulate_fill(B, R, hf, hb):
|
||||
def triangulate_fill(self, B, R, hf, hb):
|
||||
"""对填充区域进行三角剖分"""
|
||||
log("== 6. 区域三角化 (耳切法 + 内部网格点插入, 无翻转风险) ==")
|
||||
self.log("== 6. 区域三角化 (耳切法 + 内部网格点插入, 无翻转风险) ==")
|
||||
H, W = R.shape
|
||||
Nb = len(B)
|
||||
|
||||
# 耳切三角化
|
||||
T = np.asarray(ear_clip(B.tolist()), np.int64)
|
||||
log(f" 耳切三角化: {len(T)} 三角形")
|
||||
T = np.asarray(self.ear_clip(B.tolist()), np.int64)
|
||||
self.log(f" 耳切三角化: {len(T)} 三角形")
|
||||
|
||||
# 内部点候选
|
||||
inner2 = R.copy()
|
||||
@@ -838,14 +831,14 @@ def triangulate_fill(B, R, hf, hb):
|
||||
ne[1:, :] &= inner2[:-1, :]; ne[:-1, :] &= inner2[1:, :]
|
||||
ne[:, 1:] &= inner2[:, :-1]; ne[:, :-1] &= inner2[:, 1:]
|
||||
inner2 = ne
|
||||
step = max(1, int(round(0.3 / RES)))
|
||||
step = max(1, int(round(0.3 / self.RES)))
|
||||
cand = []
|
||||
for j in range(0, H, step):
|
||||
for i in range(0, W, step):
|
||||
if inner2[j, i]:
|
||||
cand.append((X0 + i * RES, Y0 + j * RES))
|
||||
cand.append((self.X0 + i * self.RES, self.Y0 + j * self.RES))
|
||||
cand = np.asarray(cand)
|
||||
log(f" 内部插入点候选 {len(cand)}")
|
||||
self.log(f" 内部插入点候选 {len(cand)}")
|
||||
|
||||
# 逐点插入
|
||||
xy = np.vstack([B, cand])
|
||||
@@ -949,7 +942,7 @@ def triangulate_fill(B, R, hf, hb):
|
||||
nflips += k
|
||||
if k == 0:
|
||||
break
|
||||
log(f" 边翻转优化: {nflips} 次, 收敛于 {_p+1} 趟")
|
||||
self.log(f" 边翻转优化: {nflips} 次, 收敛于 {_p+1} 趟")
|
||||
|
||||
# 残余长边细分
|
||||
MAXEDGE = 0.5
|
||||
@@ -992,18 +985,18 @@ def triangulate_fill(B, R, hf, hb):
|
||||
newT += [[a, mab, mca], [mab, b, mbc], [mca, mbc, c], [mab, mbc, mca]]
|
||||
Tlist = newT
|
||||
T = np.asarray(Tlist, np.int64)
|
||||
log(f" 残余长边细分(>{MAXEDGE}): 新增 {nsplit} 中点, 总三角形 {len(T)}")
|
||||
self.log(f" 残余长边细分(>{MAXEDGE}): 新增 {nsplit} 中点, 总三角形 {len(T)}")
|
||||
|
||||
cr = xy[T[:, 1]] - xy[T[:, 0]]
|
||||
cs = xy[T[:, 2]] - xy[T[:, 0]]
|
||||
area = cr[:, 0] * cs[:, 1] - cr[:, 1] * cs[:, 0]
|
||||
if area.min() <= 0:
|
||||
raise SystemExit(f"三角形定向异常 (min area {area.min():.2e})")
|
||||
log(f" 实际插入 {extra} 点, 总三角形 {len(T)}, 最小有向面积 {area.min():.5f} (>0 = 无翻转)")
|
||||
self.log(f" 实际插入 {extra} 点, 总三角形 {len(T)}, 最小有向面积 {area.min():.5f} (>0 = 无翻转)")
|
||||
|
||||
# 计算 z 坐标
|
||||
zf = bilinear(hf, xy)
|
||||
zb = bilinear(hb, xy)
|
||||
zf = self.bilinear(hf, xy)
|
||||
zb = self.bilinear(hb, xy)
|
||||
zb = np.minimum(zb, zf - 0.15)
|
||||
zb = np.maximum(zb, zf - 50)
|
||||
|
||||
@@ -1015,9 +1008,9 @@ def triangulate_fill(B, R, hf, hb):
|
||||
# ======================================================================
|
||||
# 步骤 7: 组装实体 (前/后表面 + 侧墙)
|
||||
# ======================================================================
|
||||
def assemble_mesh(Pf, Pb, T):
|
||||
def assemble_mesh(self, Pf, Pb, T):
|
||||
"""组装填充实体: 前表面 + 后表面 + 侧墙"""
|
||||
log("== 7. 组装填充实体 ==")
|
||||
self.log("== 7. 组装填充实体 ==")
|
||||
nV = len(Pf)
|
||||
faces = []
|
||||
|
||||
@@ -1058,18 +1051,17 @@ def assemble_mesh(Pf, Pb, T):
|
||||
|
||||
return Pall, Ff, nV
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 步骤 8: 校验
|
||||
# ======================================================================
|
||||
def validate_mesh(Pall, Ff, Pf, Pb, T, V, F, hf, hb, gz1f, frame_band,
|
||||
def validate_mesh(self, Pall, Ff, Pf, Pb, T, V, F, hf, hb, gz1f, frame_band,
|
||||
wall_zone, bzs, have_b, real_f, fz, R, xy):
|
||||
"""对生成的网格进行各项校验"""
|
||||
log("== 8. 校验 ==")
|
||||
self.log("== 8. 校验 ==")
|
||||
|
||||
# 定向校验
|
||||
vfill = signed_volume(Pall, Ff)
|
||||
vorig = signed_volume(V, F)
|
||||
vfill = self.signed_volume(Pall, Ff)
|
||||
vorig = self.signed_volume(V, F)
|
||||
if vfill * vorig < 0:
|
||||
Ff = Ff[:, [0, 2, 1]]
|
||||
vfill = -vfill
|
||||
@@ -1079,7 +1071,7 @@ def validate_mesh(Pall, Ff, Pf, Pb, T, V, F, hf, hb, gz1f, frame_band,
|
||||
exp_vol = float(np.sum(0.5 * np.abs(area) *
|
||||
((Pf[T[:, 0], 2] + Pf[T[:, 1], 2] + Pf[T[:, 2], 2]
|
||||
- Pb[T[:, 0], 2] - Pb[T[:, 1], 2] - Pb[T[:, 2], 2]) / 3.0)))
|
||||
log(f" 填充实体体积 {vfill:.2f} (期望 {exp_vol:.2f}), 原模型 {vorig:.0f}, 定向一致")
|
||||
self.log(f" 填充实体体积 {vfill:.2f} (期望 {exp_vol:.2f}), 原模型 {vorig:.0f}, 定向一致")
|
||||
if abs(vfill - exp_vol) > 0.02 * abs(exp_vol) + 1e-6:
|
||||
raise SystemExit("体积校验失败: 面片定向不一致")
|
||||
|
||||
@@ -1088,7 +1080,7 @@ def validate_mesh(Pall, Ff, Pf, Pb, T, V, F, hf, hb, gz1f, frame_band,
|
||||
ue, cnt = np.unique(e2, axis=0, return_counts=True)
|
||||
de = np.concatenate([Ff[:, [0, 1]], Ff[:, [1, 2]], Ff[:, [2, 0]]])
|
||||
ude, udcnt = np.unique(de, axis=0, return_counts=True)
|
||||
log(f" 水密性: 非流形边(计数!=2) {int((cnt != 2).sum())} / {len(ue)}, "
|
||||
self.log(f" 水密性: 非流形边(计数!=2) {int((cnt != 2).sum())} / {len(ue)}, "
|
||||
f"方向冲突边 {int((udcnt != 1).sum())} (两者都应为 0)")
|
||||
|
||||
# 镜片光滑度
|
||||
@@ -1109,47 +1101,46 @@ def validate_mesh(Pall, Ff, Pf, Pb, T, V, F, hf, hb, gz1f, frame_band,
|
||||
lang = np.asarray(lang)
|
||||
if len(lang):
|
||||
k = np.argmax(lang)
|
||||
log(f" 镜片光滑度(相邻面二面角): 中位 {np.median(lang):.2f}°, "
|
||||
self.log(f" 镜片光滑度(相邻面二面角): 中位 {np.median(lang):.2f}°, "
|
||||
f"p90 {np.percentile(lang, 90):.2f}°, "
|
||||
f"max {lang[k]:.2f}° @ ({lloc[k][0]:.1f},{lloc[k][1]:.1f}) (中位应<2°)")
|
||||
|
||||
# 嵌入校验
|
||||
gmargin = (gz1f - hf)[R & frame_band & wall_zone]
|
||||
log(f" 前表面低于本地框面(墙区): 最小余量 {np.nanmin(gmargin):.3f} (应>0)")
|
||||
self.log(f" 前表面低于本地框面(墙区): 最小余量 {np.nanmin(gmargin):.3f} (应>0)")
|
||||
gmargin2 = (gz1f - hf)[R & frame_band]
|
||||
log(f" 前表面低于本地框面(内部允许贴齐): 最小余量 {np.nanmin(gmargin2):.3f} (应>-0.10)")
|
||||
self.log(f" 前表面低于本地框面(内部允许贴齐): 最小余量 {np.nanmin(gmargin2):.3f} (应>-0.10)")
|
||||
bmargin = (hb - bzs)[R & have_b]
|
||||
log(f" 后表面高于最后表面: 最小余量 {np.nanmin(bmargin):.3f} (应>0)")
|
||||
self.log(f" 后表面高于最后表面: 最小余量 {np.nanmin(bmargin):.3f} (应>0)")
|
||||
embed = (fz - hb)[R & real_f]
|
||||
log(f" 底面嵌入真实脸面: 最小 {np.nanmin(embed):.3f} (应>-0.05, 负=缝隙), "
|
||||
self.log(f" 底面嵌入真实脸面: 最小 {np.nanmin(embed):.3f} (应>-0.05, 负=缝隙), "
|
||||
f"中位 {np.nanmedian(embed):.2f}")
|
||||
inR = inside_mask(xy, R)
|
||||
log(f" 圆盘顶点落在 R 内比例 {inR.mean()*100:.1f}%")
|
||||
inR = self.inside_mask(xy, R)
|
||||
self.log(f" 圆盘顶点落在 R 内比例 {inR.mean()*100:.1f}%")
|
||||
|
||||
return Ff, vfill
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 步骤 9: 输出
|
||||
# ======================================================================
|
||||
def output_mesh(Pall, Ff, V, F):
|
||||
def output_mesh(self, Pall, Ff, V, F):
|
||||
"""输出填充网格和合并后的模型"""
|
||||
log("== 9. 输出 ==")
|
||||
self.log("== 9. 输出 ==")
|
||||
|
||||
# 输出填充网格
|
||||
with open(OUT_FILL, "w") as f:
|
||||
with open(self.OUT_FILL, "w") as f:
|
||||
f.write("# fill mesh between glasses and face\n")
|
||||
f.write("o fill\n")
|
||||
for p in Pall:
|
||||
f.write(f"v {p[0]:.5f} {p[1]:.5f} {p[2]:.5f}\n")
|
||||
for a, b, c in Ff:
|
||||
f.write(f"f {a+1} {b+1} {c+1}\n")
|
||||
log(f" {OUT_FILL}: {len(Pall)} 顶点, {len(Ff)} 面")
|
||||
self.log(f" {self.OUT_FILL}: {len(Pall)} 顶点, {len(Ff)} 面")
|
||||
|
||||
# 输出合并模型
|
||||
with open(IN_OBJ) as f:
|
||||
with open(self.IN_OBJ) as f:
|
||||
orig = f.read()
|
||||
with open(OUT_MERGED, "w") as f:
|
||||
with open(self.OUT_MERGED, "w") as f:
|
||||
f.write(orig.rstrip("\n") + "\n")
|
||||
f.write("o fill\n")
|
||||
off = len(V)
|
||||
@@ -1157,15 +1148,15 @@ def output_mesh(Pall, Ff, V, F):
|
||||
f.write(f"v {p[0]:.5f} {p[1]:.5f} {p[2]:.5f}\n")
|
||||
for a, b, c in Ff:
|
||||
f.write(f"f {a+1+off} {b+1+off} {c+1+off}\n")
|
||||
log(f" {OUT_MERGED}: 原模型 + 填充 (顶点偏移 {off})")
|
||||
|
||||
self.log(f" {self.OUT_MERGED}: 原模型 + 填充 (顶点偏移 {off})")
|
||||
|
||||
# ======================================================================
|
||||
# 步骤 10: 生成预览图
|
||||
# ======================================================================
|
||||
def generate_preview(gz1, hf, hb, R, B):
|
||||
def generate_preview(self, gz1, hf, hb, R, B):
|
||||
"""生成前视图深度预览"""
|
||||
log("== 10. 生成预览图 ==")
|
||||
self.log("== 10. 生成预览图 ==")
|
||||
H, W = self.H, self.W
|
||||
|
||||
# 合成前表面深度图
|
||||
front = np.where(np.isfinite(gz1) & (gz1 > -1e30), gz1, np.nan)
|
||||
@@ -1189,7 +1180,7 @@ def generate_preview(gz1, hf, hb, R, B):
|
||||
span if "thick" not in name else (0, 4)))
|
||||
im = im.resize((W * 4, H * 4), Image.NEAREST)
|
||||
d = ImageDraw.Draw(im)
|
||||
pts = [((b[0] - X0) / RES * 4, (Y1 - b[1]) / RES * 4) for b in B]
|
||||
pts = [((b[0] - self.X0) / self.RES * 4, (self.Y1 - b[1]) / self.RES * 4) for b in B]
|
||||
d.line(pts + pts[:1], fill=255, width=2)
|
||||
panels.append((name, im))
|
||||
|
||||
@@ -1200,50 +1191,59 @@ def generate_preview(gz1, hf, hb, R, B):
|
||||
for name, p in panels:
|
||||
combo.paste(p, (xoff, 0))
|
||||
xoff += p.width + 10
|
||||
combo.save(OUT_PREVIEW)
|
||||
log(f" {OUT_PREVIEW}: 3 面板(前深度/填充前表面/厚度), 红线=平滑边界")
|
||||
|
||||
combo.save(self.OUT_PREVIEW)
|
||||
self.log(f" {self.OUT_PREVIEW}: 3 面板(前深度/填充前表面/厚度), 红线=平滑边界")
|
||||
|
||||
# ======================================================================
|
||||
# 主流程
|
||||
# 主流程 run()
|
||||
# ======================================================================
|
||||
if __name__ == "__main__":
|
||||
def run(self):
|
||||
"""执行完整填充网格生成流程"""
|
||||
# 步骤 1: 加载模型
|
||||
V, F, face_g = load_and_classify()
|
||||
V, F, face_g = self.load_and_classify()
|
||||
|
||||
# 步骤 2: 栅格化
|
||||
gz1, gz2, fz, bz, have_g1, have_g2, have_f, have_b, g1f, W, H = \
|
||||
rasterize(V, F, face_g)
|
||||
self.rasterize(V, F, face_g)
|
||||
self.W, self.H = W, H
|
||||
|
||||
# 步骤 3: 构建足迹
|
||||
mask, R = build_footprint(g1f)
|
||||
mask, R = self.build_footprint(g1f)
|
||||
|
||||
# 步骤 4: 构造高度场
|
||||
hf, hb, frame_band, wall_zone, gz1f, bzs, real_f, fz = \
|
||||
build_height_fields(gz1, gz2, fz, bz, have_g1, have_f, have_b,
|
||||
self.build_height_fields(gz1, gz2, fz, bz, have_g1, have_f, have_b,
|
||||
g1f, mask, R)
|
||||
|
||||
# 步骤 5: 边界提取与平滑
|
||||
B, loop = extract_and_smooth_boundary(R, mask, g1f)
|
||||
B, loop = self.extract_and_smooth_boundary(R, mask, g1f)
|
||||
|
||||
# 步骤 6: 三角剖分
|
||||
Pf, Pb, T, xy = triangulate_fill(B, R, hf, hb)
|
||||
Pf, Pb, T, xy = self.triangulate_fill(B, R, hf, hb)
|
||||
|
||||
# 步骤 7: 组装实体
|
||||
Pall, Ff, nV = assemble_mesh(Pf, Pb, T)
|
||||
Pall, Ff, nV = self.assemble_mesh(Pf, Pb, T)
|
||||
|
||||
# 步骤 8: 校验
|
||||
Ff, vfill = validate_mesh(Pall, Ff, Pf, Pb, T, V, F, hf, hb,
|
||||
Ff, vfill = self.validate_mesh(Pall, Ff, Pf, Pb, T, V, F, hf, hb,
|
||||
gz1f, frame_band, wall_zone, bzs,
|
||||
have_b, real_f, fz, R, xy)
|
||||
|
||||
# 步骤 9: 输出
|
||||
output_mesh(Pall, Ff, V, F)
|
||||
self.output_mesh(Pall, Ff, V, F)
|
||||
|
||||
# 步骤 10: 预览图
|
||||
generate_preview(gz1, hf, hb, R, B)
|
||||
self.generate_preview(gz1, hf, hb, R, B)
|
||||
|
||||
log("\n== 完成 ==")
|
||||
self.log("\n== 完成 ==")
|
||||
with open("fill_report.txt", "w") as f:
|
||||
f.write("\n".join(report) + "\n")
|
||||
f.write("\n".join(self.report) + "\n")
|
||||
print("报告已写入 fill_report.txt")
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# 入口
|
||||
# ======================================================================
|
||||
if __name__ == "__main__":
|
||||
gen = FillMeshGenerator()
|
||||
gen.run()
|
||||
Reference in new issue
Block a user