feat: 升级算法包 delivery_v1.1 → v1.2,新增任务级 CAD 与测距面板
- 算法包替换为 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++ 依赖
This commit is contained in:
335
alg/delivery_v1.2/underframe_deflection/frame.py
Normal file
335
alg/delivery_v1.2/underframe_deflection/frame.py
Normal file
@@ -0,0 +1,335 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
|
||||
import numpy as np
|
||||
from scipy.ndimage import gaussian_filter1d
|
||||
|
||||
from .structures import FrameHypothesis
|
||||
|
||||
|
||||
def normalize(vector: np.ndarray) -> np.ndarray:
|
||||
norm = float(np.linalg.norm(vector))
|
||||
if norm == 0:
|
||||
return vector.copy()
|
||||
return vector / norm
|
||||
|
||||
|
||||
def orthonormalize_axes(x_axis: np.ndarray, z_axis_seed: np.ndarray) -> np.ndarray:
|
||||
x_axis = normalize(x_axis)
|
||||
z_axis_seed = normalize(z_axis_seed)
|
||||
y_axis = normalize(np.cross(z_axis_seed, x_axis))
|
||||
z_axis = normalize(np.cross(x_axis, y_axis))
|
||||
return np.vstack([x_axis, y_axis, z_axis])
|
||||
|
||||
|
||||
def transform_points(points: np.ndarray, axes: np.ndarray, origin: np.ndarray) -> np.ndarray:
|
||||
return (points - origin) @ axes.T
|
||||
|
||||
|
||||
def transform_vectors(vectors: np.ndarray, axes: np.ndarray) -> np.ndarray:
|
||||
return vectors @ axes.T
|
||||
|
||||
|
||||
def generate_frame_hypotheses(points: np.ndarray) -> tuple[np.ndarray, list[np.ndarray]]:
|
||||
centered = points - points.mean(axis=0, keepdims=True)
|
||||
covariance = centered.T @ centered / max(len(points), 1)
|
||||
eigenvalues, eigenvectors = np.linalg.eigh(covariance)
|
||||
order = np.argsort(eigenvalues)[::-1]
|
||||
eigenvectors = eigenvectors[:, order]
|
||||
primary = eigenvectors[:, 0]
|
||||
residual_axes = [eigenvectors[:, 1], eigenvectors[:, 2]]
|
||||
return primary, residual_axes
|
||||
|
||||
|
||||
def _contiguous_ranges(mask: np.ndarray) -> list[tuple[int, int]]:
|
||||
ranges: list[tuple[int, int]] = []
|
||||
start = None
|
||||
for idx, value in enumerate(mask):
|
||||
if value and start is None:
|
||||
start = idx
|
||||
elif not value and start is not None:
|
||||
ranges.append((start, idx - 1))
|
||||
start = None
|
||||
if start is not None:
|
||||
ranges.append((start, len(mask) - 1))
|
||||
return ranges
|
||||
|
||||
|
||||
def _score_frame_hypothesis(
|
||||
coords: np.ndarray,
|
||||
normals: np.ndarray,
|
||||
curvature: np.ndarray,
|
||||
config: dict,
|
||||
) -> tuple[float, float, float, float, float, float]:
|
||||
frame_cfg = config["frame"]
|
||||
coarse_slice_count = frame_cfg["coarse_slice_count"]
|
||||
x_min, x_max = float(coords[:, 0].min()), float(coords[:, 0].max())
|
||||
y_min, y_max = float(coords[:, 1].min()), float(coords[:, 1].max())
|
||||
width = max(y_max - y_min, 1e-6)
|
||||
slice_edges = np.linspace(x_min, x_max, coarse_slice_count + 1)
|
||||
flat_thresh = frame_cfg["flatness_mm"]
|
||||
|
||||
bottom_scores: list[float] = []
|
||||
symmetry_scores: list[float] = []
|
||||
flatness_scores: list[float] = []
|
||||
continuity_flags: list[float] = []
|
||||
hole_scores: list[float] = []
|
||||
|
||||
global_curvature_scale = float(np.quantile(curvature, 0.75) + 1e-6)
|
||||
|
||||
for slice_index in range(coarse_slice_count):
|
||||
mask = (coords[:, 0] >= slice_edges[slice_index]) & (
|
||||
coords[:, 0] < slice_edges[slice_index + 1]
|
||||
)
|
||||
slice_points = coords[mask]
|
||||
if len(slice_points) < frame_cfg["candidate_bin_min_points"] * 6:
|
||||
continue
|
||||
slice_normals = normals[mask]
|
||||
slice_curvature = curvature[mask]
|
||||
trim_q = 0.02
|
||||
y_lo, y_hi = np.quantile(slice_points[:, 1], [trim_q, 1.0 - trim_q])
|
||||
y_mid = 0.5 * (y_lo + y_hi)
|
||||
y_edges = np.linspace(y_lo, y_hi, frame_cfg["coarse_y_bins"] + 1)
|
||||
|
||||
low_z = np.full(frame_cfg["coarse_y_bins"], np.nan)
|
||||
flatness = np.full(frame_cfg["coarse_y_bins"], np.nan)
|
||||
normal_alignment = np.full(frame_cfg["coarse_y_bins"], np.nan)
|
||||
counts = np.zeros(frame_cfg["coarse_y_bins"], dtype=np.int32)
|
||||
curvature_score = np.zeros(frame_cfg["coarse_y_bins"], dtype=np.float64)
|
||||
|
||||
for bin_index in range(frame_cfg["coarse_y_bins"]):
|
||||
bin_mask = (slice_points[:, 1] >= y_edges[bin_index]) & (
|
||||
slice_points[:, 1] < y_edges[bin_index + 1]
|
||||
)
|
||||
counts[bin_index] = int(np.count_nonzero(bin_mask))
|
||||
if counts[bin_index] < frame_cfg["candidate_bin_min_points"]:
|
||||
continue
|
||||
bin_points = slice_points[bin_mask]
|
||||
z_threshold = np.quantile(bin_points[:, 2], frame_cfg["bottom_quantile"])
|
||||
low_mask = bin_points[:, 2] <= z_threshold
|
||||
low_points = bin_points[low_mask]
|
||||
low_z[bin_index] = float(np.median(low_points[:, 2]))
|
||||
flatness[bin_index] = float(
|
||||
np.median(np.abs(low_points[:, 2] - low_z[bin_index]))
|
||||
)
|
||||
normal_alignment[bin_index] = float(
|
||||
np.mean(np.abs(slice_normals[bin_mask][low_mask, 2]))
|
||||
)
|
||||
curvature_score[bin_index] = float(
|
||||
np.mean(slice_curvature[bin_mask][low_mask]) / global_curvature_scale
|
||||
)
|
||||
|
||||
valid = ~np.isnan(low_z)
|
||||
if valid.sum() < 4:
|
||||
continue
|
||||
smoothed_low_z = low_z.copy()
|
||||
smoothed_low_z[valid] = gaussian_filter1d(low_z[valid], sigma=1.0)
|
||||
z_cut = float(
|
||||
np.quantile(smoothed_low_z[valid], 0.25) + frame_cfg["low_band_margin_mm"]
|
||||
)
|
||||
candidate_mask = (
|
||||
valid
|
||||
& (smoothed_low_z <= z_cut)
|
||||
& (flatness <= flat_thresh)
|
||||
& (normal_alignment >= frame_cfg["normal_alignment_min"])
|
||||
)
|
||||
groups = _contiguous_ranges(candidate_mask)
|
||||
if not groups:
|
||||
continue
|
||||
|
||||
group_summaries = []
|
||||
for start, end in groups:
|
||||
y_center = 0.5 * (y_edges[start] + y_edges[end + 1])
|
||||
group_summaries.append(
|
||||
{
|
||||
"start": start,
|
||||
"end": end,
|
||||
"y_center": y_center,
|
||||
"z_low": float(np.nanmedian(smoothed_low_z[start : end + 1])),
|
||||
"flatness": float(np.nanmean(flatness[start : end + 1])),
|
||||
"normal": float(np.nanmean(normal_alignment[start : end + 1])),
|
||||
"count": int(np.sum(counts[start : end + 1])),
|
||||
"curvature": float(np.nanmean(curvature_score[start : end + 1])),
|
||||
}
|
||||
)
|
||||
|
||||
left = [group for group in group_summaries if group["y_center"] < y_mid]
|
||||
right = [group for group in group_summaries if group["y_center"] >= y_mid]
|
||||
if not left or not right:
|
||||
continuity_flags.append(0.35)
|
||||
continue
|
||||
left_best = min(left, key=lambda item: item["z_low"])
|
||||
right_best = min(right, key=lambda item: item["z_low"])
|
||||
|
||||
slice_bottom_ref = float(np.quantile(slice_points[:, 2], 0.15))
|
||||
low_scale = max(np.std(slice_points[:, 2]), 1.0)
|
||||
bottom_scores.append(
|
||||
np.clip(
|
||||
((slice_bottom_ref - left_best["z_low"]) + (slice_bottom_ref - right_best["z_low"]))
|
||||
/ (2.0 * low_scale)
|
||||
+ 1.0,
|
||||
0.0,
|
||||
2.0,
|
||||
)
|
||||
)
|
||||
sym_error = abs(
|
||||
abs(left_best["y_center"] - y_mid) - abs(right_best["y_center"] - y_mid)
|
||||
) / max(width * 0.5, 1.0)
|
||||
symmetry_scores.append(float(np.clip(1.0 - sym_error, 0.0, 1.0)))
|
||||
flatness_scores.append(
|
||||
float(
|
||||
np.clip(
|
||||
(
|
||||
(1.0 - left_best["flatness"] / max(flat_thresh, 1e-6))
|
||||
+ (1.0 - right_best["flatness"] / max(flat_thresh, 1e-6))
|
||||
)
|
||||
/ 2.0
|
||||
* (left_best["normal"] + right_best["normal"])
|
||||
/ 2.0,
|
||||
0.0,
|
||||
1.5,
|
||||
)
|
||||
)
|
||||
)
|
||||
continuity_flags.append(1.0)
|
||||
hole_scores.append(
|
||||
float(
|
||||
np.clip(
|
||||
(left_best["curvature"] + right_best["curvature"]) / 2.0,
|
||||
0.0,
|
||||
1.5,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if not continuity_flags:
|
||||
return (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
bottom_score = float(np.mean(bottom_scores)) if bottom_scores else 0.0
|
||||
symmetry_score = float(np.mean(symmetry_scores)) if symmetry_scores else 0.0
|
||||
flatness_score = float(np.mean(flatness_scores)) if flatness_scores else 0.0
|
||||
hole_score = float(np.mean(hole_scores)) if hole_scores else 0.0
|
||||
continuity_score = float(np.mean(continuity_flags))
|
||||
|
||||
total_score = (
|
||||
frame_cfg["bottom_weight"] * bottom_score
|
||||
+ frame_cfg["symmetry_weight"] * symmetry_score
|
||||
+ frame_cfg["flatness_weight"] * flatness_score
|
||||
+ frame_cfg["hole_weight"] * hole_score
|
||||
+ continuity_score
|
||||
)
|
||||
return (
|
||||
total_score,
|
||||
bottom_score,
|
||||
symmetry_score,
|
||||
flatness_score,
|
||||
hole_score,
|
||||
continuity_score,
|
||||
)
|
||||
|
||||
|
||||
def infer_vehicle_frame(
|
||||
points: np.ndarray,
|
||||
normals: np.ndarray,
|
||||
curvature: np.ndarray,
|
||||
config: dict,
|
||||
) -> tuple[list[FrameHypothesis], FrameHypothesis]:
|
||||
origin = points.mean(axis=0)
|
||||
primary_axis, residual_axes = generate_frame_hypotheses(points)
|
||||
hypotheses: list[FrameHypothesis] = []
|
||||
for x_sign in (-1.0, 1.0):
|
||||
for z_base_index, z_base in enumerate(residual_axes):
|
||||
for z_sign in (-1.0, 1.0):
|
||||
axes = orthonormalize_axes(x_sign * primary_axis, z_sign * z_base)
|
||||
coords = transform_points(points, axes, origin)
|
||||
transformed_normals = transform_vectors(normals, axes)
|
||||
score_tuple = _score_frame_hypothesis(
|
||||
coords,
|
||||
transformed_normals,
|
||||
curvature,
|
||||
config,
|
||||
)
|
||||
name = f"x{x_sign:+.0f}_z{z_base_index + 2}{z_sign:+.0f}"
|
||||
hypotheses.append(
|
||||
FrameHypothesis(
|
||||
name=name,
|
||||
axes=axes,
|
||||
origin=origin,
|
||||
score=score_tuple[0],
|
||||
bottom_score=score_tuple[1],
|
||||
symmetry_score=score_tuple[2],
|
||||
flatness_score=score_tuple[3],
|
||||
hole_score=score_tuple[4],
|
||||
continuity_score=score_tuple[5],
|
||||
)
|
||||
)
|
||||
hypotheses.sort(key=lambda item: item.score, reverse=True)
|
||||
return hypotheses, hypotheses[0]
|
||||
|
||||
|
||||
def build_bottom_roi_mask(points_vehicle: np.ndarray, config: dict) -> tuple[np.ndarray, dict]:
|
||||
roi_cfg = config["roi"]
|
||||
aabb = roi_cfg.get("aabb") if isinstance(roi_cfg, dict) else None
|
||||
z_floor = float(points_vehicle[:, 2].min())
|
||||
if aabb is not None:
|
||||
x_lo = float(aabb["x_min"])
|
||||
x_hi = float(aabb["x_max"])
|
||||
y_lo = float(aabb["y_min"])
|
||||
y_hi = float(aabb["y_max"])
|
||||
z_lo = float(aabb["z_min"])
|
||||
z_ceiling = float(aabb["z_max"])
|
||||
mask = (
|
||||
(points_vehicle[:, 0] >= x_lo)
|
||||
& (points_vehicle[:, 0] <= x_hi)
|
||||
& (points_vehicle[:, 1] >= y_lo)
|
||||
& (points_vehicle[:, 1] <= y_hi)
|
||||
& (points_vehicle[:, 2] >= z_lo)
|
||||
& (points_vehicle[:, 2] <= z_ceiling)
|
||||
)
|
||||
payload = {
|
||||
"source": "external_aabb",
|
||||
"x_min": x_lo,
|
||||
"x_max": x_hi,
|
||||
"y_min": y_lo,
|
||||
"y_max": y_hi,
|
||||
"z_min": z_lo,
|
||||
"z_floor": z_floor,
|
||||
"z_ceiling": z_ceiling,
|
||||
"point_count": int(np.count_nonzero(mask)),
|
||||
}
|
||||
return mask, payload
|
||||
|
||||
x_lo = float(np.quantile(points_vehicle[:, 0], roi_cfg["x_trim_quantile"]))
|
||||
x_hi = float(np.quantile(points_vehicle[:, 0], 1.0 - roi_cfg["x_trim_quantile"]))
|
||||
z_quantile = float(np.quantile(points_vehicle[:, 2], roi_cfg["bottom_quantile"]))
|
||||
z_span = float(points_vehicle[:, 2].max() - z_floor)
|
||||
z_ceiling = min(
|
||||
z_quantile + roi_cfg["z_margin_mm"],
|
||||
z_floor + roi_cfg["bottom_height_ratio"] * z_span,
|
||||
)
|
||||
z_ceiling = max(z_ceiling, roi_cfg["min_z_ceiling_mm"])
|
||||
mask = (
|
||||
(points_vehicle[:, 0] >= x_lo)
|
||||
& (points_vehicle[:, 0] <= x_hi)
|
||||
& (points_vehicle[:, 2] <= z_ceiling)
|
||||
)
|
||||
payload = {
|
||||
"source": "auto",
|
||||
"x_min": x_lo,
|
||||
"x_max": x_hi,
|
||||
"z_floor": z_floor,
|
||||
"z_ceiling": z_ceiling,
|
||||
"point_count": int(np.count_nonzero(mask)),
|
||||
}
|
||||
return mask, payload
|
||||
|
||||
|
||||
def frame_hypotheses_to_dict(hypotheses: list[FrameHypothesis]) -> list[dict]:
|
||||
payload = []
|
||||
for hypothesis in hypotheses:
|
||||
item = asdict(hypothesis)
|
||||
item["axes"] = hypothesis.axes.tolist()
|
||||
item["origin"] = hypothesis.origin.tolist()
|
||||
payload.append(item)
|
||||
return payload
|
||||
Reference in New Issue
Block a user