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:
@@ -0,0 +1,128 @@
|
||||
"""Generate synthetic test point clouds at varying injected panel-deflection
|
||||
angles. Each cloud is produced by the *existing* CAD synthesis path
|
||||
(parse → sample → deflect → noise) **with the scanner-pose jitter step
|
||||
turned off**, so each PLY already lives in the CAD frame — which is the
|
||||
contract the main measurement pipeline expects (no registration step).
|
||||
|
||||
Each PLY is paired with one row in ``manifest.json`` so the batch
|
||||
evaluator knows the GT angle to compare against.
|
||||
|
||||
Run::
|
||||
|
||||
python examples/generate_test_clouds.py \
|
||||
--stp test_data/lu1.stp \
|
||||
--out test_data/synth \
|
||||
--profile lu1_af4
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from coupler_panel.config import load_config, resolve_auto
|
||||
from coupler_panel.data_prep import parse_cad, synthesize_cloud
|
||||
from coupler_panel.io_utils import write_point_cloud, write_json
|
||||
|
||||
|
||||
# (theta_right_deg, theta_ref_deg) pairs — mirrors the spirit of the
|
||||
# existing lu1_deflection_sweep matrix.
|
||||
DEFAULT_INJECTIONS = [
|
||||
(0.0, 0.0),
|
||||
(0.1, 0.0),
|
||||
(0.3, 0.0),
|
||||
(0.5, 0.0),
|
||||
(1.0, 0.0),
|
||||
(2.0, 0.0),
|
||||
(0.0, 0.5),
|
||||
(0.5, 0.5),
|
||||
(1.0, 1.0),
|
||||
]
|
||||
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--stp", type=Path, required=True, help="Path to the panel STEP file.")
|
||||
p.add_argument("--out", type=Path, required=True, help="Output directory for PLYs + manifest.")
|
||||
p.add_argument("--profile", type=str, default="lu1_af4", help="Config profile.")
|
||||
p.add_argument(
|
||||
"--config",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Optional YAML override; takes precedence over --profile.",
|
||||
)
|
||||
return p
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
log = logging.getLogger("generate_test_clouds")
|
||||
args = _build_parser().parse_args(argv)
|
||||
|
||||
cfg = load_config(args.config, profile=args.profile)
|
||||
cfg = resolve_auto(cfg)
|
||||
|
||||
log.info("parsing CAD: %s", args.stp)
|
||||
parsed = parse_cad(args.stp, cfg["cad"])
|
||||
|
||||
out_dir = args.out
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# CAD nominal GT — kept as an audit reference (not consumed by the
|
||||
# main pipeline, which only needs the STP).
|
||||
write_json(parsed.gt.to_dict(), out_dir / "gt.json")
|
||||
|
||||
manifest: list[dict] = []
|
||||
for i, (th_r, th_z) in enumerate(DEFAULT_INJECTIONS):
|
||||
cfg_i = dict(cfg)
|
||||
cfg_i["panel_deflection"] = {
|
||||
"enabled": True,
|
||||
"theta_right_deg": float(th_r),
|
||||
"theta_ref_deg": float(th_z),
|
||||
}
|
||||
# Scanner-pose jitter is OFF: the cloud must already be in the
|
||||
# CAD frame because the main pipeline does not register.
|
||||
cfg_i["transform"] = {"enabled": False}
|
||||
|
||||
log.info("[%d/%d] synthesising θ_right=%+.2f° θ_ref=%+.2f°",
|
||||
i + 1, len(DEFAULT_INJECTIONS), th_r, th_z)
|
||||
synth = synthesize_cloud(
|
||||
parsed,
|
||||
sampling_cfg=cfg_i["sampling"],
|
||||
noise_cfg=cfg_i.get("noise", {}),
|
||||
transform_cfg=cfg_i["transform"],
|
||||
scan_cfg=cfg_i.get("scan_simulator", {}),
|
||||
deflection_cfg=cfg_i["panel_deflection"],
|
||||
)
|
||||
sample_id = (
|
||||
f"r{th_r:+.2f}_z{th_z:+.2f}"
|
||||
.replace("+", "p").replace("-", "n").replace(".", "")
|
||||
)
|
||||
ply_path = out_dir / f"cloud_{sample_id}.ply"
|
||||
# synth.transformed == synth.noisy when transform is disabled.
|
||||
write_point_cloud(synth.transformed, ply_path)
|
||||
|
||||
manifest.append({
|
||||
"sample_id": sample_id,
|
||||
"ply": ply_path.name,
|
||||
"theta_right_deg": float(th_r),
|
||||
"theta_ref_deg": float(th_z),
|
||||
"n_points": int(synth.transformed.shape[0]),
|
||||
})
|
||||
|
||||
write_json(
|
||||
{
|
||||
"stp": str(args.stp),
|
||||
"profile": args.profile,
|
||||
"gt": "gt.json",
|
||||
"samples": manifest,
|
||||
},
|
||||
out_dir / "manifest.json",
|
||||
)
|
||||
log.info("done: %d clouds + gt.json + manifest.json under %s", len(manifest), out_dir)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Minimal integration example for software developers.
|
||||
|
||||
This script shows the *only* code path a production caller needs:
|
||||
|
||||
load a clean, registered point cloud → call the API → use the result
|
||||
|
||||
Run::
|
||||
|
||||
python examples/integrate_minimal.py path/to/scan.ply
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from coupler_panel import (
|
||||
CouplerError,
|
||||
compute_coupler_measurement,
|
||||
load_point_cloud,
|
||||
)
|
||||
|
||||
|
||||
def main(input_path: Path) -> int:
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
log = logging.getLogger("integrate_minimal")
|
||||
|
||||
points = load_point_cloud(input_path)
|
||||
log.info("loaded %d points from %s", points.shape[0], input_path)
|
||||
|
||||
try:
|
||||
result = compute_coupler_measurement(
|
||||
points,
|
||||
progress=lambda s, f: log.info("[%5.1f%%] %s", f * 100, s),
|
||||
)
|
||||
except CouplerError as exc:
|
||||
log.error("measurement failed: %s", exc)
|
||||
return 2
|
||||
|
||||
print(f"theta_right_deg = {result.theta_right_deg:+.4f}")
|
||||
print(f"theta_ref_deg = {result.theta_ref_deg:+.4f}")
|
||||
print(f"D_y_max_mm = {result.D_y_max_mm:.3f}")
|
||||
print(f"D_z_max_mm = {result.D_z_max_mm:.3f}")
|
||||
print(f"dx_y_max_mm = {result.dx_y_max_mm:.3f}")
|
||||
print(f"dx_z_max_mm = {result.dx_z_max_mm:.3f}")
|
||||
print(f"center_C = {tuple(round(c, 3) for c in result.center_C)}")
|
||||
print(f"normal = {tuple(round(c, 6) for c in result.normal)}")
|
||||
print(f"quality_flags = {result.quality_flags}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__.strip())
|
||||
sys.exit(1)
|
||||
sys.exit(main(Path(sys.argv[1])))
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"x_min": -12000.0,
|
||||
"x_max": 100.0,
|
||||
"y_min": -300.0,
|
||||
"y_max": 1200.0,
|
||||
"z_min": -1000.0,
|
||||
"z_max": 800.0
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"x_min": 11526.0,
|
||||
"x_max": 11733.0,
|
||||
"y_min": 588.0,
|
||||
"y_max": 1192.0,
|
||||
"z_min": -950.0,
|
||||
"z_max": 950.0,
|
||||
"comment": "Coupler mounting panel ROI on train_2 full-vehicle scan. Dataset frame: x=length, y=height, z=width. These six numbers are the cropping hyper-parameters consumed directly by the algorithm."
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Batch evaluator: main measurement program with CAD + cloud + ROI inputs.
|
||||
|
||||
For each PLY listed in the manifest:
|
||||
|
||||
1. Load the cloud (PLY).
|
||||
2. Apply the ROI JSON (currently ``mode='all'`` placeholder).
|
||||
3. Feed (CAD STP, ROI'd cloud, GT JSON) into the existing measurement
|
||||
pipeline — internal stages (denoise / Kabsch / detection / ROI /
|
||||
plane fit / metrics) are unchanged.
|
||||
4. Compare θ_right / θ_ref against the injected GT recorded in the
|
||||
manifest.
|
||||
5. Append a row to the Excel report.
|
||||
|
||||
Run::
|
||||
|
||||
python examples/run_batch_eval.py \
|
||||
--cad test_data/lu1.stp \
|
||||
--data test_data/synth \
|
||||
--roi examples/roi_all.json \
|
||||
--out runs/batch_eval \
|
||||
--profile lu1_af4
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from coupler_panel.exceptions import CouplerError
|
||||
from coupler_panel.io_utils import load_point_cloud
|
||||
from coupler_panel.pipeline import run_coupler_measurement_pipeline
|
||||
from coupler_panel.reporting import build_row_from_summary, write_excel_report
|
||||
from coupler_panel.roi_select import load_roi_json
|
||||
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--cad", type=Path, required=True, help="STEP file (panel CAD).")
|
||||
p.add_argument("--data", type=Path, required=True, help="Directory with PLYs + manifest.json.")
|
||||
p.add_argument("--roi", type=Path, required=True, help="ROI JSON file.")
|
||||
p.add_argument("--out", type=Path, required=True, help="Output directory for runs + xlsx.")
|
||||
p.add_argument("--profile", type=str, default="lu1_af4", help="Config profile.")
|
||||
p.add_argument("--config", type=Path, default=None, help="Optional YAML override.")
|
||||
return p
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
log = logging.getLogger("run_batch_eval")
|
||||
args = _build_parser().parse_args(argv)
|
||||
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
manifest_path = args.data / "manifest.json"
|
||||
with manifest_path.open("r", encoding="utf-8") as fh:
|
||||
manifest = json.load(fh)
|
||||
samples = manifest["samples"]
|
||||
roi_spec = load_roi_json(args.roi)
|
||||
log.info(
|
||||
"loaded ROI spec (%s): x∈[%g,%g] y∈[%g,%g] z∈[%g,%g]",
|
||||
roi_spec.kind,
|
||||
roi_spec.x_min, roi_spec.x_max,
|
||||
roi_spec.y_min, roi_spec.y_max,
|
||||
roi_spec.z_min, roi_spec.z_max,
|
||||
)
|
||||
log.info("samples in manifest: %d", len(samples))
|
||||
|
||||
rows = []
|
||||
for i, sample in enumerate(samples, start=1):
|
||||
sid = sample["sample_id"]
|
||||
ply = args.data / sample["ply"]
|
||||
log.info("[%d/%d] %s — %s", i, len(samples), sid, ply.name)
|
||||
|
||||
run_dir = args.out / sid
|
||||
try:
|
||||
# Pre-count points to record n_points_in (before ROI).
|
||||
n_in = int(load_point_cloud(ply).shape[0])
|
||||
|
||||
# Main pipeline: CAD STP is parsed for the panel-normal seed
|
||||
# only; the cloud is assumed to be in the CAD frame already
|
||||
# (scanner extrinsics calibrated upstream). No Kabsch.
|
||||
# The wall-clock duration from "inputs enter the algorithm"
|
||||
# to "result returned" goes straight into the Excel report.
|
||||
t_start = time.perf_counter()
|
||||
result = run_coupler_measurement_pipeline(
|
||||
input_cloud=ply,
|
||||
out_dir=run_dir,
|
||||
config_path=args.config,
|
||||
profile=args.profile,
|
||||
cad_stp_path=args.cad,
|
||||
roi_spec=roi_spec,
|
||||
)
|
||||
pipeline_seconds = time.perf_counter() - t_start
|
||||
except CouplerError as exc:
|
||||
log.error(" -> FAILED: %s", exc)
|
||||
continue
|
||||
except Exception as exc:
|
||||
log.exception(" -> CRASH: %s", exc)
|
||||
continue
|
||||
|
||||
# n_after_roi is recorded in run_dir/roi.json.diagnostics.n_out
|
||||
roi_diag_path = run_dir / "roi.json"
|
||||
n_after = n_in
|
||||
if roi_diag_path.exists():
|
||||
with roi_diag_path.open("r", encoding="utf-8") as fh:
|
||||
n_after = int(json.load(fh).get("diagnostics", {}).get("n_out", n_in))
|
||||
|
||||
# Kabsch RMSE: pull from the prepared/registration log if present
|
||||
# (the persisted pipeline does not currently expose this in the
|
||||
# report dict; it's available via report.json side-files but for a
|
||||
# minimum-touch wiring we leave it None when not surfaced).
|
||||
registration = None
|
||||
|
||||
row = build_row_from_summary(
|
||||
sample_id=sid,
|
||||
ply_path=str(ply),
|
||||
roi_kind=roi_spec.kind,
|
||||
measured=result.to_dict(),
|
||||
gt_angles=(float(sample["theta_right_deg"]), float(sample["theta_ref_deg"])),
|
||||
registration=registration,
|
||||
n_points_in=n_in,
|
||||
n_points_after_roi=n_after,
|
||||
pipeline_seconds=pipeline_seconds,
|
||||
)
|
||||
rows.append(row)
|
||||
log.info(" θ_right pred=%+.4f° GT=%+.2f° err=%+.4f°",
|
||||
row.theta_right_pred_deg, row.theta_right_gt_deg, row.theta_right_err_deg)
|
||||
log.info(" θ_ref pred=%+.4f° GT=%+.2f° err=%+.4f°",
|
||||
row.theta_ref_pred_deg, row.theta_ref_gt_deg, row.theta_ref_err_deg)
|
||||
log.info(" runtime: %.3f s (inner stages: %.3f s)",
|
||||
row.pipeline_seconds, row.inner_stage_seconds)
|
||||
|
||||
if not rows:
|
||||
log.error("no successful runs; nothing to write")
|
||||
return 2
|
||||
|
||||
xlsx_path = args.out / "eval_report.xlsx"
|
||||
write_excel_report(rows, xlsx_path)
|
||||
log.info("wrote %s (%d rows)", xlsx_path, len(rows))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Full research demo: STP → synthetic cloud → measure → diff GT.
|
||||
|
||||
Run::
|
||||
|
||||
python examples/run_full_validation.py path/to/panel.stp runs/demo
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from coupler_panel.pipeline import run_from_cad_pipeline
|
||||
|
||||
|
||||
def main(stp: Path, out: Path) -> int:
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
summary = run_from_cad_pipeline(
|
||||
stp_path=stp,
|
||||
out_dir=out,
|
||||
progress=lambda s, f: print(f"[{f*100:5.1f}%] {s}", flush=True),
|
||||
)
|
||||
print("\n=== diff (measured - GT) ===")
|
||||
print(json.dumps(summary["diff"], indent=2))
|
||||
|
||||
# Render diagnostic PNGs.
|
||||
try:
|
||||
from coupler_panel.viz import render_all
|
||||
|
||||
render_all(out)
|
||||
except ImportError:
|
||||
print("(matplotlib not installed; skip rendering)")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print(__doc__.strip())
|
||||
sys.exit(1)
|
||||
sys.exit(main(Path(sys.argv[1]), Path(sys.argv[2])))
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Integration entry point — the contract for the software integrator.
|
||||
|
||||
Three input files, one Excel output:
|
||||
|
||||
--roi ROI JSON (6-key AABB crop hyper-parameters, in the cloud frame)
|
||||
--cloud full-vehicle point cloud PLY (may be multi-GB; cropped by streaming)
|
||||
--stp theoretical CAD STEP file (recorded for audit/traceability)
|
||||
--out output directory (gets eval_report.xlsx + intermediates)
|
||||
|
||||
The cloud is cropped to the ROI inside the algorithm (no external
|
||||
pre-processing needed), the coupler panel is measured, and the two
|
||||
deflection angles + intermediate values are written to an .xlsx report.
|
||||
|
||||
Example::
|
||||
|
||||
python examples/run_integration.py \
|
||||
--roi examples/roi_train2_coupler.json \
|
||||
--cloud E:/BaiduNetdiskDownload/pointsCloud_data/train_2_1T.ply \
|
||||
--stp E:/BaiduNetdiskDownload/pointsCloud_data/m000004802418tc01_asm.stp \
|
||||
--out runs/train2_integration \
|
||||
--profile train2_coupler
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
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.roi_select import load_roi_json
|
||||
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--roi", type=Path, required=True, help="ROI JSON (6-key AABB).")
|
||||
p.add_argument("--cloud", type=Path, required=True, help="Full-vehicle point cloud PLY.")
|
||||
p.add_argument("--stp", type=Path, default=None, help="Theoretical CAD STEP (audit).")
|
||||
p.add_argument("--out", type=Path, required=True, help="Output directory.")
|
||||
p.add_argument("--profile", type=str, default="train2_coupler",
|
||||
help="Config profile (default: train2_coupler).")
|
||||
p.add_argument("--config", type=Path, default=None, help="Optional YAML override.")
|
||||
p.add_argument("--gt-theta-right", type=float, default=0.0,
|
||||
help="Known/expected theta_right (deg) for the error column. Default 0.")
|
||||
p.add_argument("--gt-theta-ref", type=float, default=0.0,
|
||||
help="Known/expected theta_ref (deg) for the error column. Default 0.")
|
||||
p.add_argument("--sample-id", type=str, default=None, help="Row label in the report.")
|
||||
return p
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
log = logging.getLogger("run_integration")
|
||||
args = _build_parser().parse_args(argv)
|
||||
|
||||
roi_spec = load_roi_json(args.roi)
|
||||
log.info("ROI bbox: x[%g,%g] y[%g,%g] z[%g,%g]",
|
||||
roi_spec.x_min, roi_spec.x_max, roi_spec.y_min,
|
||||
roi_spec.y_max, roi_spec.z_min, roi_spec.z_max)
|
||||
log.info("cloud: %s", args.cloud)
|
||||
log.info("stp (audit): %s", args.stp)
|
||||
|
||||
sample_id = args.sample_id or args.cloud.stem
|
||||
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
result = run_coupler_measurement_pipeline(
|
||||
input_cloud=args.cloud,
|
||||
out_dir=args.out,
|
||||
config_path=args.config,
|
||||
profile=args.profile,
|
||||
roi_spec=roi_spec,
|
||||
progress=lambda s, f: log.info("[%5.1f%%] %s", f * 100, s),
|
||||
)
|
||||
except CouplerError as exc:
|
||||
log.error("measurement failed: %s", exc)
|
||||
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))
|
||||
|
||||
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,
|
||||
)
|
||||
# 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)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user