feat: initial commit - massive-cloud point cloud processing system
This commit is contained in:
271
server/plugins/adapters/flatness_suite_run.py
Normal file
271
server/plugins/adapters/flatness_suite_run.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user