- 算法包替换为 delivery_v1.2(coupler_panel 新增 autotune/layer_select,underframe_deflection 新增 single_beam 管线),删除 delivery_v1.1 - manifests/adapters 的 PYTHONPATH 与版本说明同步更新到 v1.2 - 新增任务级 CAD 文件支持(task_cad_files 表、上传/读取路由、TaskManager 全选与加载显示) - 新增 train-measurement 自定义前端面板(YAML 编辑器 + 可视化 ROI 配置 + 3D 取点) - PointCloudViewer 支持 PLY 叠加层渲染与 ROI 拾取 - Dockerfile 改用 Miniforge 安装 pythonocc-core,修复 libgomp/libstdc++ 依赖
252 lines
8.3 KiB
Python
252 lines
8.3 KiB
Python
"""Public Python API — **measurement algorithm only**.
|
|
|
|
This module is what software developers integrate against. It takes a
|
|
*clean, already-registered* point cloud (in the CAD / world frame) of
|
|
the coupler panel and returns a :class:`CouplerMeasurement`.
|
|
|
|
Responsibilities of this layer:
|
|
|
|
validate → detect 4 holes → build reference frame
|
|
→ extract panel ROI → fit panel plane → compute metrics
|
|
|
|
Things that are **not** in this layer (they live in
|
|
:mod:`coupler_panel.data_prep`):
|
|
|
|
- Reading STEP files / generating synthetic point clouds
|
|
- Adding noise / random rigid transforms
|
|
- Denoising (passthrough / SOR / ROR / voxel / smoothing)
|
|
- 4-hole Kabsch registration into the CAD frame
|
|
|
|
The reason: those steps are *engineering* steps that vary per sensor
|
|
setup; the production caller usually has their own scan + cleanup
|
|
pipeline and just wants the deflection numbers out the other side.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
import tracemalloc
|
|
from typing import Any, Callable
|
|
|
|
import numpy as np
|
|
|
|
from ._version import ALGORITHM_VERSION, SCHEMA_VERSION
|
|
from .config import load_config, resolve_auto
|
|
from .detection import detect_four_holes
|
|
from .exceptions import (
|
|
InsufficientPointsError,
|
|
InvalidPointCloudError,
|
|
PlaneFitError,
|
|
)
|
|
from .geometry import build_reference_frame, extract_panel_roi, fit_panel_plane
|
|
from .metrics import compute_deflection_metrics
|
|
from .structures import (
|
|
CouplerMeasurement,
|
|
HoleSet,
|
|
StageTiming,
|
|
)
|
|
|
|
|
|
ProgressCallback = Callable[[str, float], None]
|
|
|
|
|
|
_STAGES = (
|
|
"validate",
|
|
"detect_holes",
|
|
"build_frame",
|
|
"extract_roi",
|
|
"fit_plane",
|
|
"metrics",
|
|
)
|
|
|
|
|
|
def _ping(progress: ProgressCallback | None, stage: str, frac: float) -> None:
|
|
if progress is not None:
|
|
try:
|
|
progress(stage, float(frac))
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _validate_points(points: Any) -> np.ndarray:
|
|
try:
|
|
pts = np.asarray(points, dtype=np.float64)
|
|
except Exception as exc:
|
|
raise InvalidPointCloudError(f"could not coerce points to ndarray: {exc}") from exc
|
|
if pts.ndim != 2 or pts.shape[1] != 3:
|
|
raise InvalidPointCloudError(f"expected (N, 3) points; got shape {pts.shape}")
|
|
if pts.shape[0] < 200:
|
|
raise InsufficientPointsError(f"too few points: {pts.shape[0]} (need ≥ 200)")
|
|
if not np.isfinite(pts).all():
|
|
raise InvalidPointCloudError("input contains NaN/Inf")
|
|
return pts
|
|
|
|
|
|
def compute_coupler_measurement(
|
|
points: Any,
|
|
config: str | dict[str, Any] | None = None,
|
|
profile: str = "auto",
|
|
*,
|
|
panel_normal_seed: np.ndarray | None = None,
|
|
progress: ProgressCallback | None = None,
|
|
) -> CouplerMeasurement:
|
|
"""Run the **measurement algorithm** on an already-prepared point cloud.
|
|
|
|
Parameters
|
|
----------
|
|
points : array-like
|
|
``(N, 3)`` cloud in the CAD / world frame, units mm.
|
|
The cloud is expected to be denoised and rigidly aligned to the
|
|
CAD frame before this call. See :mod:`coupler_panel.data_prep`
|
|
for a reference implementation of that preparation step.
|
|
config : path / dict / None
|
|
Optional override of the default YAML config.
|
|
profile : str
|
|
``"auto"`` / ``"default"`` skip profile loading. Other names are
|
|
resolved against ``configs/profiles/<name>.yaml``.
|
|
panel_normal_seed : (3,) or None
|
|
Optional prior on the panel normal (e.g. the GT normal). Speeds
|
|
up and stabilises the in-plane projection used by hole detection.
|
|
``None`` falls back to PCA on the cloud.
|
|
progress : callable or None
|
|
``progress(stage_name, fraction)`` called once per stage.
|
|
|
|
Returns
|
|
-------
|
|
:class:`CouplerMeasurement`
|
|
|
|
Raises
|
|
------
|
|
InvalidPointCloudError, InsufficientPointsError, HoleDetectionError,
|
|
PlaneFitError, CouplerError
|
|
"""
|
|
cfg = load_config(config, profile=profile)
|
|
cfg = resolve_auto(cfg)
|
|
|
|
quality_flags: list[str] = []
|
|
timings: list[StageTiming] = []
|
|
|
|
# ---- validate ----------------------------------------------------------
|
|
pts = _validate_points(points)
|
|
_ping(progress, "validate", 1.0 / len(_STAGES))
|
|
|
|
# ---- detect 4 holes ----------------------------------------------------
|
|
t0 = time.perf_counter()
|
|
tracemalloc.start()
|
|
detection = detect_four_holes(
|
|
pts,
|
|
detection_cfg=cfg["detection"],
|
|
cad_cfg=cfg["cad"],
|
|
panel_normal_seed=panel_normal_seed,
|
|
)
|
|
_, peak = tracemalloc.get_traced_memory()
|
|
tracemalloc.stop()
|
|
timings.append(StageTiming("detect_holes", time.perf_counter() - t0, peak / 1024))
|
|
_ping(progress, "detect_holes", 2.0 / len(_STAGES))
|
|
|
|
detected_holes = detection.holes_3d
|
|
|
|
# ---- reference frame ---------------------------------------------------
|
|
t0 = time.perf_counter()
|
|
frame = build_reference_frame(detected_holes)
|
|
timings.append(StageTiming("build_frame", time.perf_counter() - t0, 0.0))
|
|
_ping(progress, "build_frame", 3.0 / len(_STAGES))
|
|
|
|
# ---- extract panel ROI -------------------------------------------------
|
|
t0 = time.perf_counter()
|
|
tracemalloc.start()
|
|
if panel_normal_seed is not None:
|
|
seed3 = np.asarray(panel_normal_seed, dtype=np.float64)
|
|
else:
|
|
seed3 = np.array([1.0, 0.0, 0.0])
|
|
roi_pts, roi_diag = extract_panel_roi(
|
|
pts,
|
|
holes=detected_holes,
|
|
panel_normal_seed=seed3,
|
|
roi_cfg=cfg["roi"],
|
|
)
|
|
if roi_pts.shape[0] < 100:
|
|
raise InsufficientPointsError(
|
|
f"only {roi_pts.shape[0]} points survived ROI extraction"
|
|
)
|
|
_, peak = tracemalloc.get_traced_memory()
|
|
tracemalloc.stop()
|
|
timings.append(StageTiming("extract_roi", time.perf_counter() - t0, peak / 1024))
|
|
_ping(progress, "extract_roi", 4.0 / len(_STAGES))
|
|
|
|
# ---- fit panel plane ---------------------------------------------------
|
|
t0 = time.perf_counter()
|
|
tracemalloc.start()
|
|
plane = fit_panel_plane(roi_pts, plane_fit_cfg=cfg["plane_fit"])
|
|
if plane.inlier_ratio < float(cfg["plane_fit"].get("min_inlier_ratio", 0.6)):
|
|
quality_flags.append("plane_inlier_ratio_low")
|
|
_, peak = tracemalloc.get_traced_memory()
|
|
tracemalloc.stop()
|
|
timings.append(StageTiming("fit_plane", time.perf_counter() - t0, peak / 1024))
|
|
_ping(progress, "fit_plane", 5.0 / len(_STAGES))
|
|
|
|
# ---- metrics -----------------------------------------------------------
|
|
t0 = time.perf_counter()
|
|
metrics_cfg = cfg.get("metrics", {})
|
|
metrics = compute_deflection_metrics(
|
|
normal=plane.normal,
|
|
center_C=frame.center_C,
|
|
panel_points=roi_pts,
|
|
right_axis_sign=int(metrics_cfg.get("right_axis_sign", 1)),
|
|
angle_clamp_deg=float(metrics_cfg.get("angle_clamp_deg", 0.0)),
|
|
)
|
|
timings.append(StageTiming("metrics", time.perf_counter() - t0, 0.0))
|
|
_ping(progress, "metrics", 1.0)
|
|
|
|
report = {
|
|
"schema_version": SCHEMA_VERSION,
|
|
"algorithm_version": ALGORITHM_VERSION,
|
|
"config": cfg,
|
|
"n_points_in": int(pts.shape[0]),
|
|
"detection": detection.diagnostics,
|
|
"roi": roi_diag,
|
|
"plane": {
|
|
"normal": list(plane.normal),
|
|
"point_on_plane": list(plane.point_on_plane),
|
|
"d_offset": plane.d_offset,
|
|
"inlier_ratio": plane.inlier_ratio,
|
|
"inlier_residual_rms_mm": plane.inlier_residual_rms_mm,
|
|
},
|
|
"frame": {
|
|
"center_C": list(frame.center_C),
|
|
"reference_plane_z": frame.reference_plane_z,
|
|
"longitudinal_mid_y": frame.longitudinal_mid_y,
|
|
},
|
|
"metrics": metrics.to_dict(),
|
|
"quality_flags": quality_flags,
|
|
"timings": [
|
|
{"stage": t.stage, "seconds": t.seconds, "peak_kib": t.peak_kib}
|
|
for t in timings
|
|
],
|
|
}
|
|
|
|
return CouplerMeasurement(
|
|
schema_version=SCHEMA_VERSION,
|
|
algorithm_version=ALGORITHM_VERSION,
|
|
theta_right_deg=metrics.theta_right_deg,
|
|
theta_ref_deg=metrics.theta_ref_deg,
|
|
D_y_max_mm=metrics.D_y_max_mm,
|
|
D_z_max_mm=metrics.D_z_max_mm,
|
|
dx_y_max_mm=metrics.dx_y_max_mm,
|
|
dx_z_max_mm=metrics.dx_z_max_mm,
|
|
center_C=frame.center_C,
|
|
normal=plane.normal,
|
|
holes=detected_holes,
|
|
plane=plane,
|
|
frame=frame,
|
|
quality_flags=quality_flags,
|
|
timings=timings,
|
|
report=report,
|
|
)
|
|
|
|
|
|
__all__ = [
|
|
"compute_coupler_measurement",
|
|
"ProgressCallback",
|
|
]
|