Files
massive-cloud/server/plugins/adapters/flatness_suite_run.py

272 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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())