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

21
.gitignore vendored
View File

@@ -143,5 +143,22 @@ alg/点云测距v1.1/train_measurement_system/results/
**/dist-info/
# 算法运行中间产物
alg/delivery_v1.2/coupler_panel_release/runs/
alg/delivery_v1.2/underframe_deflection/runs/
alg/delivery_v1.2.1/coupler_panel_release/runs/
alg/delivery_v1.2.1/underframe_deflection/runs/
# ─── 平整度/平面度/轮廓测量v1.5(大文件排除)────────────────────────────────────
alg/平整度、平面度、轮廓测量v1.5/*.zip
alg/平整度、平面度、轮廓测量v1.5/datasets/
alg/平整度、平面度、轮廓测量v1.5/vis/
alg/平整度、平面度、轮廓测量v1.5/results/
alg/平整度、平面度、轮廓测量v1.5/**/*.ply
# ─── train_measurement_system_v2.2(大文件排除)─────────────────────────────────
alg/train_measurement_system_v2.2/**/*.ply
alg/train_measurement_system_v2.2/**/results/
alg/train_measurement_system_v2.2/**/visualizations*/
# ─── 整车配准v1.3(大文件排除)──────────────────────────────────────────────────
alg/整车配准v1.3/**/*.ply
alg/整车配准v1.3/datasets/
alg/整车配准v1.3/results/

View File

@@ -92,6 +92,15 @@ RUN PY_VER=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.versi
&& SYS_SP=$(python3 -c "import site; print(site.getsitepackages()[0])") \
&& ln -sf "${CONDA_SP}/OCC" "${SYS_SP}/OCC"
# conda-forge 编译的 .so 需要新版 libstdc++(CXXABI_1.3.15);
# 动态查找 conda 提供的最新真实文件,避免硬编码版本号或双重软链问题
RUN set -eux \
&& LIBSTDCPP=$(ls /opt/conda/lib/libstdc++.so.6.0.* 2>/dev/null | sort -V | tail -1) \
&& [ -n "$LIBSTDCPP" ] && ln -sf "$LIBSTDCPP" /usr/lib/x86_64-linux-gnu/libstdc++.so.6 \
&& echo "libstdc++ -> $LIBSTDCPP" \
&& python3 -c "from OCC.Core.STEPControl import STEPControl_Reader; print('pythonocc-core OK')" \
&& python3 -c "from OCC.Core.BRep import BRep_Builder; print('OCC BRep OK')"
ENV PYTHON3_PATH=/usr/bin/python3
# 创建非 root 用户(bookworm 用 groupadd/useradd)

View File

@@ -0,0 +1,12 @@
"""Batch-evaluation reporting helpers (xlsx output)."""
from .excel import EvalRow, write_excel_report, build_row_from_summary
from .inspection import InspectionItem, write_inspection_report
__all__ = [
"EvalRow",
"write_excel_report",
"build_row_from_summary",
"InspectionItem",
"write_inspection_report",
]

View File

@@ -0,0 +1,124 @@
"""Inspection-item Excel reports (项点报表).
Each report is one measurement item with the six columns the quality
department expects:
项点名称 | 理论尺寸 | 公差要求 | 测量结果 | 误差结果 | 测量结论
For the coupler panel, the two deflection angles are reported in a
**slope form** ``value/L mm`` (e.g. ``0.8/375mm``) rather than degrees,
because the tolerance is specified that way. ``value`` is the lateral
offset over a baseline of ``L`` mm:
value = L * tan(theta) (theta in radians)
so a measured angle ``theta`` becomes ``(L*tan theta)/L mm``. The
tolerance ``T/L mm`` is the angle whose offset over ``L`` is ``T``; the
result passes when ``|L*tan(theta)| <= T``.
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from pathlib import Path
try:
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill
except ImportError as exc: # pragma: no cover
raise ImportError(
"openpyxl is required for Excel reporting; install with `pip install openpyxl`"
) from exc
COLUMNS = ("项点名称", "理论尺寸", "公差要求", "测量结果", "误差结果", "测量结论")
@dataclass
class InspectionItem:
"""One deflection-angle inspection item, reported in ``value/L mm`` form."""
name: str # 项点名称
theta_deg: float # measured angle (degrees), theory = 0
baseline_mm: float # L in the slope form
tol_value_mm: float # tolerance numerator T (so tolerance = T/L mm)
# --- derived ---
@property
def offset_mm(self) -> float:
"""L * tan(theta): lateral offset over the baseline, mm."""
return self.baseline_mm * math.tan(math.radians(self.theta_deg))
@property
def theoretical_str(self) -> str:
return f"0/{self.baseline_mm:g}mm"
@property
def tolerance_str(self) -> str:
return f"{self.tol_value_mm:g}/{self.baseline_mm:g}mm"
@property
def measured_str(self) -> str:
return f"{self.offset_mm:.6f}/{self.baseline_mm:g}mm"
@property
def error_str(self) -> str:
# Error == measured result here (theory is 0).
return self.measured_str
@property
def passed(self) -> bool:
return abs(self.offset_mm) <= self.tol_value_mm
@property
def conclusion_str(self) -> str:
return "合格" if self.passed else "不合格"
def row(self) -> tuple[str, str, str, str, str, str]:
return (
self.name,
self.theoretical_str,
self.tolerance_str,
self.measured_str,
self.error_str,
self.conclusion_str,
)
def write_inspection_report(item: InspectionItem, path: str | Path) -> Path:
"""Write a single-item inspection report to an xlsx file."""
out = Path(path)
out.parent.mkdir(parents=True, exist_ok=True)
wb = Workbook()
ws = wb.active
ws.title = "inspection"
header_font = Font(bold=True)
header_fill = PatternFill("solid", fgColor="DDDDDD")
center = Alignment(horizontal="center", vertical="center")
for c, label in enumerate(COLUMNS, start=1):
cell = ws.cell(row=1, column=c, value=label)
cell.font = header_font
cell.fill = header_fill
cell.alignment = center
for c, val in enumerate(item.row(), start=1):
cell = ws.cell(row=2, column=c, value=val)
cell.alignment = center
# red font for 不合格 conclusion
if not item.passed:
ws.cell(row=2, column=6).font = Font(color="CC0000", bold=True)
widths = (44, 12, 14, 16, 16, 12)
for c, w in enumerate(widths, start=1):
ws.column_dimensions[ws.cell(row=1, column=c).column_letter].width = w
wb.save(out)
return out
__all__ = ["InspectionItem", "write_inspection_report", "COLUMNS"]

View File

@@ -0,0 +1,9 @@
{
"x_min": -11753.0,
"x_max": -11506.0,
"y_min": -950.0,
"y_max": 950.0,
"z_min": 788.0,
"z_max": 1192.0,
"comment": "Coupler mounting panel ROI on train_1 full-vehicle scan (m000004802409t02_asm). Six numbers are the cropping hyper-parameters consumed directly by the algorithm; coordinates are in the cloud's own frame."
}

View File

@@ -25,12 +25,15 @@ from __future__ import annotations
import argparse
import logging
import math
import time
from pathlib import Path
import numpy as np
from coupler_panel.exceptions import CouplerError
from coupler_panel.pipeline import run_coupler_measurement_pipeline
from coupler_panel.reporting import build_row_from_summary, write_excel_report
from coupler_panel.reporting import InspectionItem, write_inspection_report
from coupler_panel.roi_select import load_roi_json
@@ -80,32 +83,33 @@ def main(argv: list[str] | None = None) -> int:
return 2
pipeline_seconds = time.perf_counter() - t0
# n_points after ROI crop is recorded in <out>/roi.json diagnostics.
import json as _json
roi_diag = _json.loads((args.out / "roi.json").read_text(encoding="utf-8"))
n_roi = int(roi_diag.get("diagnostics", {}).get("n_out", 0))
n_in = int(roi_diag.get("diagnostics", {}).get("n_in", n_roi))
# --- Raw (pre-clamp) deflection angles from the fitted plane normal ----
# Use the un-clamped normal so a tiny non-zero angle is NOT rounded to 0.
n = np.asarray(result.report["plane"]["normal"], dtype=np.float64)
theta_right_deg = math.degrees(math.atan2(n[1], n[0])) # 距纵向中心 (y)
theta_ref_deg = math.degrees(math.atan2(n[2], n[0])) # 距参考面 (z)
row = build_row_from_summary(
sample_id=sample_id,
ply_path=str(args.cloud),
roi_kind=f"aabb {roi_spec.x_min:.0f}-{roi_spec.x_max:.0f},"
f"{roi_spec.y_min:.0f}-{roi_spec.y_max:.0f},"
f"{roi_spec.z_min:.0f}-{roi_spec.z_max:.0f}",
measured=result.to_dict(),
gt_angles=(args.gt_theta_right, args.gt_theta_ref),
registration=None,
n_points_in=n_in,
n_points_after_roi=n_roi,
pipeline_seconds=pipeline_seconds,
# --- Two inspection-item Excel reports (slope form value/L mm) ---------
item_ref = InspectionItem(
name="车钩面板距参考面的右偏转角(头车一位端处车钩)",
theta_deg=theta_ref_deg,
baseline_mm=375.0,
tol_value_mm=0.8, # 公差 0.8/375mm
)
item_long = InspectionItem(
name="车钩面板距纵向中心的右偏转角(头车一位端处车钩)",
theta_deg=theta_right_deg,
baseline_mm=700.0,
tol_value_mm=1.0, # 公差 1/700mm
)
# Record the STP path in the row's extra metadata (audit trail).
row.extra["stp_audit_path"] = str(args.stp) if args.stp else ""
xlsx = write_excel_report([row], args.out / "eval_report.xlsx")
log.info("theta_right=%+.4f theta_ref=%+.4f flags=%s",
result.theta_right_deg, result.theta_ref_deg, result.quality_flags)
log.info("wrote %s", xlsx)
out_ref = write_inspection_report(item_ref, args.out / "report_ref_plane.xlsx")
out_long = write_inspection_report(item_long, args.out / "report_longitudinal.xlsx")
for it, p in ((item_ref, out_ref), (item_long, out_long)):
log.info("%s | 测量结果=%s | 公差=%s | %s",
it.name, it.measured_str, it.tolerance_str, it.conclusion_str)
log.info("wrote %s , %s", out_ref, out_long)
return 0

View File

@@ -132,26 +132,30 @@ report = run_single_beam_pipeline(
---
## 5. 输出 Excel 字段(report.xlsx)
## 5. 输出 Excel(report.xlsx,左右各一份)
### Sheet「Summary」(key/value)
### Sheet「检测报表」(核心交付,6 列)
| 字段 | 含义 |
| 列 | 本项目取值 |
| --- | --- |
| `mode` / `side` | `single_beam` / `left`、`right` |
| `input_cloud` / `roi_source` / `cad_reference` | 三个输入文件的绝对路径 |
| `registration_performed` / `registration_note` | 是否配准 + 说明 |
| `roi_point_count` | ROI 裁剪命中点数 |
| `n_slices` / `n_kept` | 沿车长切片数 / 鲁棒拟合保留的切片数 |
| `reference_slope_mm_per_m` | 拟合参考线斜率 |
| **`selected_signed_deflection_mm`** | **最大绝对挠度(带符号)← 核心结果** |
| `selected_absolute_deflection_mm` | 最大绝对挠度 |
| `selected_x_mm` | 该点的车长位置 |
| `max_downward_mm` / `max_upward_mm` | 最大下凹 / 上凸挠度 |
| `rms_mm` | 挠度 RMS |
| `quality_flags` | 质量警告(空 = 正常) |
| 项点名称 | 纵向梁到直线的垂直偏差(挠度) |
| 理论尺寸 | 0 |
| 公差要求 | 左梁 ±7mm / 右梁 ±12mm |
| 测量结果 | 实测最大挠度(mm) |
| 误差结果 | 与理论 0 的误差(= 测量结果,因理论值为 0) |
| 测量结论 | 误差 ≤ 公差 → **合格**,否则 → **不合格** |
### Sheet「Deflection_Curve」
**左右判定**:面朝 x 正方向,`z > 0` 为左梁(公差 7mm),`z < 0` 为右梁(公差 12mm)。
程序按 ROI 的 z 中心**自动判定**,不依赖文件名——即使左右 ROI 文件传反也不会用错公差。
公差可覆盖:命令行 `--left-tolerance` / `--right-tolerance`(两梁模式)或 `--tolerance`(单梁模式);
Python API 同名参数 `left_tolerance_mm` / `right_tolerance_mm` / `tolerance_mm`。
### Sheet「明细」(溯源,key/value)
输入文件路径、是否配准、ROI 命中点数、切片数、参考线斜率、最大下凹/上凸、RMS、质量警告等。
### Sheet「挠度曲线」
沿车长逐点表:`x_mm, underside_height_mm, reference_height_mm, deflection_mm, kept`。

View File

@@ -56,7 +56,27 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
"--side",
default=None,
help="Single mode label for --roi (left/right); inferred from the file name if omitted.",
help="Single mode label for --roi (left/right); inferred from the ROI's "
"lateral z (x-forward: z>0 left, z<0 right) if omitted.",
)
parser.add_argument(
"--tolerance",
type=float,
default=None,
help="Single mode --roi: pass/fail tolerance in mm. Default by side "
"(left 7, right 12).",
)
parser.add_argument(
"--left-tolerance",
type=float,
default=None,
help="Both-beam mode: left-beam tolerance in mm (default 7).",
)
parser.add_argument(
"--right-tolerance",
type=float,
default=None,
help="Both-beam mode: right-beam tolerance in mm (default 12).",
)
parser.add_argument(
"--enable-registration",
@@ -135,6 +155,8 @@ def main() -> None:
config=config,
cad_path=args.cad,
enable_registration=args.enable_registration,
left_tolerance_mm=args.left_tolerance,
right_tolerance_mm=args.right_tolerance,
skip_excel=args.skip_excel,
skip_plot=args.skip_plot,
)
@@ -144,6 +166,7 @@ def main() -> None:
roi_path=args.roi,
out_dir=args.out_dir,
side=args.side,
tolerance_mm=args.tolerance,
config=config,
cad_path=args.cad,
enable_registration=args.enable_registration,

Some files were not shown because too many files have changed in this diff Show More