#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ dd_voxel 离线验证 / 渲染基准:把两条交叉剖面(剖面原数据1+2)三维空间插值(IDW + maxDist 裁剪) 成体素,切片可视化。用于: 1) 提前验证 IDW + 包络裁剪算法(与 Phase 1 core/algo/IdwInterpolator 同逻辑); 2) 把"可信体"问题看实——两条 1D 测线只在其所在竖直面附近有约束,体内大部分靠插值/留空; 3) 产出 VTK 体绘制/切片的地面真值对照图。 每个测点三维坐标 = (projX, projY, z=ylist[高程]),值 = vlist。 运行: python tools/validate_voxel.py 输出: docs/_validate/voxel_*.png 依赖: numpy, scipy, matplotlib """ import json import os import sys import numpy as np from scipy.spatial import cKDTree import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt DATA = os.path.join(os.path.dirname(__file__), "..", "docs", "剖面网格数据的色阶数据2等文件") OUT = os.path.join(os.path.dirname(__file__), "..", "docs", "_validate") POWER = 2.0 # IDW 幂 K = 8 # 取最近 K 个点 MAX_DIST = 4.0 # 超过此距离(米)→ 留空(NaN),约束插值域(设计 §10) DXY = 1.0 # 水平网格步长(米) DZ = 0.5 # 垂向步长(米) def load_points(name): d = json.load(open(os.path.join(DATA, name), encoding="utf-8"))["data"] px = np.asarray(d["projectXList"], float) py = np.asarray(d["projectYList"], float) z = np.asarray(d["ylist"], float) # 高程 v = np.asarray(d["vlist"], float) return np.column_stack([px, py, z]), v def idw_voxel(pts, vals, spec): ox, oy, oz, nx, ny, nz, dx, dy, dz = spec tree = cKDTree(pts) # 网格中心点 gx = ox + (np.arange(nx) + 0.5) * dx gy = oy + (np.arange(ny) + 0.5) * dy gz = oz + (np.arange(nz) + 0.5) * dz GX, GY, GZ = np.meshgrid(gx, gy, gz, indexing="ij") Q = np.column_stack([GX.ravel(), GY.ravel(), GZ.ravel()]) dist, idx = tree.query(Q, k=K) dist = np.maximum(dist, 1e-9) w = 1.0 / dist ** POWER vol = (w * vals[idx]).sum(1) / w.sum(1) vol[dist[:, 0] > MAX_DIST] = np.nan # 最近点都超距 → 留空 return vol.reshape(nx, ny, nz), (gx, gy, gz) def main(): os.makedirs(OUT, exist_ok=True) p1, v1 = load_points("剖面原数据1.txt") p2, v2 = load_points("剖面原数据2.txt") pts = np.vstack([p1, p2]) vals = np.concatenate([v1, v2]) # 局部化(减最小,规避大坐标;同 LocalFrame 思路) origin = pts.min(0) ptsL = pts - origin ext = ptsL.max(0) nx = int(ext[0] / DXY) + 1 ny = int(ext[1] / DXY) + 1 nz = int(ext[2] / DZ) + 1 spec = (0, 0, 0, nx, ny, nz, DXY, DXY, DZ) print(f"points={len(vals)} grid={nx}x{ny}x{nz}={nx*ny*nz} cells " f"horiz_extent={ext[0]:.1f}x{ext[1]:.1f}m depth={ext[2]:.1f}m") vol, (gx, gy, gz) = idw_voxel(ptsL, vals, spec) filled = np.isfinite(vol).sum() print(f"filled cells={filled} ({100*filled/vol.size:.1f}%) " f"-> 其余为留空(两测线包络外无约束) | v[{np.nanmin(vol):.0f},{np.nanmax(vol):.0f}]") vmin, vmax = np.nanpercentile(vol, [2, 98]) # 1) 中深度水平切片:应显示两条交叉的带(十字支撑) kz = nz // 2 plt.figure(figsize=(6, 5)) plt.imshow(vol[:, :, kz].T, origin="lower", extent=[0, ext[0], 0, ext[1]], cmap="turbo", vmin=vmin, vmax=vmax) plt.scatter(ptsL[:, 0], ptsL[:, 1], s=1, c="k", alpha=0.15) plt.title(f"voxel 水平切片 z={gz[kz]:.1f} (应见交叉十字支撑)") plt.xlabel("E(m)"); plt.ylabel("N(m)"); plt.colorbar(shrink=.8) plt.tight_layout(); plt.savefig(f"{OUT}/voxel_hslice.png", dpi=90); plt.close() # 2) 沿 E 的垂直切片(取 profile1 附近 N):应重现剖面竖直结构 jy = ny // 2 plt.figure(figsize=(8, 3)) plt.imshow(vol[:, jy, :].T, origin="lower", extent=[0, ext[0], 0, ext[2]], cmap="turbo", vmin=vmin, vmax=vmax, aspect="auto") plt.title(f"voxel 垂直切片 N={gy[jy]:.1f}") plt.xlabel("E(m)"); plt.ylabel("elev(m)"); plt.colorbar(shrink=.8) plt.tight_layout(); plt.savefig(f"{OUT}/voxel_vslice.png", dpi=90); plt.close() # 3) 非留空区的三维散点(直观看"十字片"支撑) xi, yi, zi = np.where(np.isfinite(vol)) sub = slice(None, None, max(1, len(xi) // 6000)) fig = plt.figure(figsize=(7, 5)); ax = fig.add_subplot(111, projection="3d") sc = ax.scatter(gx[xi[sub]], gy[yi[sub]], gz[zi[sub]], c=vol[xi[sub], yi[sub], zi[sub]], cmap="turbo", vmin=vmin, vmax=vmax, s=3) ax.set_title("voxel 非留空区(两测线竖直面附近=十字片)") ax.set_xlabel("E"); ax.set_ylabel("N"); ax.set_zlabel("elev") fig.colorbar(sc, shrink=.6) plt.tight_layout(); plt.savefig(f"{OUT}/voxel_support.png", dpi=90); plt.close() print("written:", sorted(f for f in os.listdir(OUT) if f.startswith("voxel"))) if __name__ == "__main__": sys.stdout.reconfigure(encoding="utf-8") main()