feat: initial commit - massive-cloud point cloud processing system

This commit is contained in:
2026-05-27 09:51:42 +00:00
commit 16811100d1
391 changed files with 78338 additions and 0 deletions

View 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())