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:
21
.gitignore
vendored
21
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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"]
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`。
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user