feat: update algorithm integrations and point cloud overlays
This commit is contained in:
@@ -7,15 +7,16 @@ v1.5 变更:
|
||||
- pipeline 新增 result_template_path / combined_context_pointcloud_path 参数
|
||||
|
||||
约定:
|
||||
- module=all:plane + curvature(contour 需要 CAD STL,缺失时跳过)
|
||||
- module=contour:轮廓/平整度,需要 CAD STEP(推荐,算法方基准 3min25s)
|
||||
- module=plane:只跑平面拟合
|
||||
- module=curvature:只跑曲率估计
|
||||
- module=contour:需要 CAD STL,缺失时报错
|
||||
- module=curvature:只跑曲率估计(大点云极慢,慎用)
|
||||
- module=all:plane + curvature + contour(已从前端选项移除,大点云不可用)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
@@ -39,6 +40,39 @@ def _safe_float(v: object) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def _mesh_cache_path(cad_path: str, mesh_deflection: float) -> Path:
|
||||
"""根据 STEP 路径 + 文件指纹 + 三角化精度生成缓存路径。
|
||||
|
||||
优先返回配准阶段已生成的 mesh 缓存(_mesh.ply),
|
||||
其次返回 flatness 自己的缓存路径,均放在 CAD 同目录的 .cad_sample_cache/ 下。
|
||||
"""
|
||||
stat = os.stat(cad_path)
|
||||
cache_key = "|".join([
|
||||
os.path.abspath(cad_path),
|
||||
str(stat.st_size),
|
||||
str(stat.st_mtime_ns),
|
||||
f"{mesh_deflection:.12g}",
|
||||
])
|
||||
digest = hashlib.sha1(cache_key.encode()).hexdigest()[:16]
|
||||
cad_stem = Path(cad_path).stem
|
||||
shared_dir = Path(cad_path).parent / ".cad_sample_cache"
|
||||
|
||||
# 配准阶段已缓存的 mesh(与配准采样缓存同目录,命名规则:<stem>_<digest>_mesh.ply)
|
||||
registration_mesh = shared_dir / f"{cad_stem}_{digest}_mesh.ply"
|
||||
if registration_mesh.exists():
|
||||
return registration_mesh
|
||||
|
||||
# flatness 自己写缓存的目标路径
|
||||
try:
|
||||
shared_dir.mkdir(parents=True, exist_ok=True)
|
||||
test = shared_dir / ".write_test"
|
||||
test.touch()
|
||||
test.unlink()
|
||||
return shared_dir / f"{cad_stem}_{digest}_mesh.ply"
|
||||
except OSError:
|
||||
return Path(cad_path).parent / f"{cad_stem}_{digest}_mesh.ply"
|
||||
|
||||
|
||||
def _find_alg_dir(alg_root: str) -> Path:
|
||||
"""定位平整度算法目录,支持多个历史版本路径,优先使用最新版。"""
|
||||
candidates = [
|
||||
@@ -99,11 +133,12 @@ def main() -> int:
|
||||
|
||||
try:
|
||||
alg_root = os.environ.get("ALG_ROOT") or os.environ.get("ALG_ROOT_HOST") or "/alg"
|
||||
emit_progress(0.05, "定位 point_porj 算法目录")
|
||||
emit_progress(0.05, "定位算法目录")
|
||||
alg_dir = _find_alg_dir(alg_root)
|
||||
|
||||
# ── 判断走单点云模式还是 datasets 批量模式 ──────────────────────────────
|
||||
pc_path_raw = first_pointcloud(payload)
|
||||
emit_progress(0.10, f"读取点云:{os.path.basename(pc_path_raw)}")
|
||||
use_batch_mode = bool(config.get("useBatchDatasets", True))
|
||||
|
||||
# ── ROI 裁剪(整车点云场景下必须填写,否则算法极慢)──────────────────────
|
||||
def _f(key: str) -> float | None:
|
||||
@@ -147,10 +182,61 @@ def main() -> int:
|
||||
# v1.5 用 o3d.io.read_point_cloud 读取,直接支持二进制 PLY,无需转 ASCII
|
||||
pc_path = pc_path_raw
|
||||
|
||||
module = str(config.get("module") or "all")
|
||||
module = str(config.get("module") or "contour")
|
||||
emit_progress(0.15, f"初始化 MeasurementPipeline(module={module})")
|
||||
|
||||
pipeline = _build_pipeline(alg_dir, pc_path, out_dir, config)
|
||||
pipeline = _build_pipeline(alg_dir, pc_path if not use_batch_mode else str(alg_dir), out_dir, config)
|
||||
|
||||
# ── datasets 批量模式(useBatchDatasets=true 时) ──────────────────────
|
||||
if use_batch_mode and module == "contour":
|
||||
emit_progress(0.20, "批量模式:从 datasets/Noise_PointCloud/ 读取所有面板点云")
|
||||
data_root = alg_dir / "datasets"
|
||||
# 所选整车点云作为可视化背景;用户也可在 contextPointCloudPath 里手动指定
|
||||
context_pc = (inputs.get("contextPointCloudPath")
|
||||
or pc_path_raw
|
||||
or None)
|
||||
save_dir = str(out_dir / "contour_batch")
|
||||
excel_path = str(out_dir / "contour.xlsx")
|
||||
res = pipeline.run_contour_batch(
|
||||
all_tasks=True,
|
||||
data_root=str(data_root),
|
||||
pointcloud_kind="noise",
|
||||
point_chunk_size=8192,
|
||||
triangle_chunk_size=512,
|
||||
save_path=save_dir,
|
||||
excel_output_path=excel_path,
|
||||
combined_context_pointcloud_path=context_pc,
|
||||
no_visualize=True,
|
||||
)
|
||||
emit_progress(0.95, "整理批量结果")
|
||||
batch_stats = res.get("batch_stats") or {}
|
||||
measurements: list[dict] = []
|
||||
for task_id, task_res in (res.get("results") or {}).items():
|
||||
s = task_res.get("distance_stats") or {}
|
||||
if s:
|
||||
measurements.append({
|
||||
"name": f"任务{task_id} 轮廓偏差均值",
|
||||
"value": round(float(s.get("mean", 0)), 4),
|
||||
"unit": "mm", "status": "info",
|
||||
})
|
||||
# 各面板热力图 PLY 列表(不含背景点,全部是有意义的颜色)
|
||||
panel_plys = sorted([
|
||||
f"contour_batch/{p.name}"
|
||||
for p in (out_dir / "contour_batch").glob("*_contour.ply")
|
||||
if p.name != "combined_contour.ply"
|
||||
])
|
||||
if panel_plys:
|
||||
overlay3d = {"kind": "ply", "file": panel_plys[0], "files": panel_plys}
|
||||
else:
|
||||
overlay3d = {"kind": "none"}
|
||||
write_result(out_dir, {
|
||||
"status": "completed",
|
||||
"message": f"批量完成 {len(res.get('results', {}))} 个面板测量",
|
||||
"measurements": measurements,
|
||||
"overlay3d": overlay3d,
|
||||
"extra": {"module": "contour_batch", "excelPath": "contour.xlsx"},
|
||||
})
|
||||
return 0
|
||||
|
||||
measurements: list[dict] = []
|
||||
extra: dict[str, Any] = {"module": module}
|
||||
@@ -226,7 +312,7 @@ def main() -> int:
|
||||
|
||||
# ── contour ────────────────────────────────────────────────────────────
|
||||
if module in ("all", "contour"):
|
||||
emit_progress(0.75, "轮廓-平整度(需 CAD STL)")
|
||||
emit_progress(0.75, "轮廓-平整度(需 CAD STEP)")
|
||||
cad_path = inputs.get("cadPath") or None
|
||||
mesh_path = inputs.get("meshPath") or None
|
||||
if not cad_path and not mesh_path:
|
||||
@@ -235,14 +321,54 @@ def main() -> int:
|
||||
try:
|
||||
save_path = str(out_dir / "contour.ply")
|
||||
excel_path = str(out_dir / "contour.xlsx")
|
||||
|
||||
# STEP mesh 缓存:避免每次重新三角化 442 MB+ 整车 STEP
|
||||
cached_mesh_ply: str | None = None
|
||||
if cad_path and not mesh_path:
|
||||
_deflection = float(
|
||||
_safe_float(config.get("meshDeflection")) or 0.1
|
||||
)
|
||||
_cache_ply = _mesh_cache_path(cad_path, _deflection)
|
||||
if _cache_ply.exists():
|
||||
emit_progress(0.76, "使用已缓存 CAD mesh,跳过 STEP 三角化")
|
||||
mesh_path = str(_cache_ply)
|
||||
cad_path = None
|
||||
else:
|
||||
# 先单独做三角化,完成后立即缓存,再跑点距计算
|
||||
# 这样即使点距阶段 OOM,下次也不用重新三角化
|
||||
emit_progress(0.76, "首次三角化 CAD STEP,结果将缓存供后续复用…")
|
||||
try:
|
||||
import open3d as o3d # type: ignore
|
||||
_orig_cad = cad_path
|
||||
sys.path.insert(0, str(alg_dir))
|
||||
from cad_converter import CADConverter # type: ignore
|
||||
_converter = CADConverter()
|
||||
_verts, _faces = _converter.step_to_numpy_mesh(
|
||||
_orig_cad, mesh_deflection=_deflection
|
||||
)
|
||||
_mesh_o3d = o3d.geometry.TriangleMesh()
|
||||
_mesh_o3d.vertices = o3d.utility.Vector3dVector(_verts)
|
||||
_mesh_o3d.triangles = o3d.utility.Vector3iVector(_faces.astype(int))
|
||||
o3d.io.write_triangle_mesh(str(_cache_ply), _mesh_o3d)
|
||||
emit_progress(0.80, f"CAD mesh 已缓存:{_cache_ply.name},开始点距计算…")
|
||||
mesh_path = str(_cache_ply)
|
||||
cad_path = None
|
||||
except Exception as _tri_exc:
|
||||
emit_progress(0.76, f"三角化缓存写入失败({_tri_exc}),继续直接跑…")
|
||||
|
||||
context_pc_path = (inputs.get("contextPointCloudPath") or None)
|
||||
res = pipeline.run_contour(
|
||||
pointcloud_path=pc_path,
|
||||
cad_path=cad_path,
|
||||
mesh_path=mesh_path,
|
||||
point_chunk_size=8192,
|
||||
triangle_chunk_size=512,
|
||||
save_path=save_path,
|
||||
excel_output_path=excel_path,
|
||||
combined_context_pointcloud_path=context_pc_path,
|
||||
no_visualize=True,
|
||||
)
|
||||
|
||||
stats = res.get("distance_stats")
|
||||
for key, label, unit in [
|
||||
("mean", "轮廓偏差均值", "mm"),
|
||||
|
||||
Reference in New Issue
Block a user