算法更新: delivery v1.2.1、平整度v1.5、测距v2.2、整车配准v1.3 适配器: vehicle-registration 新增、各算法 manifest 和 adapter 更新 后端: taskCadFiles/source 字段迁移、derivedPointClouds 注册、取消任务联动 前端: ElapsedTime 时长显示、两步算法流程、canRun 逻辑修复 文档: 新增5份算法性能分析报告
370 lines
12 KiB
Python
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
|