feat: 算法升级、整车配准集成与性能分析文档

算法更新: delivery v1.2.1、平整度v1.5、测距v2.2、整车配准v1.3
适配器: vehicle-registration 新增、各算法 manifest 和 adapter 更新
后端: taskCadFiles/source 字段迁移、derivedPointClouds 注册、取消任务联动
前端: ElapsedTime 时长显示、两步算法流程、canRun 逻辑修复
文档: 新增5份算法性能分析报告
This commit is contained in:
2026-06-25 08:22:20 +00:00
parent cec0766e67
commit 29f1d45d66
266 changed files with 1311475 additions and 2846 deletions

View File

@@ -0,0 +1,209 @@
"""Configuration loading and merging.
Configs are nested dicts. The single source of truth for defaults is
``configs/default.yaml`` (shipped alongside the package); ``DEFAULT_CONFIG``
mirrors its structure for in-memory programmatic overrides.
Public functions:
- :func:`load_config` — load a YAML file or accept an already-built dict.
- :func:`load_profile` — load ``configs/profiles/<name>.yaml`` and merge
on top of the defaults.
- :func:`deep_merge` — recursive dict merge (override wins).
- :func:`apply_dotted` — set a value via ``"sampling.spacing_mm"``.
- :func:`resolve_auto` — fill in fields whose value is the literal
string ``"auto"`` (currently: ``plane_fit.ransac_inlier_thresh_mm``).
"""
from __future__ import annotations
import copy
from pathlib import Path
from typing import Any
import yaml
_PKG_ROOT = Path(__file__).resolve().parent
_DEFAULT_YAML = _PKG_ROOT.parent / "configs" / "default.yaml"
def _load_yaml(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as fh:
data = yaml.safe_load(fh)
if not isinstance(data, dict):
raise ValueError(f"{path}: expected a mapping at the top level")
return data
def _load_default() -> dict[str, Any]:
if _DEFAULT_YAML.exists():
return _load_yaml(_DEFAULT_YAML)
# Package may have been installed without configs/. Fall back to a minimal
# in-code default so library use without the YAML still works.
return _IN_CODE_FALLBACK
# Loaded lazily; recomputed on each call to keep tests deterministic.
def default_config() -> dict[str, Any]:
"""Return a fresh copy of the default configuration."""
return copy.deepcopy(_load_default())
DEFAULT_CONFIG: dict[str, Any] # populated below for convenience
def load_config(
source: str | Path | dict[str, Any] | None,
*,
profile: str = "auto",
) -> dict[str, Any]:
"""Resolve the final configuration to use.
The merge order is::
default.yaml < profile (if any) < source (path or dict)
Parameters
----------
source : path, dict, or None
If ``None``, only defaults (and the profile) are used.
profile : str
``"auto"`` / ``"default"`` skip the profile step. Any other name
is looked up at ``configs/profiles/<name>.yaml``.
"""
cfg = default_config()
if profile not in (None, "auto", "default", "custom"):
prof_path = _PKG_ROOT.parent / "configs" / "profiles" / f"{profile}.yaml"
if not prof_path.exists():
raise FileNotFoundError(f"profile '{profile}' not found at {prof_path}")
cfg = deep_merge(cfg, _load_yaml(prof_path))
if source is None:
pass
elif isinstance(source, dict):
cfg = deep_merge(cfg, source)
else:
cfg = deep_merge(cfg, _load_yaml(Path(source)))
return cfg
def load_profile(name: str) -> dict[str, Any]:
"""Load a profile YAML directly; useful for tests."""
path = _PKG_ROOT.parent / "configs" / "profiles" / f"{name}.yaml"
return _load_yaml(path)
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Recursive dict merge. ``override`` wins on leaf collisions."""
out = copy.deepcopy(base)
for key, val in override.items():
if (
key in out
and isinstance(out[key], dict)
and isinstance(val, dict)
):
out[key] = deep_merge(out[key], val)
else:
out[key] = copy.deepcopy(val)
return out
def apply_dotted(cfg: dict[str, Any], dotted_key: str, value: Any) -> dict[str, Any]:
"""Set ``cfg["a"]["b"]["c"] = value`` from ``"a.b.c"``. Returns ``cfg``."""
parts = dotted_key.split(".")
node = cfg
for p in parts[:-1]:
node = node.setdefault(p, {})
if not isinstance(node, dict):
raise TypeError(f"cannot apply '{dotted_key}': '{p}' is not a dict")
node[parts[-1]] = value
return cfg
def resolve_auto(cfg: dict[str, Any]) -> dict[str, Any]:
"""Replace literal ``"auto"`` markers with concrete values.
Currently:
- ``plane_fit.ransac_inlier_thresh_mm == "auto"`` → ``3 * noise.sigma_mm``
(or 0.5 mm if ``noise.sigma_mm == 0``).
"""
out = copy.deepcopy(cfg)
pf = out.get("plane_fit", {})
if pf.get("ransac_inlier_thresh_mm", None) == "auto":
sigma = float(out.get("noise", {}).get("sigma_mm", 0.0) or 0.0)
pf["ransac_inlier_thresh_mm"] = max(3.0 * sigma, 0.5)
out["plane_fit"] = pf
return out
# Minimal in-code fallback used only when the YAML is missing.
_IN_CODE_FALLBACK: dict[str, Any] = {
"schema_version": "1.0",
"cad": {
"linear_deflection_mm": 0.5,
"angular_deflection_rad": 0.1,
"locating_hole_radius_mm": [4.0, 12.0],
"central_hole_radius_min_mm": 20.0,
"rectangle_aspect_tol": 0.15,
},
"sampling": {"method": "poisson_disk", "spacing_mm": 1.0, "num_points": 100000, "seed": 42},
"noise": {"enabled": True, "sigma_mm": 0.5, "mode": "isotropic", "seed": 43},
"transform": {
"enabled": True,
"rotation_deg": 3.0,
"rotation_axis": "random",
"translation_mm": 5.0,
"translation_dir": "random",
"seed": 44,
},
"denoise": {"steps": []},
"registration": {"method": "kabsch_four_holes", "pre_align": "pca_centroid"},
"detection": {
"panel_normal_seed": "from_cad",
"grid_pixel_mm": 0.5,
"panel_min_area_ratio": 0.4,
"hole_min_area_mm2": 30.0,
"hole_max_area_mm2": 800.0,
"circle_fit": {"method": "algebraic_then_lsq", "inlier_residual_mm": 0.5},
"select_top_k_by_radius_match": 4,
"rectangle_aspect_tol": 0.20,
},
"roi": {
"hole_dilate_mm": 3.0,
"central_hole_dilate_mm": 5.0,
"edge_buffer_mm": 5.0,
"chamfer_normal_dot_min": 0.85,
"outlier_z_score": 3.0,
},
"plane_fit": {
"method": "ransac_svd",
"ransac_iterations": 200,
"ransac_inlier_thresh_mm": "auto",
"min_inlier_ratio": 0.6,
"enforce_nx_positive": True,
},
"metrics": {"right_axis_sign": 1, "angle_clamp_deg": 0.0},
"artifacts": {
"save_intermediate": True,
"ply_format": "binary",
"image_dpi": 150,
"heatmap_colormap": "viridis",
},
}
DEFAULT_CONFIG = copy.deepcopy(_IN_CODE_FALLBACK)
__all__ = [
"DEFAULT_CONFIG",
"default_config",
"load_config",
"load_profile",
"deep_merge",
"apply_dotted",
"resolve_auto",
]