"""flatness-suite 算法适配器 (v1.2)。 v1.2 变更: - 算法目录从 alg/平整度、平面度、轮廓测量/ → alg/point_porj/ - 入口从独立脚本(importlib 加载)→ MeasurementPipeline 类 - run_plane / run_curvature / run_contour 统一通过 MeasurementPipeline 调用 - 点云路径通过 config["paths"]["pointcloud"] 注入,不再依赖 open3d 直接读取 约定: - module=all:plane + curvature(contour 需要 CAD STL,缺失时跳过) - module=plane:只跑平面拟合 - module=curvature:只跑曲率估计 - module=contour:需要 CAD STL,缺失时报错 """ from __future__ import annotations import copy import os import sys import traceback from pathlib import Path from typing import Any sys.path.insert(0, str(Path(__file__).parent)) from _plugin_io import ( emit_progress, ensure_ascii_ply, first_pointcloud, load_input, write_error_result, write_result, ) def _safe_float(v: object) -> float | None: try: return float(v) # type: ignore[arg-type] except (TypeError, ValueError): return None def _find_alg_dir(alg_root: str) -> Path: """定位 point_porj 目录,支持多种可能的路径。""" candidates = [ Path(alg_root) / "point_porj", Path(alg_root) / "平整度、平面度、轮廓测量v1.2" / "point_porj", Path(alg_root) / "平整度、平面度、轮廓测量" / "point_porj", ] for c in candidates: if c.is_dir() and (c / "pipeline.py").exists(): return c raise RuntimeError( f"找不到 point_porj 算法目录,已尝试:\n" + "\n".join(f" {c}" for c in candidates) + "\n请确认 alg/ 下已解压 point_porj/。" ) def _build_pipeline(alg_dir: Path, pc_path: str, out_dir: Path, params: dict) -> Any: """初始化 MeasurementPipeline,注入点云路径和输出路径。""" # 把 alg_dir 加到 sys.path,让 pipeline.py 能 import 同级模块 alg_dir_str = str(alg_dir) if alg_dir_str not in sys.path: sys.path.insert(0, alg_dir_str) from pipeline import MeasurementPipeline # type: ignore # 加载内置 config.yaml,然后覆盖关键路径 pipeline = MeasurementPipeline(config_path=None) # 注入点云路径 pipeline.config["paths"]["pointcloud"] = pc_path # 注入输出路径(curvature_output 写到 outDir) pipeline.config["paths"]["curvature_output"] = str(out_dir / "curvature.ply") # 关闭可视化(无显示器) pipeline.config["visualization"]["enabled"] = False # 用户参数覆盖 voxel_size = _safe_float(params.get("voxelSizeMm")) if voxel_size is not None: pipeline.config["filters"]["voxel_size"] = voxel_size / 1000.0 # mm → m threshold_mm = _safe_float(params.get("thresholdMm")) if threshold_mm is not None: pipeline.config["plane"]["threshold_mm"] = threshold_mm return pipeline def main() -> int: payload = load_input(sys.argv) inputs = payload.get("inputs") or {} config = inputs.get("config") or {} out_dir = Path(payload["outDir"]) out_dir.mkdir(parents=True, exist_ok=True) try: alg_root = os.environ.get("ALG_ROOT") or os.environ.get("ALG_ROOT_HOST") or "/alg" emit_progress(0.05, "定位 point_porj 算法目录") alg_dir = _find_alg_dir(alg_root) pc_path_raw = first_pointcloud(payload) emit_progress(0.10, f"读取点云:{os.path.basename(pc_path_raw)}") # 二进制 PLY → ASCII PLY(算法内置 reader 不依赖 open3d) pc_path = ensure_ascii_ply(pc_path_raw, out_dir / "_ply_cache") module = str(config.get("module") or "all") emit_progress(0.15, f"初始化 MeasurementPipeline(module={module})") pipeline = _build_pipeline(alg_dir, pc_path, out_dir, config) measurements: list[dict] = [] extra: dict[str, Any] = {"module": module} skipped: list[str] = [] # ── plane ────────────────────────────────────────────────────────────── if module in ("all", "plane"): emit_progress(0.30, "平面拟合 (RANSAC)") try: res = pipeline.run_plane( pointcloud_path=pc_path, threshold_mm=_safe_float(config.get("thresholdMm")), no_visualize=True, ) pr = res.get("plane_result") if pr is not None: rmse_mm = float(pr.rmse) * 1000.0 # m → mm measurements += [ { "name": "平面 RMSE", "value": round(rmse_mm, 4), "unit": "mm", "status": "pass" if rmse_mm <= 1.0 else "warning" if rmse_mm <= 2.0 else "fail", }, { "name": "内点率", "value": round(float(pr.inlier_ratio), 2), "unit": "%", "status": "pass" if pr.inlier_ratio >= 80 else "warning" if pr.inlier_ratio >= 60 else "fail", }, { "name": "法向角误差", "value": round(float(pr.normal_angle_deg), 4), "unit": "deg", "status": "info", }, ] extra["planeModel"] = list(pr.model) if pr.model else None except Exception as exc: skipped.append(f"plane: {exc}") # ── curvature ────────────────────────────────────────────────────────── if module in ("all", "curvature"): emit_progress(0.55, "曲率估计") try: res = pipeline.run_curvature( pointcloud_path=pc_path, no_visualize=True, ) curvatures = res.get("curvatures") if curvatures is not None: import numpy as np measurements += [ { "name": "曲率均值", "value": round(float(np.mean(curvatures)), 6), "unit": "1/mm", "status": "info", }, { "name": "曲率最大值", "value": round(float(np.max(curvatures)), 6), "unit": "1/mm", "status": "info", }, ] # 保存曲率点云路径 curv_ply = out_dir / "curvature.ply" if curv_ply.exists(): extra["curvaturePly"] = "curvature.ply" except Exception as exc: skipped.append(f"curvature: {exc}") # ── contour ──────────────────────────────────────────────────────────── if module in ("all", "contour"): emit_progress(0.75, "轮廓-平整度(需 CAD STL)") cad_path = inputs.get("cadPath") or None mesh_path = inputs.get("meshPath") or None if not cad_path and not mesh_path: skipped.append("contour: 未提供 CAD STL/STEP,跳过") else: try: save_path = str(out_dir / "contour.ply") excel_path = str(out_dir / "contour.xlsx") res = pipeline.run_contour( pointcloud_path=pc_path, cad_path=cad_path, mesh_path=mesh_path, save_path=save_path, excel_output_path=excel_path, no_visualize=True, ) stats = res.get("distance_stats") for key, label, unit in [ ("mean", "轮廓偏差均值", "mm"), ("max", "轮廓偏差最大值", "mm"), ("std", "轮廓偏差标准差", "mm"), ]: v = _safe_float(getattr(stats, key, None) if stats is not None else None) if v is not None: measurements.append({ "name": label, "value": round(v, 4), "unit": unit, "status": "info", }) if Path(save_path).exists(): extra["contourPly"] = "contour.ply" except Exception as exc: skipped.append(f"contour: {exc}") emit_progress(0.95, "整理结果") # 3D 叠加层:优先 contour PLY,其次 curvature PLY overlay3d: dict = {"kind": "none"} for ply_key in ("contourPly", "curvaturePly"): if ply_key in extra: overlay3d = {"kind": "vertexColors", "url": extra[ply_key]} break if skipped: extra["skipped"] = skipped status_vals = [m.get("status") for m in measurements] overall = ( "fail" if "fail" in status_vals else "warning" if "warning" in status_vals else "completed" ) payload_out = { "schemaVersion": 1, "status": overall, "message": ( f"完成 {len(measurements)} 项测量。" + (f" 跳过:{'; '.join(skipped)}" if skipped else "") ), "measurements": measurements, "overlay3d": overlay3d, "extra": extra, } write_result(out_dir, payload_out) emit_progress(1.0, "完成") return 0 except Exception as exc: traceback.print_exc() write_error_result( out_dir, f"flatness-suite 计算失败:{exc}", extra={"traceback": traceback.format_exc()}, ) return 1 if __name__ == "__main__": sys.exit(main())