Files
massive-cloud/alg/delivery_v1.2.1/underframe_deflection/config.py
lipan 29f1d45d66 feat: 算法升级、整车配准集成与性能分析文档
算法更新: delivery v1.2.1、平整度v1.5、测距v2.2、整车配准v1.3
适配器: vehicle-registration 新增、各算法 manifest 和 adapter 更新
后端: taskCadFiles/source 字段迁移、derivedPointClouds 注册、取消任务联动
前端: ElapsedTime 时长显示、两步算法流程、canRun 逻辑修复
文档: 新增5份算法性能分析报告
2026-06-25 08:22:20 +00:00

370 lines
12 KiB
Python

from __future__ import annotations
import copy
import json
from pathlib import Path
from typing import Any
import numpy as np
DEFAULT_CONFIG: dict[str, Any] = {
"preprocess": {
"stat_k": 18,
"stat_std_ratio": 2.8,
"radius_spacing_multiplier": 2.8,
"radius_min_mm": 12.0,
"radius_min_neighbors": 4,
"voxel_spacing_multiplier": 1.5,
"voxel_min_mm": 8.0,
"normal_k": 20,
"spacing_sample_size": 4000,
# Optional passthrough crop performed before any other filter.
# ``aabb`` is populated from the external ROI JSON when supplied.
"passthrough": {
"enabled": False,
"aabb": None,
},
# Point-cloud bilateral filter applied after radius filtering.
"bilateral": {
"enabled": True,
"k": 24,
"sigma_d_spacing_multiplier": 2.0,
"sigma_d_min_mm": 2.0,
"sigma_n_deg": 15.0,
"iterations": 1,
},
},
"frame": {
"use_input_axes": True,
"coarse_slice_count": 28,
"coarse_y_bins": 30,
"bottom_quantile": 0.14,
"low_band_margin_spacing_multiplier": 6.0,
"candidate_bin_min_points": 12,
"normal_alignment_min": 0.55,
"flatness_spacing_multiplier": 4.0,
"flatness_min_mm": 8.0,
"symmetry_weight": 1.25,
"bottom_weight": 1.3,
"flatness_weight": 1.0,
"hole_weight": 0.55,
},
"roi": {
"x_trim_quantile": 0.004,
"bottom_height_ratio": 0.24,
"bottom_quantile": 0.18,
"z_margin_spacing_multiplier": 3.0,
"min_z_ceiling_mm": 1500.0,
# External axis-aligned bbox supplied via JSON.
# Fields: x_min, x_max, y_min, y_max, z_min, z_max (mm, CAD frame).
"aabb": None,
},
"beam": {
"slice_thickness_length_ratio": 0.012,
"slice_thickness_spacing_multiplier": 22.0,
"slice_step_ratio": 0.5,
"slice_min_points": 80,
"y_bin_width_width_ratio": 0.03,
"y_bin_width_spacing_multiplier": 3.5,
"low_quantile": 0.18,
"low_core_quantile": 0.28,
"min_bin_points": 10,
"width_min_ratio": 0.018,
"width_max_ratio": 0.18,
"width_preferred_ratio": 0.065,
"flatness_spacing_multiplier": 4.5,
"flatness_min_mm": 10.0,
"normal_alignment_min": 0.6,
"low_band_margin_spacing_multiplier": 8.0,
"midline_trim_quantile": 0.02,
"edge_preference_weight": 3.2,
"strong_edge_ratio": 0.58,
"inner_reject_ratio": 0.32,
"edge_keep_min_ratio": 0.72,
"relative_edge_window": 0.18,
"drop_weak_edge_slices": True,
},
"tracking": {
"top_k": 3,
"beam_width": 24,
"min_coverage_ratio": 0.28,
"weights": {
"coverage_reward": 2.2,
"node_reward": 1.0,
"y_penalty": 1.6,
"z_penalty": 1.4,
"width_penalty": 0.7,
"normal_penalty": 0.8,
"gap_penalty": 2.0,
"gap_growth_penalty": 0.7,
"jitter_penalty": 0.9,
},
},
"representative": {
"band_y_margin_spacing_multiplier": 4.0,
"band_z_margin_spacing_multiplier": 10.0,
"core_quantile": 0.28,
"core_margin_spacing_multiplier": 5.0,
"underside_profile_bin_count": 14,
"underside_quantile": 0.2,
"underside_outer_fraction": 0.4,
"underside_rise_spacing_multiplier": 0.55,
"underside_core_spacing_multiplier": 0.7,
"underside_profile_margin_spacing_multiplier": 1.0,
"underside_min_bin_points": 18,
"max_iter": 5,
"spline_smoothing_y_spacing_multiplier": 3.0,
"spline_smoothing_z_spacing_multiplier": 8.0,
"max_gap_slices": 4,
"grid_step_slice_ratio": 1.0,
# Robust rejection of representative points contaminated by gussets /
# cross-members before the spline fit. Window is in units of beam
# slice steps; sigma is the robust (MAD-based) rejection threshold.
"outlier_rejection_enabled": True,
"outlier_window_slices": 2.5,
"outlier_sigma": 2.5,
},
"reference": {
"mode": "line_endpoints",
"stable_region_fraction": 0.14,
"search_extension_spacing_multiplier": 12.0,
"search_extension_min_mm": 650.0,
"lateral_margin_width_ratio": 0.11,
"vertical_margin_height_ratio": 0.22,
"underside_lower_margin_spacing_multiplier": 1.2,
"underside_upper_margin_spacing_multiplier": 3.8,
"circle_trials": 360,
"circle_sample_size": 700,
"normal_axis_max_alignment": 0.65,
"preferred_axes": ["y"],
"radius_min_mm": 35.0,
"radius_max_width_ratio": 0.04,
"fit_tolerance_spacing_multiplier": 0.38,
"coverage_bin_count": 20,
"min_inliers": 18,
"cluster_eps_spacing_multiplier": 2.5,
"cluster_min_samples": 2,
"x_window_count": 6,
"x_window_overlap_ratio": 0.35,
"axis_bonus": {
"y": 0.42,
"z": 0.18,
"x": -0.85
},
"extremity_reward": 2.2,
"underside_penalty_weight": 1.2,
"vertical_center_penalty_weight": 0.25,
"lateral_penalty_weight": 0.55,
"low_confidence_threshold": 0.6,
},
"deflection": {
"evaluation_grid_factor": 1.0,
},
"registration": {
"enabled": True,
"skip": False,
"icp_max_iter": 60,
"icp_tol_mm": 0.05,
"icp_tol_rel": 1.0e-4,
"icp_max_corr_spacing_multiplier": 6.0,
"icp_max_corr_min_mm": 25.0,
"icp_downsample_target": 30000,
"trim_ratio": 0.2,
"pca_sign_disambiguation": True,
# If the recovered transform is within these tolerances of identity
# (and snapping does not worsen correspondence), return exact identity
# so an already-aligned scan is left untouched.
"identity_rot_tol_deg": 0.5,
"identity_trans_tol_mm": 5.0,
},
"cad": {
"path": None,
"downsample_voxel_mm": 4.0,
},
"visualization": {
"max_scatter_points": 40000,
"dpi": 180,
},
}
PROFILE_OVERRIDES: dict[str, dict[str, Any]] = {
"auto": {},
"M": {
"beam": {
"slice_thickness_length_ratio": 0.011,
"width_preferred_ratio": 0.06,
}
},
"TC": {
"beam": {
"slice_thickness_length_ratio": 0.014,
"width_preferred_ratio": 0.07,
},
"reference": {"stable_region_fraction": 0.16},
},
"TP": {
"beam": {
"slice_thickness_length_ratio": 0.013,
"width_preferred_ratio": 0.072,
},
"reference": {"vertical_margin_height_ratio": 0.24},
},
"custom": {},
}
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
merged = copy.deepcopy(base)
for key, value in override.items():
if isinstance(value, dict) and isinstance(merged.get(key), dict):
merged[key] = deep_merge(merged[key], value)
else:
merged[key] = copy.deepcopy(value)
return merged
def load_config(config_path: str | None, profile: str) -> dict[str, Any]:
profile_name = profile if profile in PROFILE_OVERRIDES else "auto"
config = deep_merge(DEFAULT_CONFIG, PROFILE_OVERRIDES[profile_name])
if config_path:
user_config = json.loads(Path(config_path).read_text(encoding="utf-8"))
config = deep_merge(config, user_config)
return config
def cloud_geometry(points: np.ndarray) -> dict[str, float]:
mins = points.min(axis=0)
maxs = points.max(axis=0)
spans = maxs - mins
return {
"x_min": float(mins[0]),
"x_max": float(maxs[0]),
"y_min": float(mins[1]),
"y_max": float(maxs[1]),
"z_min": float(mins[2]),
"z_max": float(maxs[2]),
"length": float(spans[0]),
"width": float(spans[1]),
"height": float(spans[2]),
}
def resolve_numeric_parameters(
config: dict[str, Any],
geometry: dict[str, float],
spacing: float,
) -> dict[str, Any]:
resolved = copy.deepcopy(config)
length = geometry["length"]
width = geometry["width"]
height = geometry["height"]
preprocess = resolved["preprocess"]
preprocess["radius_mm"] = max(
preprocess["radius_min_mm"],
preprocess["radius_spacing_multiplier"] * spacing,
)
preprocess["voxel_mm"] = max(
preprocess["voxel_min_mm"],
preprocess["voxel_spacing_multiplier"] * spacing,
)
bilateral = preprocess.get("bilateral")
if isinstance(bilateral, dict):
bilateral["sigma_d_mm"] = max(
float(bilateral.get("sigma_d_min_mm", 2.0)),
float(bilateral.get("sigma_d_spacing_multiplier", 2.0)) * spacing,
)
bilateral["sigma_n_rad"] = float(
np.deg2rad(float(bilateral.get("sigma_n_deg", 15.0)))
)
frame = resolved["frame"]
frame["flatness_mm"] = max(
frame["flatness_min_mm"],
frame["flatness_spacing_multiplier"] * spacing,
)
frame["low_band_margin_mm"] = (
frame["low_band_margin_spacing_multiplier"] * spacing
)
roi = resolved["roi"]
roi["z_margin_mm"] = roi["z_margin_spacing_multiplier"] * spacing
beam = resolved["beam"]
beam["slice_thickness_mm"] = max(
beam["slice_thickness_length_ratio"] * length,
beam["slice_thickness_spacing_multiplier"] * spacing,
)
beam["slice_step_mm"] = beam["slice_thickness_mm"] * beam["slice_step_ratio"]
beam["y_bin_width_mm"] = max(
beam["y_bin_width_width_ratio"] * width,
beam["y_bin_width_spacing_multiplier"] * spacing,
)
beam["width_min_mm"] = beam["width_min_ratio"] * width
beam["width_max_mm"] = beam["width_max_ratio"] * width
beam["width_preferred_mm"] = beam["width_preferred_ratio"] * width
beam["flatness_mm"] = max(
beam["flatness_min_mm"],
beam["flatness_spacing_multiplier"] * spacing,
)
beam["low_band_margin_mm"] = (
beam["low_band_margin_spacing_multiplier"] * spacing
)
representative = resolved["representative"]
representative["band_y_margin_mm"] = (
representative["band_y_margin_spacing_multiplier"] * spacing
)
representative["band_z_margin_mm"] = (
representative["band_z_margin_spacing_multiplier"] * spacing
)
representative["core_margin_mm"] = (
representative["core_margin_spacing_multiplier"] * spacing
)
representative["grid_step_mm"] = (
representative["grid_step_slice_ratio"] * beam["slice_step_mm"]
)
reference = resolved["reference"]
reference["lateral_margin_mm"] = (
reference["lateral_margin_width_ratio"] * width
)
reference["vertical_margin_mm"] = (
reference["vertical_margin_height_ratio"] * height
)
reference["search_extension_mm"] = max(
reference["search_extension_min_mm"],
reference["search_extension_spacing_multiplier"] * spacing,
)
reference["underside_lower_margin_mm"] = (
reference["underside_lower_margin_spacing_multiplier"] * spacing
)
reference["underside_upper_margin_mm"] = (
reference["underside_upper_margin_spacing_multiplier"] * spacing
)
reference["radius_max_mm"] = max(
reference["radius_min_mm"] * 1.5,
reference["radius_max_width_ratio"] * width,
)
reference["fit_tolerance_mm"] = (
reference["fit_tolerance_spacing_multiplier"] * spacing
)
reference["cluster_eps_mm"] = (
reference["cluster_eps_spacing_multiplier"] * spacing
)
registration = resolved.get("registration")
if isinstance(registration, dict):
registration["icp_max_corr_mm"] = max(
float(registration.get("icp_max_corr_min_mm", 25.0)),
float(registration.get("icp_max_corr_spacing_multiplier", 6.0)) * spacing,
)
resolved["runtime"] = {
"spacing_mm": float(spacing),
"geometry": geometry,
}
return resolved